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 ( return (
<div <div
onMouseEnter={handleMouseEnter} 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 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" /> <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>
<div className="relative mt-2 w-full px-4 text-center"> <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} {feature.title}
</p> </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} {feature.description}
</p> </p>
</div> </div>
@@ -119,27 +119,31 @@ const Features = ({ theme }: { theme: "light" | "dark" }) => {
return ( return (
<> <>
<div className="flex flex-col items-center justify-center pb-24"> <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-sm text-light-1000 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900"> <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> <p>Features</p>
</div> </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 Kanban simplified
</p> </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, Simple, visual task management that just works. Drag and drop cards,
collaborate with your team, and get more done. collaborate with your team, and get more done.
</p> </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) => { {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> <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"> <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. `} {`We're just getting started. `}
<Link href="/kan/roadmap" className="underline"> <Link href="/kan/roadmap" className="underline">
View our roadmap. View our roadmap.

View File

@@ -80,7 +80,7 @@ const Footer = () => {
</ul> </ul>
</div> </div>
<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 Company
</h3> </h3>
<ul role="list" className="mt-6 space-y-4"> <ul role="list" className="mt-6 space-y-4">
@@ -116,7 +116,7 @@ const Footer = () => {
</ul> </ul>
</div> </div>
<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 Legal
</h3> </h3>
<ul role="list" className="mt-6 space-y-4"> <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 Header = ({ isLoggedIn }: { isLoggedIn: boolean }) => {
const [isScrolled, setIsScrolled] = useState(false); 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(() => { useEffect(() => {
const handleScroll = () => { const handleScroll = () => {
@@ -15,42 +23,72 @@ const Header = ({ isLoggedIn }: { isLoggedIn: boolean }) => {
return () => window.removeEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll);
}, []); }, []);
const toggleMenu = () => {
setIsMenuOpen(!isMenuOpen);
document.body.style.overflow = !isMenuOpen ? "hidden" : "";
};
return ( return (
<> <>
<div <div
className={twMerge( className={twMerge(
"z-50 m-auto flex w-full max-w-[1100px] px-4 pt-4", "fixed z-50 m-auto flex w-full transition-all duration-500 lg:max-w-[1100px] lg:px-4 lg:pt-4",
isScrolled && "fixed",
)} )}
> >
<div <div
className={twMerge( 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 && 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="flex w-full items-center justify-between lg:px-4">
<div className="my-auto flex items-center justify-between pl-2"> <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"> <h1 className="w-[200px] text-lg font-bold tracking-tight text-neutral-900 dark:text-dark-1000">
kan.bn kan.bn
</h1> </h1>
</div> </div>
<div className="flex justify-center gap-10 dark:text-dark-1000"> {/* Desktop Menu */}
<Link href="/kan/roadmap" className="text-sm font-bold"> <div className="hidden justify-center gap-10 dark:text-dark-1000 lg:flex">
Roadmap {menuItems.map((item) => (
</Link> <Link
<Link href="#features" className="text-sm font-bold"> key={item.label}
Features href={item.href}
</Link> target={item.openInNewTab ? "_blank" : undefined}
<Link href="#pricing" className="text-sm font-bold"> rel={item.openInNewTab ? "noopener noreferrer" : undefined}
Pricing className="text-sm font-bold"
</Link> >
<Link href="/docs" className="text-sm font-bold"> {item.label}
Docs </Link>
</Link> ))}
</div> </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 ? ( {isLoggedIn ? (
<Button href="/boards">Go to app</Button> <Button href="/boards">Go to app</Button>
) : ( ) : (
@@ -65,7 +103,46 @@ const Header = ({ isLoggedIn }: { isLoggedIn: boolean }) => {
</div> </div>
</div> </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 Link from "next/link";
import { twMerge } from "tailwind-merge";
import { Radio, RadioGroup } from "@headlessui/react"; import { Radio, RadioGroup } from "@headlessui/react";
import { useState } from "react";
import { HiCheckCircle } from "react-icons/hi2"; import { HiCheckCircle } from "react-icons/hi2";
import { twMerge } from "tailwind-merge";
type Frequency = "monthly" | "annually"; type Frequency = "monthly" | "annually";
@@ -44,7 +44,7 @@ const tiers = [
id: "tier-teams", id: "tier-teams",
href: "signup", href: "signup",
buttonText: "Get Started", buttonText: "Get Started",
price: { monthly: "£7.50", annually: "£6" }, price: { monthly: "$10.00", annually: "$8.00" },
description: description:
"Kanban is better with a team. Perfect for small and growing teams looking to collaborate.", "Kanban is better with a team. Perfect for small and growing teams looking to collaborate.",
featureHeader: "Everything in the free plan, plus:", featureHeader: "Everything in the free plan, plus:",
@@ -83,15 +83,15 @@ const Pricing = () => {
return ( return (
<> <>
<div className="flex flex-col items-center justify-center pb-10"> <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-sm text-light-1000 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900"> <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> <p>Pricing</p>
</div> </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 Simple pricing to fit your needs
</p> </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 Get started for free, with no usage limits. For collaboration, upgrade
to a plan that fits the size of your team. to a plan that fits the size of your team.
</p> </p>
@@ -106,7 +106,7 @@ const Pricing = () => {
<Radio <Radio
key={option.value} key={option.value}
value={option} 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} {option.label}
</Radio> </Radio>
@@ -116,7 +116,7 @@ const Pricing = () => {
</div> </div>
</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) => ( {tiers.map((tier) => (
<div <div
key={tier.id} key={tier.id}
@@ -139,7 +139,7 @@ const Pricing = () => {
> >
{tier.name} {tier.name}
</h3> </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"> <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% -20%
</p> </p>

View File

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