feat: setup landing page
This commit is contained in:
@@ -8,25 +8,6 @@ await import("./src/env.mjs");
|
|||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const config = {
|
const config = {
|
||||||
reactStrictMode: false,
|
reactStrictMode: false,
|
||||||
images: {
|
|
||||||
remotePatterns: [
|
|
||||||
{
|
|
||||||
protocol: "https",
|
|
||||||
hostname: "storage.googleapis.com",
|
|
||||||
port: "",
|
|
||||||
pathname: "/**",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
redirects: async () => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
source: "/",
|
|
||||||
destination: "/boards",
|
|
||||||
permanent: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export function Auth({ setIsMagicLinkSent }: AuthProps) {
|
|||||||
Please enter a valid email address
|
Please enter a valid email address
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{loginWithEmail.error ?? loginWithOAuth.error ? (
|
{(loginWithEmail.error ?? loginWithOAuth.error) ? (
|
||||||
<p className="mt-2 text-xs text-red-400">
|
<p className="mt-2 text-xs text-red-400">
|
||||||
Something went wrong, please try again later or contact customer
|
Something went wrong, please try again later or contact customer
|
||||||
support.
|
support.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import Link from "next/link";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import LoadingSpinner from "./LoadingSpinner";
|
import LoadingSpinner from "./LoadingSpinner";
|
||||||
|
|
||||||
@@ -5,52 +6,79 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|||||||
variant?: "primary" | "secondary" | "danger" | "ghost";
|
variant?: "primary" | "secondary" | "danger" | "ghost";
|
||||||
size?: "sm" | "md" | "lg";
|
size?: "sm" | "md" | "lg";
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
icon?: React.ReactNode;
|
iconLeft?: React.ReactNode;
|
||||||
|
iconRight?: React.ReactNode;
|
||||||
|
href?: string;
|
||||||
|
openInNewTab?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = ({
|
const Button = ({
|
||||||
children,
|
children,
|
||||||
size = "md",
|
size = "md",
|
||||||
icon,
|
iconLeft,
|
||||||
|
iconRight,
|
||||||
isLoading,
|
isLoading,
|
||||||
variant = "primary",
|
variant = "primary",
|
||||||
|
href,
|
||||||
|
openInNewTab,
|
||||||
...props
|
...props
|
||||||
}: ButtonProps) => {
|
}: ButtonProps) => {
|
||||||
|
const classes = twMerge(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-2 text-sm font-semibold text-light-50 shadow-sm focus-visible:outline-none",
|
||||||
|
size === "sm" && "text-xs",
|
||||||
|
size === "lg" && "px-4 py-3 text-lg",
|
||||||
|
variant === "primary" &&
|
||||||
|
"bg-light-1000 dark:bg-dark-1000 dark:text-dark-50",
|
||||||
|
variant === "secondary" &&
|
||||||
|
"border-[1px] border-light-600 bg-light-50 text-light-1000 dark:border-dark-600 dark:bg-dark-300 dark:text-dark-1000",
|
||||||
|
variant === "danger" &&
|
||||||
|
"dark:text-red-1000 border-[1px] border-red-600 bg-red-50 dark:border-red-600 dark:bg-red-500",
|
||||||
|
variant === "ghost" &&
|
||||||
|
"bg-none text-light-1000 shadow-none hover:bg-light-300 dark:text-dark-1000 dark:hover:bg-dark-200",
|
||||||
|
props.disabled && "opacity-50",
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<span className="relative flex items-center justify-center">
|
||||||
|
{isLoading && (
|
||||||
|
<span className="absolute">
|
||||||
|
<LoadingSpinner size={size} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"flex items-center",
|
||||||
|
isLoading ? "invisible" : "visible",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{iconLeft && <span className="mr-2">{iconLeft}</span>}
|
||||||
|
{children}
|
||||||
|
{iconRight && <span className="ml-1">{iconRight}</span>}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (href) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
className={classes}
|
||||||
|
target={openInNewTab ? "_blank" : undefined}
|
||||||
|
rel={openInNewTab ? "noopener noreferrer" : undefined}
|
||||||
|
{...(props as React.AnchorHTMLAttributes<HTMLAnchorElement>)}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={twMerge(
|
className={classes}
|
||||||
"inline-flex items-center justify-center rounded-md px-3 py-2 text-sm font-semibold text-light-50 shadow-sm focus-visible:outline-none",
|
|
||||||
size === "sm" && "text-xs",
|
|
||||||
size === "lg" && "px-4 py-3 text-lg",
|
|
||||||
variant === "primary" &&
|
|
||||||
"bg-light-1000 dark:bg-dark-1000 dark:text-dark-50",
|
|
||||||
variant === "secondary" &&
|
|
||||||
"border-[1px] border-light-600 bg-light-50 text-light-1000 dark:border-dark-600 dark:bg-dark-300 dark:text-dark-1000",
|
|
||||||
variant === "danger" &&
|
|
||||||
"dark:text-red-1000 border-[1px] border-red-600 bg-red-50 dark:border-red-600 dark:bg-red-500",
|
|
||||||
variant === "ghost" &&
|
|
||||||
"bg-none text-light-1000 shadow-none hover:bg-light-300 dark:text-dark-1000 dark:hover:bg-dark-200",
|
|
||||||
props.disabled && "opacity-50",
|
|
||||||
)}
|
|
||||||
disabled={isLoading ?? props.disabled}
|
disabled={isLoading ?? props.disabled}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="relative flex items-center justify-center">
|
{content}
|
||||||
{isLoading && (
|
|
||||||
<span className="absolute">
|
|
||||||
<LoadingSpinner size={size} />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className={twMerge(
|
|
||||||
"flex items-center",
|
|
||||||
isLoading ? "invisible" : "visible",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{icon && <span className="mr-2">{icon}</span>}
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import Link from "next/link";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import SideNavigation from "./SideNavigation";
|
import SideNavigation from "./SideNavigation";
|
||||||
import FeedbackButton from "./FeedbackButton";
|
import FeedbackButton from "./FeedbackButton";
|
||||||
@@ -6,20 +7,30 @@ export default function Dashboard(props: { children: React.ReactNode }) {
|
|||||||
const { data, isLoading } = api.auth.getUser.useQuery();
|
const { data, isLoading } = api.auth.getUser.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen flex-col items-center bg-light-100 dark:bg-dark-50">
|
<>
|
||||||
<div className="m-auto flex h-16 min-h-16 w-full justify-between border-b border-light-600 px-5 py-2 align-middle dark:border-dark-400">
|
<style jsx global>{`
|
||||||
<div className="my-auto flex w-full items-center justify-between">
|
html {
|
||||||
<h1 className="text-lg font-bold tracking-tight text-neutral-900 dark:text-dark-1000">
|
height: 100vh;
|
||||||
kan.bn
|
overflow: hidden;
|
||||||
</h1>
|
}
|
||||||
<FeedbackButton />
|
`}</style>
|
||||||
|
<div className="flex h-screen flex-col items-center bg-light-100 dark:bg-dark-50">
|
||||||
|
<div className="m-auto flex h-16 min-h-16 w-full justify-between border-b border-light-600 px-5 py-2 align-middle dark:border-dark-400">
|
||||||
|
<div className="my-auto flex w-full items-center justify-between">
|
||||||
|
<Link href="/">
|
||||||
|
<h1 className="text-lg font-bold tracking-tight text-neutral-900 dark:text-dark-1000">
|
||||||
|
kan.bn
|
||||||
|
</h1>
|
||||||
|
</Link>
|
||||||
|
<FeedbackButton />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex h-full w-full">
|
||||||
|
<SideNavigation user={{ email: data?.email }} isLoading={isLoading} />
|
||||||
|
<div className="w-full overflow-hidden">{props.children}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
<div className="flex h-full w-full">
|
|
||||||
<SideNavigation user={{ email: data?.email }} isLoading={isLoading} />
|
|
||||||
<div className="w-full overflow-hidden">{props.children}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
const PatternedBackground = () => (
|
const PatternedBackground = () => (
|
||||||
<div>
|
<div className="absolute inset-0 h-full w-full">
|
||||||
<svg
|
<svg className="h-full w-full">
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
top: "0px",
|
|
||||||
left: "0px",
|
|
||||||
color: "white",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<pattern
|
<pattern
|
||||||
id="pattern"
|
id="pattern"
|
||||||
x="0.034759358288862785"
|
x="0.034759358288862785"
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ const MyApp: AppType = ({ Component, pageProps }) => {
|
|||||||
<style jsx global>{`
|
<style jsx global>{`
|
||||||
html {
|
html {
|
||||||
font-family: ${jakarta.style.fontFamily};
|
font-family: ${jakarta.style.fontFamily};
|
||||||
height: 100vh;
|
}
|
||||||
overflow: hidden;
|
body {
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
<main className="font-sans">
|
<main className="font-sans">
|
||||||
|
|||||||
5
src/pages/index.tsx
Normal file
5
src/pages/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import HomeView from "~/views/home";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return <HomeView />;
|
||||||
|
}
|
||||||
@@ -7,3 +7,20 @@
|
|||||||
display: block;
|
display: block;
|
||||||
color: #707070;
|
color: #707070;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gradient-border::before {
|
||||||
|
background: conic-gradient(
|
||||||
|
from 0deg,
|
||||||
|
transparent 0deg,
|
||||||
|
transparent 45deg,
|
||||||
|
rgba(244, 114, 182, 0.4) 85deg,
|
||||||
|
rgba(192, 132, 252, 0.4) 95deg,
|
||||||
|
transparent 110deg,
|
||||||
|
transparent 360deg
|
||||||
|
);
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
}
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ const Filters = () => {
|
|||||||
handleSelect={handleSelect}
|
handleSelect={handleSelect}
|
||||||
menuSpacing="md"
|
menuSpacing="md"
|
||||||
>
|
>
|
||||||
<Button variant="secondary" icon={<IoFilterOutline />}>
|
<Button variant="secondary" iconLeft={<IoFilterOutline />}>
|
||||||
Filter
|
Filter
|
||||||
{numOfFilters > 0 && (
|
{numOfFilters > 0 && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
113
src/views/home/index.tsx
Normal file
113
src/views/home/index.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import Button from "~/components/Button";
|
||||||
|
import PatternedBackground from "~/components/PatternedBackground";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
|
import { IoLogoGithub } from "react-icons/io";
|
||||||
|
|
||||||
|
export default function HomeView() {
|
||||||
|
const token =
|
||||||
|
typeof window !== "undefined"
|
||||||
|
? localStorage.getItem("sb-bbgbpnfcmvrpinklkgqq-auth-token.0")
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const { data } = api.auth.getUser.useQuery(undefined, {
|
||||||
|
enabled: !!token,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isLoggedIn = !!data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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]">
|
||||||
|
<div className="m-auto flex h-24 min-h-24 w-full items-center justify-between px-5 py-2 align-middle dark:border-dark-400">
|
||||||
|
<div className="my-auto flex items-center justify-between">
|
||||||
|
<h1 className="w-[100px] text-xl font-bold tracking-tight text-neutral-900 dark:text-dark-1000">
|
||||||
|
kan.bn
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full 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-[100px] 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 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 animate-border-spin absolute inset-0" />
|
||||||
|
<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="flex flex-col items-center justify-center">
|
||||||
|
<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:bg-dark-50 dark:text-dark-1000">
|
||||||
|
<p>
|
||||||
|
We are currently building the product and will be launching
|
||||||
|
soon.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,18 +2,28 @@ import { type Config } from "tailwindcss";
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
content: ["./src/**/*.tsx"],
|
content: ["./src/**/*.tsx"],
|
||||||
darkMode: 'class',
|
darkMode: "class",
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['var(--font-plus-jakarta-sans), Plus Jakarta Sans']
|
sans: ["var(--font-plus-jakarta-sans), Plus Jakarta Sans"],
|
||||||
},
|
},
|
||||||
fontSize: {
|
fontSize: {
|
||||||
sm: "0.8rem",
|
sm: "0.8rem",
|
||||||
},
|
},
|
||||||
boxShadow: {
|
boxShadow: {
|
||||||
'3xl-dark': '0px 16px 70px rgba(0, 0, 0, 0.5)',
|
"3xl-dark": "0px 16px 70px rgba(0, 0, 0, 0.5)",
|
||||||
'3xl-light': 'rgba(0, 0, 0, 0.12) 0px 4px 30px, rgba(0, 0, 0, 0.04) 0px 3px 17px, rgba(0, 0, 0, 0.04) 0px 2px 8px, rgba(0, 0, 0, 0.04) 0px 1px 1px'
|
"3xl-light":
|
||||||
|
"rgba(0, 0, 0, 0.12) 0px 4px 30px, rgba(0, 0, 0, 0.04) 0px 3px 17px, rgba(0, 0, 0, 0.04) 0px 2px 8px, rgba(0, 0, 0, 0.04) 0px 1px 1px",
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"border-spin": "border-spin 4s linear infinite",
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
"border-spin": {
|
||||||
|
from: { transform: "rotate(0deg)" },
|
||||||
|
to: { transform: "rotate(360deg)" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
"dark-50": "#161616",
|
"dark-50": "#161616",
|
||||||
@@ -39,13 +49,16 @@ export default {
|
|||||||
"light-800": "hsl(0deg 0% 56.1%)",
|
"light-800": "hsl(0deg 0% 56.1%)",
|
||||||
"light-900": "hsl(0deg 0% 52.2%)",
|
"light-900": "hsl(0deg 0% 52.2%)",
|
||||||
"light-950": "hsl(0deg 0% 43.5%)",
|
"light-950": "hsl(0deg 0% 43.5%)",
|
||||||
"light-1000": "hsl(0deg 0% 9%)"
|
"light-1000": "hsl(0deg 0% 9%)",
|
||||||
},
|
},
|
||||||
screens: {
|
screens: {
|
||||||
xxl: '1600px'
|
xxl: "1600px",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-var-requires
|
||||||
plugins: [require("@tailwindcss/forms"), require('tailwind-scrollbar')({ nocompatible: true })],
|
plugins: [
|
||||||
|
require("@tailwindcss/forms"),
|
||||||
|
require("tailwind-scrollbar")({ nocompatible: true }),
|
||||||
|
],
|
||||||
} satisfies Config;
|
} satisfies Config;
|
||||||
|
|||||||
Reference in New Issue
Block a user