feat: monorepo
This commit is contained in:
155
apps/web/src/views/home/components/Features.tsx
Normal file
155
apps/web/src/views/home/components/Features.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import LottieIcon from "~/components/LottieIcon";
|
||||
|
||||
import boardVisibilityIconLight from "~/assets/board-visibility-light.json";
|
||||
import boardVisibilityIconDark from "~/assets/board-visibility-dark.json";
|
||||
import membersIconLight from "~/assets/members-light.json";
|
||||
import membersIconDark from "~/assets/members-dark.json";
|
||||
import commentsIconLight from "~/assets/comments-light.json";
|
||||
import commentsIconDark from "~/assets/comments-dark.json";
|
||||
import integrationsIconLight from "~/assets/integrations-light.json";
|
||||
import integrationsIconDark from "~/assets/integrations-dark.json";
|
||||
import labelsIconLight from "~/assets/labels-light.json";
|
||||
import labelsIconDark from "~/assets/labels-dark.json";
|
||||
import importsIconLight from "~/assets/imports-light.json";
|
||||
import importsIconDark from "~/assets/imports-dark.json";
|
||||
import activityLogsIconLight from "~/assets/activity-logs-light.json";
|
||||
import activityLogsIconDark from "~/assets/activity-logs-dark.json";
|
||||
import templatesIconLight from "~/assets/templates-light.json";
|
||||
import templatesIconDark from "~/assets/templates-dark.json";
|
||||
|
||||
const FeatureItem = ({
|
||||
feature,
|
||||
}: {
|
||||
feature: {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: Record<string, unknown>;
|
||||
comingSoon?: boolean;
|
||||
};
|
||||
}) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [index, setIndex] = useState(0);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsHovered(true);
|
||||
setIndex((index) => index + 1);
|
||||
};
|
||||
|
||||
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"
|
||||
>
|
||||
<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 bottom-8 left-8 h-2 w-2 rounded-full bg-light-200 dark:bg-dark-200" />
|
||||
<div className="absolute bottom-8 right-8 h-2 w-2 rounded-full bg-light-200 dark:bg-dark-200" />
|
||||
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl border border-light-300 bg-light-200 dark:border-dark-600 dark:bg-dark-200">
|
||||
<LottieIcon index={index} json={feature.icon} isPlaying={isHovered} />
|
||||
</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">
|
||||
{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">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{feature.comingSoon && (
|
||||
<div className="absolute right-4 top-4 rounded-full border border-light-300 px-2 py-1 text-[10px] text-light-1000 dark:border-dark-600 dark:bg-dark-50 dark:text-dark-900">
|
||||
Coming soon
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Features = ({ theme }: { theme: "light" | "dark" }) => {
|
||||
const isDark = theme === "dark";
|
||||
const features = [
|
||||
{
|
||||
title: "Board visibility",
|
||||
description: "Control who can view and edit your boards.",
|
||||
icon: isDark ? boardVisibilityIconDark : boardVisibilityIconLight,
|
||||
},
|
||||
{
|
||||
title: "Workspace members",
|
||||
description: "Collaborate seamlessly with your team.",
|
||||
icon: isDark ? membersIconDark : membersIconLight,
|
||||
},
|
||||
{
|
||||
title: "Trello imports",
|
||||
description: "Import your Trello boards and hit the ground running.",
|
||||
icon: isDark ? importsIconDark : importsIconLight,
|
||||
},
|
||||
{
|
||||
title: "Labels & Filters",
|
||||
description:
|
||||
"Organize and find cards quickly with powerful filtering tools.",
|
||||
icon: isDark ? labelsIconDark : labelsIconLight,
|
||||
},
|
||||
{
|
||||
title: "Comments",
|
||||
description: "Discuss and collaborate on cards.",
|
||||
icon: isDark ? commentsIconDark : commentsIconLight,
|
||||
},
|
||||
{
|
||||
title: "Activity logs",
|
||||
description: "Track all card changes with detailed activity history.",
|
||||
icon: isDark ? activityLogsIconDark : activityLogsIconLight,
|
||||
},
|
||||
{
|
||||
title: "Templates",
|
||||
description: "Save time with reusable board templates.",
|
||||
icon: isDark ? templatesIconDark : templatesIconLight,
|
||||
comingSoon: true,
|
||||
},
|
||||
{
|
||||
title: "Integrations",
|
||||
description: "Connect your favorite tools to streamline your workflow.",
|
||||
icon: isDark ? integrationsIconDark : integrationsIconLight,
|
||||
comingSoon: true,
|
||||
},
|
||||
];
|
||||
|
||||
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">
|
||||
<p>Features</p>
|
||||
</div>
|
||||
|
||||
<p className="mt-2 text-center text-4xl font-bold text-light-1000 dark:text-dark-1000">
|
||||
Kanban simplified
|
||||
</p>
|
||||
<p className="mt-3 max-w-[600px] text-center text-lg text-dark-900">
|
||||
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%)]">
|
||||
{features.map((feature, index) => {
|
||||
return <FeatureItem key={`feature-${index}`} feature={feature} />;
|
||||
})}
|
||||
</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>
|
||||
{`We're just getting started. `}
|
||||
<Link href="/roadmap" className="underline">
|
||||
View our roadmap.
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Features;
|
||||
143
apps/web/src/views/home/components/Footer.tsx
Normal file
143
apps/web/src/views/home/components/Footer.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import Link from "next/link";
|
||||
import { FaDiscord, FaGithub } from "react-icons/fa";
|
||||
|
||||
const navigation = {
|
||||
documentation: [
|
||||
{ name: "Getting started", href: "#" },
|
||||
{ name: "Importing from Trello", href: "#" },
|
||||
{ name: "API Reference", href: "#" },
|
||||
],
|
||||
company: [
|
||||
{ name: "Roadmap", href: "/roadmap" },
|
||||
{ name: "GitHub", href: "https://github.com/kanbn/kan" },
|
||||
{ name: "Contact", href: "mailto:support@kan.bn" },
|
||||
],
|
||||
legal: [
|
||||
{ name: "Terms of service", href: "/terms" },
|
||||
{ name: "Privacy policy", href: "/privacy" },
|
||||
{
|
||||
name: "License",
|
||||
href: "https://github.com/kanbn/kan?tab=GPL-3.0-1-ov-file#readme",
|
||||
},
|
||||
],
|
||||
resources: [
|
||||
{ name: "Features", href: "/#features" },
|
||||
{ name: "Pricing", href: "/#pricing" },
|
||||
{ name: "FAQs", href: "/#faq" },
|
||||
],
|
||||
};
|
||||
|
||||
const StatusMarker = () => (
|
||||
<Link
|
||||
href="https://openstatus.dev"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex w-fit items-center gap-1.5 rounded-full border border-light-300 py-2 pl-3 pr-4 text-xs text-light-950 hover:bg-light-100 dark:border-dark-300 dark:text-dark-800 dark:hover:bg-dark-100"
|
||||
>
|
||||
<span className="relative mr-1 h-2 w-2">
|
||||
<span className="absolute -inset-[1px] animate-[ping_1s_infinite] rounded-full bg-green-500/30"></span>
|
||||
<span className="absolute inset-0 rounded-full bg-green-500"></span>
|
||||
</span>
|
||||
All systems operational
|
||||
</Link>
|
||||
);
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<footer className="z-10 mt-20 w-full border-t border-light-300 border-light-300 bg-light-50 py-8 dark:border-dark-300 dark:bg-dark-50">
|
||||
<div className="mx-auto max-w-7xl px-6 py-16 sm:py-24 lg:px-8 lg:py-24">
|
||||
<div className="xl:grid xl:grid-cols-3 xl:gap-8">
|
||||
<div>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Link href="https://github.com/kanbn/kan" target="_blank">
|
||||
<FaGithub className="h-8 w-8 rounded-lg border border-light-300 border-light-300 p-1.5 text-light-1000 hover:bg-light-100 dark:border-dark-300 dark:text-dark-1000 dark:hover:bg-dark-100" />
|
||||
</Link>
|
||||
<Link href="#" target="_blank">
|
||||
<FaDiscord className="h-8 w-8 rounded-lg border border-light-300 border-light-300 p-1.5 text-light-1000 hover:bg-light-100 dark:border-dark-300 dark:text-dark-1000 dark:hover:bg-dark-100" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<StatusMarker />
|
||||
</div>
|
||||
|
||||
<div className="mt-16 grid grid-cols-2 gap-8 xl:col-span-2 xl:mt-0">
|
||||
<div className="md:grid md:grid-cols-2 md:gap-8">
|
||||
<div>
|
||||
<h3 className="text-sm/6 font-semibold text-light-1000 dark:text-dark-1000">
|
||||
Documentation
|
||||
</h3>
|
||||
<ul role="list" className="mt-6 space-y-4">
|
||||
{navigation.documentation.map((item) => (
|
||||
<li key={item.name}>
|
||||
<a
|
||||
href={item.href}
|
||||
className="text-sm/6 text-light-900 hover:text-light-1000 dark:text-dark-950 dark:hover:text-dark-1000"
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm/6 font-semibold text-light-1000 dark:text-dark-1000">
|
||||
Company
|
||||
</h3>
|
||||
<ul role="list" className="mt-6 space-y-4">
|
||||
{navigation.company.map((item) => (
|
||||
<li key={item.name}>
|
||||
<a
|
||||
href={item.href}
|
||||
className="text-sm/6 text-light-900 hover:text-light-1000 dark:text-dark-950 dark:hover:text-dark-1000"
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:grid md:grid-cols-2 md:gap-8">
|
||||
<div>
|
||||
<h3 className="text-sm/6 font-semibold text-light-1000 dark:text-dark-1000">
|
||||
Resources
|
||||
</h3>
|
||||
<ul role="list" className="mt-6 space-y-4">
|
||||
{navigation.resources.map((item) => (
|
||||
<li key={item.name}>
|
||||
<a
|
||||
href={item.href}
|
||||
className="text-sm/6 text-light-900 hover:text-light-1000 dark:text-dark-950 dark:hover:text-dark-1000"
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm/6 font-semibold text-light-1000 dark:text-dark-1000">
|
||||
Legal
|
||||
</h3>
|
||||
<ul role="list" className="mt-6 space-y-4">
|
||||
{navigation.legal.map((item) => (
|
||||
<li key={item.name}>
|
||||
<a
|
||||
href={item.href}
|
||||
className="text-sm/6 text-light-900 hover:text-light-1000 dark:text-dark-950 dark:hover:text-dark-1000"
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
72
apps/web/src/views/home/components/Header.tsx
Normal file
72
apps/web/src/views/home/components/Header.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import Button from "~/components/Button";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
const Header = ({ isLoggedIn }: { isLoggedIn: boolean }) => {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 0);
|
||||
};
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={twMerge(
|
||||
"z-50 m-auto flex w-full max-w-[1100px] px-4 pt-4",
|
||||
isScrolled && "fixed",
|
||||
)}
|
||||
>
|
||||
<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",
|
||||
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",
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between px-2">
|
||||
<div className="my-auto flex items-center justify-between pl-2">
|
||||
<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="/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>
|
||||
</div>
|
||||
<div className="flex w-[200px] justify-end gap-2">
|
||||
{isLoggedIn ? (
|
||||
<Button href="/boards">Go to app</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button href="/login" variant="ghost">
|
||||
Sign in
|
||||
</Button>
|
||||
<Button href="/signup">Get started</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isScrolled && <div className="h-[5rem] min-h-[5rem]"></div>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
221
apps/web/src/views/home/components/Pricing.tsx
Normal file
221
apps/web/src/views/home/components/Pricing.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Radio, RadioGroup } from "@headlessui/react";
|
||||
import { HiCheckCircle } from "react-icons/hi2";
|
||||
|
||||
type Frequency = "monthly" | "annually";
|
||||
|
||||
const frequencies = [
|
||||
{
|
||||
value: "monthly" as Frequency,
|
||||
label: "Monthly",
|
||||
priceSuffix: "per user/month",
|
||||
},
|
||||
{
|
||||
value: "annually" as Frequency,
|
||||
label: "Yearly",
|
||||
priceSuffix: "per user/month",
|
||||
},
|
||||
];
|
||||
|
||||
const tiers = [
|
||||
{
|
||||
name: "Individuals",
|
||||
id: "tier-individuals",
|
||||
href: "signup",
|
||||
buttonText: "Get Started",
|
||||
price: { monthly: "Free", annually: "Free" },
|
||||
description:
|
||||
"Everything you need, free forever. Unlimited boards, unlimited lists, unlimited cards. Upgrade any time.",
|
||||
featureHeader: "Free, forever",
|
||||
features: [
|
||||
"1 user",
|
||||
"Unlimited boards",
|
||||
"Unlimited lists",
|
||||
"Unlimited cards",
|
||||
"Unlimited comments",
|
||||
"Unlimited activity log",
|
||||
],
|
||||
showPrice: true,
|
||||
},
|
||||
{
|
||||
name: "Teams",
|
||||
id: "tier-teams",
|
||||
href: "signup",
|
||||
buttonText: "Get Started",
|
||||
price: { monthly: "£7.50", annually: "£6" },
|
||||
description:
|
||||
"Kanban is better with a team. Perfect for small and growing teams looking to collaborate.",
|
||||
featureHeader: "Everything in the free plan, plus:",
|
||||
features: [
|
||||
"Workspace members",
|
||||
"Admin roles",
|
||||
"Priority email support",
|
||||
"Support the development of the project",
|
||||
],
|
||||
highlighted: true,
|
||||
showPrice: true,
|
||||
showPriceSuffix: true,
|
||||
},
|
||||
{
|
||||
name: "Self Host",
|
||||
id: "tier-self-host",
|
||||
href: "https://github.com/kanbn/kan",
|
||||
buttonText: "View docs",
|
||||
price: { monthly: "Free", annually: "Free" },
|
||||
description:
|
||||
"Spin up a self hosted version of Kan on your own infrastructure. Ideal for organisations that need complete control over their data.",
|
||||
featureHeader: "Complete control and ownership:",
|
||||
features: [
|
||||
"Run on your own infrastructure",
|
||||
"Own your data",
|
||||
"Custom domain",
|
||||
],
|
||||
mostPopular: false,
|
||||
showPrice: false,
|
||||
},
|
||||
];
|
||||
|
||||
const Pricing = () => {
|
||||
const initialFrequency = frequencies[1]!;
|
||||
const [frequency, setFrequency] = useState(initialFrequency);
|
||||
|
||||
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">
|
||||
<p>Pricing</p>
|
||||
</div>
|
||||
|
||||
<p className="mt-2 text-center text-4xl font-bold text-light-1000 dark:text-dark-1000">
|
||||
Simple pricing to fit your needs
|
||||
</p>
|
||||
<p className="mt-3 max-w-[600px] text-center text-lg text-dark-900">
|
||||
Get started for free, with no usage limits. For collaboration, upgrade
|
||||
to a plan that fits the size of your team.
|
||||
</p>
|
||||
<div className="mt-16 flex justify-center">
|
||||
<fieldset aria-label="Payment frequency">
|
||||
<RadioGroup
|
||||
value={frequency}
|
||||
onChange={(value) => setFrequency(value)}
|
||||
className="grid grid-cols-2 gap-x-1 rounded-full p-1 text-center text-xs/5 font-semibold ring-1 ring-inset ring-light-600 dark:ring-dark-600"
|
||||
>
|
||||
{frequencies.map((option) => (
|
||||
<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"
|
||||
>
|
||||
{option.label}
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</fieldset>
|
||||
</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">
|
||||
{tiers.map((tier) => (
|
||||
<div
|
||||
key={tier.id}
|
||||
className={twMerge(
|
||||
tier.highlighted
|
||||
? "bg-dark-100 dark:bg-light-50"
|
||||
: "bg-light-50 ring-1 ring-light-300 dark:bg-dark-50 dark:ring-dark-300",
|
||||
"rounded-3xl p-8 xl:p-10",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-x-4">
|
||||
<h3
|
||||
id={tier.id}
|
||||
className={twMerge(
|
||||
tier.highlighted
|
||||
? "text-dark-1000 dark:text-dark-100"
|
||||
: "text-dark-50 dark:text-dark-1000",
|
||||
"text-lg/8 font-semibold",
|
||||
)}
|
||||
>
|
||||
{tier.name}
|
||||
</h3>
|
||||
{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>
|
||||
) : null}
|
||||
</div>
|
||||
<p
|
||||
className={twMerge(
|
||||
"mt-4 text-sm/6 text-dark-950",
|
||||
tier.highlighted
|
||||
? "text-light-100 dark:text-dark-100"
|
||||
: "text-dark-50 dark:text-dark-1000",
|
||||
)}
|
||||
>
|
||||
{tier.description}
|
||||
</p>
|
||||
<p className="mt-6 flex items-baseline gap-x-1">
|
||||
<span
|
||||
className={twMerge(
|
||||
"text-4xl font-semibold tracking-tight text-light-100",
|
||||
tier.highlighted
|
||||
? "text-light-50 dark:text-dark-100"
|
||||
: "text-gray-900 dark:text-dark-1000",
|
||||
!tier.showPrice && "opacity-0",
|
||||
)}
|
||||
>
|
||||
{tier.price[frequency.value]}
|
||||
</span>
|
||||
{tier.showPriceSuffix && (
|
||||
<span className="text-sm/6 font-semibold text-light-50 dark:text-dark-900">
|
||||
{frequency.priceSuffix}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<Link
|
||||
href={tier.href}
|
||||
aria-describedby={tier.id}
|
||||
className={twMerge(
|
||||
tier.highlighted
|
||||
? "bg-light-50 text-dark-50 shadow-sm dark:bg-dark-50 dark:text-dark-1000"
|
||||
: "bg-dark-50 text-light-50 dark:bg-light-50 dark:text-dark-50",
|
||||
"mt-6 block rounded-md px-3 py-2 text-center text-sm/6 font-semibold focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600",
|
||||
)}
|
||||
>
|
||||
{tier.buttonText}
|
||||
</Link>
|
||||
<p
|
||||
className={twMerge(
|
||||
"mt-8 text-sm/6 font-bold",
|
||||
tier.highlighted
|
||||
? "text-light-100 dark:text-dark-100"
|
||||
: "text-dark-50 dark:text-dark-1000",
|
||||
)}
|
||||
>
|
||||
{tier.featureHeader}
|
||||
</p>
|
||||
<ul
|
||||
role="list"
|
||||
className={twMerge(
|
||||
"mt-2 space-y-3 text-sm/6 text-light-600",
|
||||
tier.highlighted
|
||||
? "text-light-100 dark:text-dark-100"
|
||||
: "text-dark-50 dark:text-dark-1000",
|
||||
)}
|
||||
>
|
||||
{tier.features.map((feature) => (
|
||||
<li key={feature} className="flex items-center gap-x-3">
|
||||
<HiCheckCircle className="h-5 w-5" />
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pricing;
|
||||
113
apps/web/src/views/home/index.tsx
Normal file
113
apps/web/src/views/home/index.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import Cookies from "js-cookie";
|
||||
import { IoLogoGithub } from "react-icons/io";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
import PatternedBackground from "~/components/PatternedBackground";
|
||||
import { env } from "~/env";
|
||||
import { useTheme } from "~/providers/theme";
|
||||
import { api } from "~/utils/api";
|
||||
import Features from "./components/Features";
|
||||
import Footer from "./components/Footer";
|
||||
import Header from "./components/Header";
|
||||
import Pricing from "./components/Pricing";
|
||||
|
||||
export default function HomeView() {
|
||||
const theme = useTheme();
|
||||
|
||||
const token =
|
||||
typeof window !== "undefined"
|
||||
? Cookies.get(env.NEXT_PUBLIC_SUPABASE_AUTH_COOKIE_NAME)
|
||||
: null;
|
||||
|
||||
const { data } = api.auth.getUser.useQuery(undefined, {
|
||||
enabled: !!token,
|
||||
});
|
||||
|
||||
const isLoggedIn = !!data;
|
||||
|
||||
const isDarkMode = theme.activeTheme === "dark";
|
||||
|
||||
return (
|
||||
<>
|
||||
<style jsx global>{`
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
body {
|
||||
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">
|
||||
<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="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">
|
||||
<Link
|
||||
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"
|
||||
>
|
||||
Star on Github
|
||||
<IoLogoGithub size={20} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mt-2 text-center text-5xl font-bold text-light-1000 dark:text-dark-1000">
|
||||
The open source <br />
|
||||
alternative to Trello
|
||||
</p>
|
||||
|
||||
<p className="mt-3 max-w-[600px] text-center text-lg text-dark-900">
|
||||
A powerful, flexible kanban app that helps you organise work,
|
||||
track progress, and deliver results—all in one place.
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex gap-2">
|
||||
<Button href="/signup">Get started on Cloud</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
href="https://github.com/kanbn/kan"
|
||||
openInNewTab
|
||||
>
|
||||
Self host with Github
|
||||
</Button>
|
||||
</div>
|
||||
<p className="mt-4 text-center text-sm text-dark-900">
|
||||
No credit card required
|
||||
</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>
|
||||
</div>
|
||||
<div className="relative pt-10">
|
||||
<div id="features" className="absolute -top-20" />
|
||||
<Features theme={theme.activeTheme} />
|
||||
</div>
|
||||
<div className="relative pt-10">
|
||||
<div id="pricing" className="absolute -top-20" />
|
||||
<Pricing />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user