feat: localisation and multi-lang support (#95)

* feat: setup localisation with lingui

* Fix hydration and locale issues

* Add missing translations and tweak selector styling

* Extend language support

* Update lang support

* Extend lang support

* Fix build and add dyanmic imports
This commit is contained in:
Henry
2025-06-25 21:30:24 +01:00
committed by GitHub
parent 014fdbb7d8
commit dd061c6d00
79 changed files with 11822 additions and 883 deletions

View File

@@ -1,4 +1,5 @@
import Link from "next/link";
import { t } from "@lingui/core/macro";
import { useEffect, useState } from "react";
import Button from "~/components/Button";
@@ -50,15 +51,14 @@ const Cta = ({ theme }: { theme: string }) => {
<p>kan.bn/{currentWorkspaceSlug}</p>
</div>
<h2 className="text-balance text-4xl font-bold tracking-tight text-light-1000 dark:text-dark-1000 sm:text-4xl">
Get started for free today
{t`Get started for free today`}
</h2>
<p className="text-md/8 mx-auto mt-6 max-w-[375px] text-pretty text-light-900 dark:text-dark-900">
Unlimited boards, unlimited lists, unlimited cards. No credit card
required.
{t`Unlimited boards, unlimited lists, unlimited cards. No credit card required.`}
</p>
<Link href="/signup">
<div className="mt-10 flex items-center justify-center gap-x-6">
<Button size="lg">Get started</Button>
<Button size="lg">{t`Get started`}</Button>
</div>
</Link>
</div>

View File

@@ -4,6 +4,8 @@ import {
DisclosureButton,
DisclosurePanel,
} from "@headlessui/react";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { HiMiniMinusSmall, HiMiniPlusSmall } from "react-icons/hi2";
const Text = ({ children }: { children: React.ReactNode }) => {
@@ -14,143 +16,145 @@ const Text = ({ children }: { children: React.ReactNode }) => {
);
};
const faqs = [
{
question: "Why make an open source Trello?",
answer: (
<Text>
When Trello launched in 2011, it blew everyone away with its carefully
designed simplicity, but over time (and atlassification) it lost its
magic and grew into something closer to "Jira Lite". This project is our
attempt to recapture the original magic of Trellos simplicity, in open
source.
</Text>
),
},
{
question: "What's the difference between Kan and Trello?",
answer: (
<Text>
The main difference between Kan and Trello is that Kan is open source,
allowing anyone to view, modify, and contribute to our code. Our cloud
offering also offers no restrictions on features for individual use,
whereas Trello locks basic features such as the number of boards you can
create behind a paywall.
</Text>
),
},
{
question: "How is the project funded?",
answer: (
<Text>
For long-term sustainability, we recognise all good open source projects
need a source of revenue. Ours comes from the paid cloud offering for
workspaces with multiple users (free during the beta) and for custom
workspace slugs. There's no obligation to use it, and you can self-host
the software on your own infrastructure free of charge.
</Text>
),
},
{
question: "How do I import my Trello boards?",
answer: (
<Text>
Importing your Trello boards into Kan is easy. You can follow our
step-by-step guide{" "}
<Link href="https://docs.kan.bn/imports/trello" className="underline">
here
</Link>
.
</Text>
),
},
{
question: "How do I get a custom URL?",
answer: (
<Text>
You can get a custom workspace URL, like{" "}
<Link href="https://kan.bn/kan" className="underline">
kan.bn/kan
</Link>
, by going into your{" "}
<Link href="https://kan.bn/settings" className="underline">
workspace settings
</Link>{" "}
and purchasing a pro workspace subscription. All subscriptions help fund
the development of the project!
</Text>
),
},
{
question: "Do you offer a free plan?",
answer: (
<Text>
Yes, we offer an forever free plan for individual use. No restrictions,
no paywalls, no limits.
</Text>
),
},
{
question: "What license are you using?",
answer: (
<Text>
We are using the{" "}
<Link
href="https://github.com/kan-bn/kan/blob/main/LICENSE"
className="underline"
>
AGPL-3.0 license
</Link>
.
</Text>
),
},
{
question: "How do I invite team members?",
answer: (
<Text>
You can invite team members by clicking the "Invite" button in the top
right corner of the{" "}
<Link href="https://kan.bn/members" className="underline">
members page
</Link>{" "}
and entering their email address. They will receive an email with a link
to join the workspace.
</Text>
),
},
{
question: "How do I self-host?",
answer: (
<Text>
You can self-host by following the instructions in our{" "}
<Link href="https://github.com/kanbn/kan" className="underline">
repo
</Link>
.
</Text>
),
},
];
const Faqs = () => {
const faqs = [
{
question: t`Why make an open source Trello?`,
answer: (
<Text>
{t`When Trello launched in 2011, it blew everyone away with its carefully designed simplicity, but over time (and atlassification) it lost its magic and grew into something closer to "Jira Lite". This project is our attempt to recapture the original magic of Trello's simplicity, in open source.`}
</Text>
),
},
{
question: t`What's the difference between Kan and Trello?`,
answer: (
<Text>
{t`The main difference between Kan and Trello is that Kan is open source, allowing anyone to view, modify, and contribute to our code. Our cloud offering also offers no restrictions on features for individual use, whereas Trello locks basic features such as the number of boards you can create behind a paywall.`}
</Text>
),
},
{
question: t`How is the project funded?`,
answer: (
<Text>
{t`For long-term sustainability, we recognise all good open source projects need a source of revenue. Ours comes from the paid cloud offering for workspaces with multiple users (free during the beta) and for custom workspace slugs. There's no obligation to use it, and you can self-host the software on your own infrastructure free of charge.`}
</Text>
),
},
{
question: t`How do I import my Trello boards?`,
answer: (
<Text>
<Trans>
Importing your Trello boards into Kan is easy. You can follow our
step-by-step guide{" "}
<Link
href="https://docs.kan.bn/imports/trello"
className="underline"
>
here
</Link>
.
</Trans>
</Text>
),
},
{
question: t`How do I get a custom URL?`,
answer: (
<Text>
<Trans>
You can get a custom workspace URL, like{" "}
<Link href="https://kan.bn/kan" className="underline">
kan.bn/kan
</Link>
, by going into your{" "}
<Link href="https://kan.bn/settings" className="underline">
workspace settings
</Link>{" "}
and purchasing a pro workspace subscription. All subscriptions help
fund the development of the project!
</Trans>
</Text>
),
},
{
question: t`Do you offer a free plan?`,
answer: (
<Text>
{t`Yes, we offer an forever free plan for individual use. No restrictions, no paywalls, no limits.`}
</Text>
),
},
{
question: t`What license are you using?`,
answer: (
<Text>
<Trans>
We are using the{" "}
<Link
href="https://github.com/kan-bn/kan/blob/main/LICENSE"
className="underline"
>
AGPL-3.0 license
</Link>
.
</Trans>
</Text>
),
},
{
question: t`How do I invite team members?`,
answer: (
<Text>
<Trans>
You can invite team members by clicking the "Invite" button in the
top right corner of the{" "}
<Link href="https://kan.bn/members" className="underline">
members page
</Link>{" "}
and entering their email address. They will receive an email with a
link to join the workspace.
</Trans>
</Text>
),
},
{
question: t`How do I self-host?`,
answer: (
<Text>
<Trans>
You can self-host by following the instructions in our{" "}
<Link href="https://github.com/kanbn/kan" className="underline">
repo
</Link>
.
</Trans>
</Text>
),
},
];
return (
<div className="mx-auto max-w-[900px] px-4">
<div className="flex flex-col items-center justify-center pb-12">
<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>FAQs</p>
<p>{t`FAQs`}</p>
</div>
<p className="mt-2 text-center text-3xl font-bold text-light-1000 dark:text-dark-1000 lg:text-4xl">
Questions?
{t`Questions?`}
</p>
<p className="text:md lg:text-md mt-3 max-w-[500px] text-center text-dark-900">
Find answers to common questions about the project. Can't find what
you're looking for? Feel free to{" "}
<Link href="mailto:support@kan.bn" className="underline">
contact us
</Link>
.
<Trans>
Find answers to common questions about the project. Can't find what
you're looking for? Feel free to{" "}
<Link href="mailto:support@kan.bn" className="underline">
contact us
</Link>
.
</Trans>
</p>
</div>
<div className="rounded-2xl bg-light-50 ring-1 ring-light-300 dark:bg-dark-50 dark:ring-dark-400">

View File

@@ -1,4 +1,5 @@
import Link from "next/link";
import { t } from "@lingui/core/macro";
import { useState } from "react";
import activityLogsIconDark from "~/assets/activity-logs-dark.json";
@@ -62,7 +63,7 @@ const FeatureItem = ({
{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
{t`Coming soon`}
</div>
)}
</div>
@@ -71,47 +72,47 @@ const FeatureItem = ({
const Features = ({ theme }: { theme: "light" | "dark" }) => {
const isDark = theme === "dark";
const features = [
{
title: "Board visibility",
description: "Control who can view and edit your boards.",
title: t`Board visibility`,
description: t`Control who can view and edit your boards.`,
icon: isDark ? boardVisibilityIconDark : boardVisibilityIconLight,
},
{
title: "Workspace members",
description: "Collaborate seamlessly with your team.",
title: t`Workspace members`,
description: t`Collaborate seamlessly with your team.`,
icon: isDark ? membersIconDark : membersIconLight,
},
{
title: "Trello imports",
description: "Import your Trello boards and hit the ground running.",
title: t`Trello imports`,
description: t`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.",
title: t`Labels & Filters`,
description: t`Organize and find cards quickly with powerful filtering tools.`,
icon: isDark ? labelsIconDark : labelsIconLight,
},
{
title: "Comments",
description: "Discuss and collaborate on cards.",
title: t`Comments`,
description: t`Discuss and collaborate on cards.`,
icon: isDark ? commentsIconDark : commentsIconLight,
},
{
title: "Activity logs",
description: "Track all card changes with detailed activity history.",
title: t`Activity logs`,
description: t`Track all card changes with detailed activity history.`,
icon: isDark ? activityLogsIconDark : activityLogsIconLight,
},
{
title: "Templates",
description: "Save time with reusable board templates.",
title: t`Templates`,
description: t`Save time with reusable board templates.`,
icon: isDark ? templatesIconDark : templatesIconLight,
comingSoon: true,
},
{
title: "Integrations",
description: "Connect your favorite tools to streamline your workflow.",
title: t`Integrations`,
description: t`Connect your favorite tools to streamline your workflow.`,
icon: isDark ? integrationsIconDark : integrationsIconLight,
comingSoon: true,
},
@@ -121,15 +122,14 @@ const Features = ({ theme }: { theme: "light" | "dark" }) => {
<>
<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>
<p>{t`Features`}</p>
</div>
<p className="mt-2 text-center text-3xl font-bold text-light-1000 dark:text-dark-1000 lg:text-4xl">
Kanban reimagined
{t`Kanban reimagined`}
</p>
<p className="text-md lg:text-md mt-3 max-w-[500px] text-center text-dark-900">
Simple, visual task management that just works. Drag and drop cards,
collaborate with your team, and get more done.
{t`Simple, visual task management that just works. Drag and drop cards, collaborate with your team, and get more done.`}
</p>
<div className="mx-auto mt-16 w-full max-w-7xl">
<div className="grid w-full grid-cols-1 gap-4 [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%)]">
@@ -142,9 +142,9 @@ const Features = ({ theme }: { theme: "light" | "dark" }) => {
<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 className="text-xs lg:text-sm">
{`We're just getting started. `}
{t`We're just getting started. `}
<Link href="/kan/roadmap" className="underline">
View our roadmap.
{t`View our roadmap.`}
</Link>
</p>
</div>

View File

@@ -1,37 +1,8 @@
import Link from "next/link";
import { t } from "@lingui/core/macro";
import { FaDiscord, FaGithub } from "react-icons/fa";
const navigation = {
documentation: [
{ name: "Getting started", href: "https://docs.kan.bn/introduction" },
{
name: "Importing from Trello",
href: "https://docs.kan.bn/imports/trello",
},
{
name: "API Reference",
href: "https://docs.kan.bn/api-reference/introduction",
},
],
company: [
{ name: "Roadmap", href: "/kan/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=AGPL-3.0-1-ov-file#readme",
},
],
resources: [
{ name: "Features", href: "/#features" },
{ name: "Pricing", href: "/#pricing" },
{ name: "FAQs", href: "/#faq" },
],
};
import { LanguageSelector } from "~/components/LanguageSelector";
const StatusMarker = () => (
<Link
@@ -44,11 +15,43 @@ const StatusMarker = () => (
<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
{t`All systems operational`}
</Link>
);
const Footer = () => {
const navigation = {
documentation: [
{ name: t`Getting started`, href: "https://docs.kan.bn/introduction" },
{
name: t`Importing from Trello`,
href: "https://docs.kan.bn/imports/trello",
},
{
name: t`API Reference`,
href: "https://docs.kan.bn/api-reference/introduction",
},
],
company: [
{ name: t`Roadmap`, href: "/kan/roadmap" },
{ name: t`GitHub`, href: "https://github.com/kanbn/kan" },
{ name: t`Contact`, href: "mailto:support@kan.bn" },
],
legal: [
{ name: t`Terms of service`, href: "/terms" },
{ name: t`Privacy policy`, href: "/privacy" },
{
name: t`License`,
href: "https://github.com/kanbn/kan?tab=AGPL-3.0-1-ov-file#readme",
},
],
resources: [
{ name: t`Features`, href: "/#features" },
{ name: t`Pricing`, href: "/#pricing" },
{ name: t`FAQs`, href: "/#faq" },
],
};
return (
<footer className="z-10 w-full border-t 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">
@@ -64,13 +67,14 @@ const Footer = () => {
</div>
<StatusMarker />
<LanguageSelector />
</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
{t`Documentation`}
</h3>
<ul role="list" className="mt-6 space-y-4">
{navigation.documentation.map((item) => (
@@ -87,7 +91,7 @@ const Footer = () => {
</div>
<div>
<h3 className="mt-10 text-sm/6 font-semibold text-light-1000 dark:text-dark-1000 md:mt-0">
Company
{t`Company`}
</h3>
<ul role="list" className="mt-6 space-y-4">
{navigation.company.map((item) => (
@@ -106,7 +110,7 @@ const Footer = () => {
<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
{t`Resources`}
</h3>
<ul role="list" className="mt-6 space-y-4">
{navigation.resources.map((item) => (
@@ -123,7 +127,7 @@ const Footer = () => {
</div>
<div>
<h3 className="mt-10 text-sm/6 font-semibold text-light-1000 dark:text-dark-1000 md:mt-0">
Legal
{t`Legal`}
</h3>
<ul role="list" className="mt-6 space-y-4">
{navigation.legal.map((item) => (

View File

@@ -1,4 +1,5 @@
import Link from "next/link";
import { t } from "@lingui/core/macro";
import { useEffect, useState } from "react";
import { twMerge } from "tailwind-merge";
@@ -9,10 +10,10 @@ const Header = ({ isLoggedIn }: { isLoggedIn: boolean }) => {
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.kan.bn", openInNewTab: true },
{ label: t`Roadmap`, href: "/kan/roadmap", openInNewTab: true },
{ label: t`Features`, href: "#features" },
{ label: t`Pricing`, href: "#pricing" },
{ label: t`Docs`, href: "https://docs.kan.bn", openInNewTab: true },
];
useEffect(() => {
@@ -69,7 +70,7 @@ const Header = ({ isLoggedIn }: { isLoggedIn: boolean }) => {
<button
onClick={toggleMenu}
className="z-50 p-2 lg:hidden"
aria-label="Toggle menu"
aria-label={t`Toggle menu`}
>
<div
className={twMerge(
@@ -92,13 +93,13 @@ const Header = ({ isLoggedIn }: { isLoggedIn: boolean }) => {
</button>
<div className="hidden w-[200px] justify-end gap-2 lg:flex">
{isLoggedIn ? (
<Button href="/boards">Go to app</Button>
<Button href="/boards">{t`Go to app`}</Button>
) : (
<>
<Button href="/login" variant="ghost">
Sign in
{t`Sign in`}
</Button>
<Button href="/signup">Get started</Button>
<Button href="/signup">{t`Get started`}</Button>
</>
)}
</div>
@@ -129,15 +130,15 @@ const Header = ({ isLoggedIn }: { isLoggedIn: boolean }) => {
<div className="mt-8 flex flex-col gap-4">
{isLoggedIn ? (
<Button href="/boards" onClick={toggleMenu}>
Go to app
{t`Go to app`}
</Button>
) : (
<>
<Button href="/login" variant="ghost" onClick={toggleMenu}>
Sign in
{t`Sign in`}
</Button>
<Button href="/signup" onClick={toggleMenu}>
Get started
{t`Get started`}
</Button>
</>
)}

View File

@@ -1,108 +1,104 @@
import Link from "next/link";
import { Radio, RadioGroup } from "@headlessui/react";
import { t } from "@lingui/core/macro";
import { useState } from "react";
import { HiCheckCircle } from "react-icons/hi2";
import { twMerge } from "tailwind-merge";
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: "$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:",
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: "-", annually: "-" },
description:
"Host 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);
const frequencies = [
{
value: "monthly" as Frequency,
label: t`Monthly`,
priceSuffix: t`per user/month`,
},
{
value: "annually" as Frequency,
label: t`Yearly`,
priceSuffix: t`per user/month`,
},
];
const [frequency, setFrequency] = useState(frequencies[1]);
const tiers = [
{
name: t`Individuals`,
id: "tier-individuals",
href: "signup",
buttonText: t`Get Started`,
price: { monthly: t`Free`, annually: t`Free` },
description: t`Everything you need, free forever. Unlimited boards, unlimited lists, unlimited cards. Upgrade any time.`,
featureHeader: t`Free, forever`,
features: [
t`1 user`,
t`Unlimited boards`,
t`Unlimited lists`,
t`Unlimited cards`,
t`Unlimited comments`,
t`Unlimited activity log`,
],
showPrice: true,
},
{
name: t`Teams`,
id: "tier-teams",
href: "signup",
buttonText: t`Get Started`,
price: { monthly: "$10.00", annually: "$8.00" },
description: t`Kanban is better with a team. Perfect for small and growing teams looking to collaborate.`,
featureHeader: t`Everything in the free plan, plus:`,
features: [
t`Workspace members`,
t`Admin roles`,
t`Priority email support`,
t`Support the development of the project`,
],
highlighted: true,
showPrice: true,
showPriceSuffix: true,
},
{
name: t`Self Host`,
id: "tier-self-host",
href: "https://github.com/kanbn/kan",
buttonText: t`View docs`,
price: { monthly: "-", annually: "-" },
description: t`Host Kan on your own infrastructure. Ideal for organisations that need complete control over their data.`,
featureHeader: t`Complete control and ownership:`,
features: [
t`Run on your own infrastructure`,
t`Own your data`,
t`Custom domain`,
],
mostPopular: false,
showPrice: false,
},
];
return (
<>
<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>
<p>{t`Pricing`}</p>
</div>
<p className="mt-2 text-center text-3xl font-bold text-light-1000 dark:text-dark-1000 lg:text-4xl">
Simple pricing
{t`Simple pricing`}
</p>
<p className="text:md lg:text-md mt-3 max-w-[500px] text-center text-dark-900">
Get started for free, with no usage limits. For collaboration, upgrade
to a plan that fits the size of your team.
{t`Get started for free, with no usage limits. For collaboration, upgrade to a plan that fits the size of your team.`}
</p>
{/* Beta free pricing pill */}
<div className="mt-4 flex items-center gap-2 rounded-full bg-green-100 px-3 py-1 text-xs font-semibold text-green-800 dark:bg-green-700 dark:text-green-100">
All seats are free during the beta until Sept 1st!
{t`All seats are free during the beta until Sept 1st!`}
</div>
<div className="mt-16 flex justify-center">
<fieldset aria-label="Payment frequency">
<fieldset aria-label={t`Payment frequency`}>
<RadioGroup
value={frequency}
onChange={(value) => setFrequency(value)}
@@ -145,7 +141,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-[12px] font-semibold text-dark-500 dark:bg-dark-1000 dark:text-dark-50">
-20%
</p>
@@ -169,11 +165,11 @@ const Pricing = () => {
!tier.showPrice && "opacity-0",
)}
>
{tier.price[frequency.value]}
{tier.price[frequency?.value || "monthly"]}
</span>
{tier.showPriceSuffix && (
<span className="text-sm/6 font-semibold text-light-50 dark:text-dark-900">
{frequency.priceSuffix}
{frequency?.priceSuffix}
</span>
)}
</p>

View File

@@ -1,5 +1,7 @@
import Image from "next/image";
import Link from "next/link";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { IoLogoGithub, IoLogoHackernews } from "react-icons/io";
import Button from "~/components/Button";
@@ -32,7 +34,7 @@ export default function HomeView() {
target="_blank"
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
{t`Star on Github`}
<IoLogoGithub size={20} />
</Link>
</div>
@@ -46,7 +48,7 @@ export default function HomeView() {
target="_blank"
className="flex items-center gap-2 px-4 py-1 text-center text-xs text-light-1000 dark:text-dark-1000 lg:text-sm"
>
#1 Hacker News
{t`#1 Hacker News`}
<div className="relative">
<div className="absolute inset-1 bg-white" />
<IoLogoHackernews
@@ -60,27 +62,27 @@ export default function HomeView() {
</div>
<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
<Trans>
The open source <br /> alternative to Trello
</Trans>
</p>
<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.
{t`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 href="/signup">{t`Get started on Cloud`}</Button>
<Button
variant="secondary"
href="https://github.com/kanbn/kan"
openInNewTab
>
Self host with Github
{t`Self host with Github`}
</Button>
</div>
<p className="mt-4 text-center text-sm text-dark-900">
No credit card required
{t`No credit card required`}
</p>
</div>
</div>