feat: responsive landing page

This commit is contained in:
Henry
2025-02-15 15:46:40 +00:00
parent 3a2942886d
commit 85f6dd1ff8
5 changed files with 141 additions and 58 deletions

View File

@@ -40,7 +40,7 @@ const FeatureItem = ({
return (
<div
onMouseEnter={handleMouseEnter}
className="group relative flex h-56 w-56 flex-col items-center justify-center overflow-hidden rounded-3xl border border-light-200 bg-light-50 dark:border-dark-200 dark:bg-dark-50"
className="group relative flex aspect-square h-auto w-full flex-col items-center justify-center overflow-hidden rounded-3xl border border-light-200 bg-light-50 dark:border-dark-200 dark:bg-dark-50 md:h-56 md:w-56"
>
<div className="absolute left-8 top-8 h-2 w-2 rounded-full bg-light-200 dark:bg-dark-200" />
<div className="absolute right-8 top-8 h-2 w-2 rounded-full bg-light-200 dark:bg-dark-200" />
@@ -52,10 +52,10 @@ const FeatureItem = ({
</div>
<div className="relative mt-2 w-full px-4 text-center">
<p className="text-sm font-bold text-light-1000 transition-opacity duration-200 group-hover:opacity-0 dark:text-dark-1000">
<p className="text-sm font-bold text-light-1000 dark:text-dark-1000 sm:transition-opacity sm:duration-200 sm:group-hover:opacity-0">
{feature.title}
</p>
<p className="absolute inset-0 px-4 text-sm text-light-950 opacity-0 transition-opacity duration-200 group-hover:opacity-100 dark:text-dark-900">
<p className="mt-2 text-sm text-light-950 dark:text-dark-900 sm:absolute sm:inset-0 sm:mt-0 sm:opacity-0 sm:transition-opacity sm:duration-200 sm:group-hover:opacity-100">
{feature.description}
</p>
</div>
@@ -119,27 +119,31 @@ const Features = ({ theme }: { theme: "light" | "dark" }) => {
return (
<>
<div className="flex flex-col items-center justify-center pb-24">
<div className="flex items-center gap-2 rounded-full border bg-light-50 px-4 py-1 text-center text-sm text-light-1000 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900">
<div className="flex flex-col items-center justify-center px-4 pb-24">
<div className="flex items-center gap-2 rounded-full border bg-light-50 px-4 py-1 text-center text-xs text-light-1000 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900 lg:text-sm">
<p>Features</p>
</div>
<p className="mt-2 text-center text-4xl font-bold text-light-1000 dark:text-dark-1000">
<p className="mt-2 text-center text-3xl font-bold text-light-1000 dark:text-dark-1000 lg:text-4xl">
Kanban simplified
</p>
<p className="mt-3 max-w-[600px] text-center text-lg text-dark-900">
<p className="text-md mt-3 max-w-[600px] text-center text-dark-900 lg:text-lg">
Simple, visual task management that just works. Drag and drop cards,
collaborate with your team, and get more done.
</p>
<div className="mt-16 grid grid-cols-4 gap-6 [mask-image:linear-gradient(to_bottom,black_80%,transparent_100%)]">
<div className="mt-16 grid w-full grid-cols-1 gap-6 [mask-image:linear-gradient(to_bottom,black_92%,transparent_100%)] sm:grid-cols-2 md:grid-cols-3 md:[mask-image:linear-gradient(to_bottom,black_85%,transparent_100%)] lg:grid-cols-4 lg:[mask-image:linear-gradient(to_bottom,black_85%,transparent_100%)]">
{features.map((feature, index) => {
return <FeatureItem key={`feature-${index}`} feature={feature} />;
return (
<div className="aspect-square w-full" key={`feature-${index}`}>
<FeatureItem feature={feature} />
</div>
);
})}
</div>
<div>
<div className="mt-8 flex items-center gap-2 rounded-full border bg-light-50 px-4 py-1 text-center text-sm text-light-1000 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900">
<p>
<p className="text-xs lg:text-sm">
{`We're just getting started. `}
<Link href="/kan/roadmap" className="underline">
View our roadmap.

View File

@@ -80,7 +80,7 @@ const Footer = () => {
</ul>
</div>
<div>
<h3 className="text-sm/6 font-semibold text-light-1000 dark:text-dark-1000">
<h3 className="mt-10 text-sm/6 font-semibold text-light-1000 dark:text-dark-1000 md:mt-0">
Company
</h3>
<ul role="list" className="mt-6 space-y-4">
@@ -116,7 +116,7 @@ const Footer = () => {
</ul>
</div>
<div>
<h3 className="text-sm/6 font-semibold text-light-1000 dark:text-dark-1000">
<h3 className="mt-10 text-sm/6 font-semibold text-light-1000 dark:text-dark-1000 md:mt-0">
Legal
</h3>
<ul role="list" className="mt-6 space-y-4">

View File

@@ -6,6 +6,14 @@ import Button from "~/components/Button";
const Header = ({ isLoggedIn }: { isLoggedIn: boolean }) => {
const [isScrolled, setIsScrolled] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const menuItems = [
{ label: "Roadmap", href: "/kan/roadmap", openInNewTab: true },
{ label: "Features", href: "#features" },
{ label: "Pricing", href: "#pricing" },
{ label: "Docs", href: "https://docs.kanbn.com", openInNewTab: true },
];
useEffect(() => {
const handleScroll = () => {
@@ -15,42 +23,72 @@ const Header = ({ isLoggedIn }: { isLoggedIn: boolean }) => {
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const toggleMenu = () => {
setIsMenuOpen(!isMenuOpen);
document.body.style.overflow = !isMenuOpen ? "hidden" : "";
};
return (
<>
<div
className={twMerge(
"z-50 m-auto flex w-full max-w-[1100px] px-4 pt-4",
isScrolled && "fixed",
"fixed z-50 m-auto flex w-full transition-all duration-500 lg:max-w-[1100px] lg:px-4 lg:pt-4",
)}
>
<div
className={twMerge(
"m-auto flex h-[4rem] min-h-[4rem] w-full rounded-3xl border border-transparent px-5 py-2 align-middle transition-colors duration-200",
"m-auto flex h-[4rem] min-h-[4rem] w-full px-5 py-2 align-middle transition-all duration-500 lg:rounded-3xl lg:border lg:border-transparent",
isScrolled &&
"rounded-2xl border border-light-300 bg-dark-100 bg-light-50/80 shadow-sm backdrop-blur-[10px] dark:border-dark-300 dark:bg-dark-50/90",
"border-b border-light-300 bg-light-50/80 opacity-100 shadow-sm backdrop-blur-[10px] dark:border-dark-300 dark:bg-dark-50/90 lg:rounded-2xl lg:border",
!isScrolled && "bg-transparent opacity-90",
)}
>
<div className="flex w-full items-center justify-between px-2">
<div className="my-auto flex items-center justify-between pl-2">
<div className="flex w-full items-center justify-between lg:px-4">
<div className="my-auto flex items-center justify-between">
<h1 className="w-[200px] text-lg font-bold tracking-tight text-neutral-900 dark:text-dark-1000">
kan.bn
</h1>
</div>
<div className="flex justify-center gap-10 dark:text-dark-1000">
<Link href="/kan/roadmap" className="text-sm font-bold">
Roadmap
</Link>
<Link href="#features" className="text-sm font-bold">
Features
</Link>
<Link href="#pricing" className="text-sm font-bold">
Pricing
</Link>
<Link href="/docs" className="text-sm font-bold">
Docs
</Link>
{/* Desktop Menu */}
<div className="hidden justify-center gap-10 dark:text-dark-1000 lg:flex">
{menuItems.map((item) => (
<Link
key={item.label}
href={item.href}
target={item.openInNewTab ? "_blank" : undefined}
rel={item.openInNewTab ? "noopener noreferrer" : undefined}
className="text-sm font-bold"
>
{item.label}
</Link>
))}
</div>
<div className="flex w-[200px] justify-end gap-2">
{/* Hamburger Menu Button */}
<button
onClick={toggleMenu}
className="z-50 p-2 lg:hidden"
aria-label="Toggle menu"
>
<div
className={twMerge(
"my-[5px] h-[1.5px] w-4 bg-current bg-light-1000 transition-all dark:bg-dark-1000",
isMenuOpen ? "translate-y-[6.5px] rotate-45" : "",
)}
/>
<div
className={twMerge(
"my-[5px] h-[1.5px] w-3 bg-current bg-light-1000 transition-all dark:bg-dark-1000",
isMenuOpen ? "opacity-0" : "",
)}
/>
<div
className={twMerge(
"my-[5px] h-[1.5px] w-4 bg-current bg-light-1000 transition-all dark:bg-dark-1000",
isMenuOpen ? "-translate-y-[6.5px] -rotate-45" : "",
)}
/>
</button>
<div className="hidden w-[200px] justify-end gap-2 lg:flex">
{isLoggedIn ? (
<Button href="/boards">Go to app</Button>
) : (
@@ -65,7 +103,46 @@ const Header = ({ isLoggedIn }: { isLoggedIn: boolean }) => {
</div>
</div>
</div>
{isScrolled && <div className="h-[5rem] min-h-[5rem]"></div>}
{/* Mobile Menu Overlay */}
<div
className={`fixed inset-0 z-40 transform transition-all duration-500 lg:hidden ${
isMenuOpen
? "translate-x-0 opacity-100"
: "pointer-events-none translate-x-0 opacity-0"
}`}
>
<div className="absolute inset-0 bg-white dark:bg-dark-100">
<div className="flex h-full flex-col items-center justify-center space-y-8">
{menuItems.map((item) => (
<Link
key={item.label}
href={item.href}
target={item.openInNewTab ? "_blank" : undefined}
rel={item.openInNewTab ? "noopener noreferrer" : undefined}
className="transform text-xl font-bold text-light-1000 transition-all duration-300 hover:scale-105 dark:text-dark-1000"
>
{item.label}
</Link>
))}
<div className="mt-8 flex flex-col gap-4">
{isLoggedIn ? (
<Button href="/boards" onClick={toggleMenu}>
Go to app
</Button>
) : (
<>
<Button href="/login" variant="ghost" onClick={toggleMenu}>
Sign in
</Button>
<Button href="/signup" onClick={toggleMenu}>
Get started
</Button>
</>
)}
</div>
</div>
</div>
</div>
</>
);
};

View File

@@ -1,8 +1,8 @@
import { useState } from "react";
import Link from "next/link";
import { twMerge } from "tailwind-merge";
import { Radio, RadioGroup } from "@headlessui/react";
import { useState } from "react";
import { HiCheckCircle } from "react-icons/hi2";
import { twMerge } from "tailwind-merge";
type Frequency = "monthly" | "annually";
@@ -44,7 +44,7 @@ const tiers = [
id: "tier-teams",
href: "signup",
buttonText: "Get Started",
price: { monthly: "£7.50", annually: "£6" },
price: { monthly: "$10.00", annually: "$8.00" },
description:
"Kanban is better with a team. Perfect for small and growing teams looking to collaborate.",
featureHeader: "Everything in the free plan, plus:",
@@ -83,15 +83,15 @@ const Pricing = () => {
return (
<>
<div className="flex flex-col items-center justify-center pb-10">
<div className="flex items-center gap-2 rounded-full border bg-light-50 px-4 py-1 text-center text-sm text-light-1000 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900">
<div className="flex flex-col items-center justify-center px-4 pb-10">
<div className="flex items-center gap-2 rounded-full border bg-light-50 px-4 py-1 text-center text-xs text-light-1000 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900 lg:text-sm">
<p>Pricing</p>
</div>
<p className="mt-2 text-center text-4xl font-bold text-light-1000 dark:text-dark-1000">
<p className="mt-2 text-center text-3xl font-bold text-light-1000 dark:text-dark-1000 lg:text-4xl">
Simple pricing to fit your needs
</p>
<p className="mt-3 max-w-[600px] text-center text-lg text-dark-900">
<p className="text:md mt-3 max-w-[600px] text-center text-dark-900 lg:text-lg">
Get started for free, with no usage limits. For collaboration, upgrade
to a plan that fits the size of your team.
</p>
@@ -106,7 +106,7 @@ const Pricing = () => {
<Radio
key={option.value}
value={option}
className="cursor-pointer rounded-full px-2.5 py-1 text-light-900 data-[checked]:bg-dark-50 data-[checked]:text-white dark:data-[checked]:bg-light-50 dark:data-[checked]:text-dark-50"
className="cursor-pointer rounded-full px-2.5 py-1 text-xs text-light-900 data-[checked]:bg-dark-50 data-[checked]:text-white dark:data-[checked]:bg-light-50 dark:data-[checked]:text-dark-50 lg:text-sm"
>
{option.label}
</Radio>
@@ -116,7 +116,7 @@ const Pricing = () => {
</div>
</div>
<div className="isolate mx-auto mb-20 grid max-w-md grid-cols-1 gap-8 lg:mx-0 lg:max-w-none lg:grid-cols-3">
<div className="isolate mx-auto mb-20 grid max-w-md grid-cols-1 gap-8 px-4 lg:mx-0 lg:max-w-none lg:grid-cols-3">
{tiers.map((tier) => (
<div
key={tier.id}
@@ -139,7 +139,7 @@ const Pricing = () => {
>
{tier.name}
</h3>
{tier.highlighted && frequency?.value === "annually" ? (
{tier.highlighted && frequency.value === "annually" ? (
<p className="rounded-full bg-light-50 px-2.5 py-1 text-xs/5 font-semibold text-dark-500 dark:bg-dark-50 dark:text-dark-1000">
-20%
</p>

View File

@@ -39,13 +39,13 @@ export default function HomeView() {
background-color: ${!isDarkMode ? "hsl(0deg 0% 98.8%)" : "#161616"};
}
`}</style>
<div className="mx-auto flex h-full min-h-screen flex-col items-center bg-light-100 dark:bg-dark-50">
<div className="mx-auto flex h-full min-h-screen min-w-[375px] flex-col items-center bg-light-100 dark:bg-dark-50">
<PatternedBackground />
<div className="z-10 mx-auto h-full w-full max-w-[1100px]">
<Header isLoggedIn={isLoggedIn} />
<div className="flex h-full w-full flex-col">
<div className="w-full py-32">
<div className="my-10 flex h-full w-full flex-col items-center justify-center">
<div className="flex h-full w-full flex-col lg:pt-[5rem]">
<div className="w-full pb-10 pt-32 lg:py-32">
<div className="my-10 flex h-full w-full flex-col items-center justify-center px-4">
<div className="relative overflow-hidden rounded-full bg-gradient-to-b from-light-300 to-light-400 p-[2px] dark:from-dark-300 dark:to-dark-400">
<div className="gradient-border absolute inset-0 animate-border-spin" />
<div className="relative z-10 rounded-full bg-light-50 dark:bg-dark-50">
@@ -53,7 +53,7 @@ export default function HomeView() {
href="https://github.com/kanbn/kan"
rel="noopener noreferrer"
target="_blank"
className="flex items-center gap-2 px-4 py-1 text-center text-sm text-light-1000 dark:text-dark-1000"
className="flex items-center gap-2 px-4 py-1 text-center text-xs text-light-1000 dark:text-dark-1000 lg:text-sm"
>
Star on Github
<IoLogoGithub size={20} />
@@ -61,12 +61,12 @@ export default function HomeView() {
</div>
</div>
<p className="mt-2 text-center text-5xl font-bold text-light-1000 dark:text-dark-1000">
<p className="mt-2 text-center text-4xl font-bold text-light-1000 dark:text-dark-1000 lg:text-5xl">
The open source <br />
alternative to Trello
</p>
<p className="mt-3 max-w-[600px] text-center text-lg text-dark-900">
<p className="text-md mt-3 max-w-[450px] text-center text-dark-900 lg:max-w-[600px] lg:text-lg">
A powerful, flexible kanban app that helps you organise work,
track progress, and deliver resultsall in one place.
</p>
@@ -86,14 +86,16 @@ export default function HomeView() {
</p>
</div>
</div>
<div className="mb-24 rounded-[24px] border border-light-300 bg-light-50 p-2 shadow-md dark:border-dark-300 dark:bg-dark-100">
<div className="overflow-hidden rounded-[16px] border border-light-300 shadow-sm dark:border-dark-300">
<Image
src={`/hero-${isDarkMode ? "dark" : "light"}.png`}
alt="kanban"
width={1100}
height={1000}
/>
<div className="px-4">
<div className="mb-24 rounded-[24px] border border-light-300 bg-light-50 p-2 shadow-md dark:border-dark-300 dark:bg-dark-100">
<div className="overflow-hidden rounded-[16px] border border-light-300 shadow-sm dark:border-dark-300">
<Image
src={`/hero-${isDarkMode ? "dark" : "light"}.png`}
alt="kanban"
width={1100}
height={1000}
/>
</div>
</div>
</div>
<div className="relative pt-10">