feat: complete dashboard design/ui (#21)

* feat: complete dashboard design/ui

* chore: fixing linting issue with `npm run format`
This commit is contained in:
Puru Dahal
2024-01-12 00:09:29 -06:00
committed by GitHub
parent 6436a696cb
commit cd49d17bb7
38 changed files with 1230 additions and 317 deletions
+1 -1
View File
@@ -14,4 +14,4 @@
"components": "@/components",
"utils": "@/lib/utils"
}
}
}
+261
View File
@@ -9,11 +9,13 @@
"version": "0.1.0",
"hasInstallScript": true,
"dependencies": {
"@headlessui/react": "^1.7.18",
"@hookform/resolvers": "^3.3.4",
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "^5.6.0",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5",
"@remixicon/react": "^4.0.1",
@@ -170,6 +172,56 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@floating-ui/core": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.3.tgz",
"integrity": "sha512-O0WKDOo0yhJuugCx6trZQj5jVJ9yR0ystG2JaNAemYUWce+pmM6WUEFIibnWyEJKdrDxhm75NoSRME35FNaM/Q==",
"dependencies": {
"@floating-ui/utils": "^0.2.0"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.4.tgz",
"integrity": "sha512-jByEsHIY+eEdCjnTVu+E3ephzTOzkQ8hgUfGwos+bg7NlH33Zc5uO+QHz1mrQUOgIKKDD1RtS201P9NvAfq3XQ==",
"dependencies": {
"@floating-ui/core": "^1.5.3",
"@floating-ui/utils": "^0.2.0"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.5.tgz",
"integrity": "sha512-UsBK30Bg+s6+nsgblXtZmwHhgS2vmbuQK22qgt2pTQM6M3X6H1+cQcLXqgRY3ihVLcZJE6IvqDQozhsnIVqK/Q==",
"dependencies": {
"@floating-ui/dom": "^1.5.4"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
"integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
},
"node_modules/@headlessui/react": {
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.18.tgz",
"integrity": "sha512-4i5DOrzwN4qSgNsL4Si61VMkUcWbcSKueUV7sFhpHzQcSShdlHENE5+QBntMSRvHt8NyoFO2AGG8si9lq+w4zQ==",
"dependencies": {
"@tanstack/react-virtual": "^3.0.0-beta.60",
"client-only": "^0.0.1"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": "^16 || ^17 || ^18",
"react-dom": "^16 || ^17 || ^18"
}
},
"node_modules/@hookform/resolvers": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.4.tgz",
@@ -598,6 +650,14 @@
"@prisma/debug": "5.7.1"
}
},
"node_modules/@radix-ui/number": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz",
"integrity": "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==",
"dependencies": {
"@babel/runtime": "^7.13.10"
}
},
"node_modules/@radix-ui/primitive": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz",
@@ -606,6 +666,29 @@
"@babel/runtime": "^7.13.10"
}
},
"node_modules/@radix-ui/react-arrow": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz",
"integrity": "sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-primitive": "1.0.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collection": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz",
@@ -702,6 +785,23 @@
}
}
},
"node_modules/@radix-ui/react-direction": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz",
"integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz",
@@ -812,6 +912,38 @@
}
}
},
"node_modules/@radix-ui/react-popper": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz",
"integrity": "sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@floating-ui/react-dom": "^2.0.0",
"@radix-ui/react-arrow": "1.0.3",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-layout-effect": "1.0.1",
"@radix-ui/react-use-rect": "1.0.1",
"@radix-ui/react-use-size": "1.0.1",
"@radix-ui/rect": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-portal": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz",
@@ -882,6 +1014,49 @@
}
}
},
"node_modules/@radix-ui/react-select": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.0.0.tgz",
"integrity": "sha512-RH5b7af4oHtkcHS7pG6Sgv5rk5Wxa7XI8W5gvB1N/yiuDGZxko1ynvOiVhFM7Cis2A8zxF9bTOUVbRDzPepe6w==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/number": "1.0.1",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-collection": "1.0.3",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-direction": "1.0.1",
"@radix-ui/react-dismissable-layer": "1.0.5",
"@radix-ui/react-focus-guards": "1.0.1",
"@radix-ui/react-focus-scope": "1.0.4",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-popper": "1.1.3",
"@radix-ui/react-portal": "1.0.4",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-slot": "1.0.2",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-controllable-state": "1.0.1",
"@radix-ui/react-use-layout-effect": "1.0.1",
"@radix-ui/react-use-previous": "1.0.1",
"@radix-ui/react-visually-hidden": "1.0.3",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "2.5.5"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
@@ -1004,6 +1179,59 @@
}
}
},
"node_modules/@radix-ui/react-use-previous": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz",
"integrity": "sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-rect": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz",
"integrity": "sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/rect": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-size": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz",
"integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-layout-effect": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-visually-hidden": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz",
@@ -1027,6 +1255,14 @@
}
}
},
"node_modules/@radix-ui/rect": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz",
"integrity": "sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==",
"dependencies": {
"@babel/runtime": "^7.13.10"
}
},
"node_modules/@remixicon/react": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@remixicon/react/-/react-4.0.1.tgz",
@@ -1115,6 +1351,31 @@
}
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.0.1.tgz",
"integrity": "sha512-IFOFuRUTaiM/yibty9qQ9BfycQnYXIDHGP2+cU+0LrFFGNhVxCXSQnaY6wkX8uJVteFEBjUondX0Hmpp7TNcag==",
"dependencies": {
"@tanstack/virtual-core": "3.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.0.0.tgz",
"integrity": "sha512-SYXOBTjJb05rXa2vl55TTwO40A6wKu0R5i1qQwhJYNDIqaIGF7D0HsLw+pJAyi2OvntlEIVusx3xtbbgSUi6zg==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@trpc/client": {
"version": "10.45.0",
"resolved": "https://registry.npmjs.org/@trpc/client/-/client-10.45.0.tgz",
+4 -1
View File
@@ -12,14 +12,17 @@
"dev": "next dev",
"postinstall": "prisma generate",
"lint": "next lint",
"start": "next start"
"start": "next start",
"format": "prettier --write \"**/*.{css,js,json,jsx,ts,tsx}\""
},
"dependencies": {
"@hookform/resolvers": "^3.3.4",
"@headlessui/react": "^1.7.18",
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "^5.6.0",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5",
"@remixicon/react": "^4.0.1",
+1 -1
View File
@@ -25,7 +25,7 @@ const handler = (req: NextRequest) =>
env.NODE_ENV === "development"
? ({ path, error }) => {
console.error(
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
);
}
: undefined,
+10 -7
View File
@@ -1,19 +1,22 @@
import { type NextRequest, NextResponse } from "next/server";
import { db } from "@/server/db";
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url)
const email = searchParams.get('email')!
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const email = searchParams.get("email")!;
try {
const newWaitlistUser = await db.waitlistUser.create({
data: {
email: email,
}
})
},
});
return NextResponse.json({ message: `Waitlist user ${newWaitlistUser.id} created successfully ` }, { status: 200 })
return NextResponse.json(
{ message: `Waitlist user ${newWaitlistUser.id} created successfully ` },
{ status: 200 },
);
} catch (e) {
return NextResponse.json({ message: e }, { status: 400 })
return NextResponse.json({ message: e }, { status: 400 });
}
}
+5
View File
@@ -0,0 +1,5 @@
const Form3921Page = () => {
return <div>3921 Form page</div>;
};
export default Form3921Page;
+5
View File
@@ -0,0 +1,5 @@
const Legal409aPage = () => {
return <div>409A Valuation page</div>;
};
export default Legal409aPage;
+5
View File
@@ -0,0 +1,5 @@
const AuditsPage = () => {
return <div>Audits page</div>;
};
export default AuditsPage;
+5
View File
@@ -0,0 +1,5 @@
const CaptablePage = () => {
return <div>Captable page</div>;
};
export default CaptablePage;
+5
View File
@@ -0,0 +1,5 @@
const DocumentsPage = () => {
return <div>Documents page</div>;
};
export default DocumentsPage;
+419
View File
@@ -0,0 +1,419 @@
"use client";
import Link from "next/link";
import { Fragment, useState } from "react";
import { usePathname } from "next/navigation";
import { Dialog, Menu, Transition } from "@headlessui/react";
import {
RiHome4Line,
RiPieChartLine,
RiSafeLine,
RiUserHeartLine,
RiUserSettingsLine,
RiFolder5Line,
RiFolderChartLine,
RiFileList2Line,
RiCloseLine,
RiEqualizer2Line,
RiNotification3Line,
RiMenuLine,
RiArrowDownSLine,
RiListOrdered2,
RiFolderShield2Line,
} from "@remixicon/react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const navigation = [
{ name: "Home", href: "/dashboard", icon: RiHome4Line },
{ name: "Team", href: "/dashboard/team", icon: RiUserHeartLine },
{ name: "SAFEs", href: "/dashboard/safe", icon: RiSafeLine },
{ name: "Cap table", href: "/dashboard/captable", icon: RiPieChartLine },
{
name: "Securities",
href: "/dashboard/securities",
icon: RiFolderShield2Line,
},
{
name: "Stakeholders",
href: "/dashboard/stakeholders",
icon: RiUserSettingsLine,
},
{ name: "Documents", href: "/dashboard/documents", icon: RiFolder5Line },
{ name: "Reports", href: "/dashboard/reports", icon: RiFolderChartLine },
{ name: "Audits", href: "/dashboard/audits", icon: RiListOrdered2 },
];
const forms = [
{
id: 1,
name: "409A Valuation",
href: "/dashboard/409a",
icon: RiFileList2Line,
},
{ id: 2, name: "Form 3921", href: "/dashboard/3921", icon: RiFileList2Line },
];
const userNavigation = [
{ name: "Your profile", href: "#" },
{ name: "Sign out", href: "#" },
];
import { cn } from "@/lib/utils";
import { OpenCapLogo } from "@/components/logo";
type DashboardLayoutProps = {
children: React.ReactNode;
};
const DashboardLayout = ({ children }: DashboardLayoutProps) => {
const currentPath = usePathname();
const [sidebarOpen, setSidebarOpen] = useState(false);
return (
// <div className="bg-[#fafafa]">
<div className="bg-gradient-to-br from-indigo-50 via-white to-cyan-100">
<Transition.Root show={sidebarOpen} as={Fragment}>
<Dialog
as="div"
className="relative z-50 lg:hidden"
onClose={setSidebarOpen}
>
<Transition.Child
as={Fragment}
enter="transition-opacity ease-linear duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity ease-linear duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-900/80" />
</Transition.Child>
<div className="fixed inset-0 flex">
<Transition.Child
as={Fragment}
enter="transition ease-in-out duration-300 transform"
enterFrom="-translate-x-full"
enterTo="translate-x-0"
leave="transition ease-in-out duration-300 transform"
leaveFrom="translate-x-0"
leaveTo="-translate-x-full"
>
<Dialog.Panel className="relative mr-16 flex w-full max-w-xs flex-1">
<Transition.Child
as={Fragment}
enter="ease-in-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in-out duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="absolute left-full top-0 flex w-16 justify-center pt-5">
<button
type="button"
className="-m-2.5 p-2.5"
onClick={() => setSidebarOpen(false)}
>
<span className="sr-only">Close sidebar</span>
<RiCloseLine
className="h-6 w-6 text-white"
aria-hidden="true"
/>
</button>
</div>
</Transition.Child>
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-white px-6 pb-4">
<div className="flex h-16 shrink-0 items-center">
<OpenCapLogo className="h-7 w-auto" />
</div>
<nav className="flex flex-1 flex-col">
<ul role="list" className="flex flex-1 flex-col gap-y-7">
<li>
<ul role="list" className="-mx-2 space-y-1">
{navigation.map((item) => (
<li key={item.name}>
<Link
href={item.href}
className={cn(
currentPath === item.href
? "bg-gray-50 font-semibold text-primary"
: "text-gray-700 hover:bg-gray-50 hover:text-primary",
"group flex gap-x-3 rounded-md p-2 text-sm leading-6",
)}
>
<item.icon
className={cn(
currentPath === item.href
? "text-primary"
: "text-gray-400 group-hover:text-primary",
"h-6 w-6 shrink-0",
)}
aria-hidden="true"
/>
{item.name}
</Link>
</li>
))}
</ul>
</li>
<li>
<div className="text-xs font-semibold leading-6 text-gray-400">
Legal
</div>
<ul role="list" className="-mx-2 mt-2 space-y-1">
{forms.map((form) => (
<li key={form.name}>
<Link
href={form.href}
className={cn(
currentPath === form.href
? "bg-gray-50 text-primary"
: "font-semibold text-gray-700 hover:bg-gray-50 hover:text-primary",
"group flex gap-x-3 rounded-md p-2 text-sm leading-6",
)}
>
<form.icon
className={cn(
currentPath === form.href
? "text-primary"
: "text-gray-400 group-hover:text-primary",
"h-6 w-6 shrink-0",
)}
aria-hidden="true"
/>
<span className="truncate">{form.name}</span>
</Link>
</li>
))}
</ul>
</li>
<li className="mt-auto">
<Link
href="/dashboard/settings"
className="group -mx-2 flex gap-x-3 rounded-md p-2 text-sm leading-6 text-gray-700 hover:bg-gray-50 hover:text-primary"
>
<RiEqualizer2Line
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-primary"
aria-hidden="true"
/>
Settings
</Link>
</li>
</ul>
</nav>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
{/* Static sidebar for desktop */}
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
<div className="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200 bg-white px-6 pb-4">
<div className="flex h-16 shrink-0 items-center">
<OpenCapLogo className="h-7 w-auto" />
<Select>
<SelectTrigger className="text-md ml-3 h-8 w-[180px] rounded border-none font-semibold">
<SelectValue placeholder="Acme Inc." />
</SelectTrigger>
<SelectContent>
<SelectItem value="acme">Acme Inc.</SelectItem>
<SelectItem value="piedPiper">Pied Pieper</SelectItem>
<SelectItem value="hooli">Hooli</SelectItem>
</SelectContent>
</Select>
</div>
<nav className="flex flex-1 flex-col">
<ul role="list" className="flex flex-1 flex-col gap-y-7">
<li>
<ul role="list" className="-mx-2 space-y-1">
{navigation.map((item) => (
<li key={item.name}>
<Link
href={item.href}
className={cn(
currentPath === item.href
? "bg-gray-50 font-semibold text-primary"
: "text-gray-700 hover:bg-gray-50 hover:text-primary",
"group flex gap-x-3 rounded-md p-2 text-sm leading-6",
)}
>
<item.icon
className={cn(
currentPath === item.href
? "text-primary"
: "text-gray-400 group-hover:text-primary",
"h-6 w-6 shrink-0",
)}
aria-hidden="true"
/>
{item.name}
</Link>
</li>
))}
</ul>
</li>
<li>
<div className="text-xs font-semibold leading-6 text-gray-400">
Legal
</div>
<ul role="list" className="-mx-2 mt-2 space-y-1">
{forms.map((form) => (
<li key={form.name}>
<Link
href={form.href}
className={cn(
currentPath === form.href
? "bg-gray-50 font-semibold text-primary"
: "text-gray-700 hover:bg-gray-50 hover:text-primary",
"group flex gap-x-3 rounded-md p-2 text-sm leading-6",
)}
>
<form.icon
className={cn(
currentPath === form.href
? "text-primary"
: "text-gray-400 group-hover:text-primary",
"h-6 w-6 shrink-0",
)}
aria-hidden="true"
/>
<span className="truncate">{form.name}</span>
</Link>
</li>
))}
</ul>
</li>
<li className="mt-auto">
<Link
href="/dashboard/settings"
className="group -mx-2 flex gap-x-3 rounded-md p-2 text-sm leading-6 text-gray-700 hover:bg-gray-50 hover:text-primary"
>
<RiEqualizer2Line
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-primary"
aria-hidden="true"
/>
Settings
</Link>
</li>
</ul>
</nav>
</div>
</div>
<div className="lg:pl-72 ">
<div className="sticky top-0 z-40 w-full border-b border-gray-200 bg-white shadow-sm lg:shadow-none">
<div className="flex h-16 items-center gap-x-4 px-4 sm:gap-x-6 lg:mx-auto lg:max-w-7xl lg:px-4">
<button
type="button"
className="-m-2.5 p-2.5 text-gray-700 lg:hidden"
onClick={() => setSidebarOpen(true)}
>
<span className="sr-only">Open sidebar</span>
<RiMenuLine className="h-6 w-6" aria-hidden="true" />
</button>
{/* Separator */}
<div
className="h-6 w-px bg-gray-200 lg:hidden"
aria-hidden="true"
/>
<div className="flex flex-1 gap-x-4 self-stretch lg:gap-x-6">
<div className="relative flex flex-1"></div>
<div className="flex items-center gap-x-4 lg:gap-x-6">
<button
type="button"
className="-m-2.5 p-2.5 text-gray-400 hover:text-gray-500"
>
<span className="sr-only">View notifications</span>
<RiNotification3Line className="h-6 w-6" aria-hidden="true" />
</button>
{/* Separator */}
<div
className="hidden lg:block lg:h-6 lg:w-px lg:bg-gray-200"
aria-hidden="true"
/>
{/* Profile dropdown */}
<Menu as="div" className="relative">
<Menu.Button className="-m-1.5 flex items-center p-1.5">
<span className="sr-only">Open user menu</span>
<img
className="h-8 w-8 rounded-full bg-gray-50"
src="https://randomuser.me/api/portraits/women/0.jpg"
alt=""
/>
<span className="hidden lg:flex lg:items-center">
<span
className="ml-4 text-sm leading-6 text-primary"
aria-hidden="true"
>
Mary Lopez
</span>
<RiArrowDownSLine
className="ml-2 h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-10 mt-2.5 w-32 origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
{userNavigation.map((item) => (
<Menu.Item key={item.name}>
{({ active }) => (
<Link
href={item.href}
className={cn(
active ? "bg-gray-50" : "",
"block px-3 py-1 text-sm leading-6 text-primary",
)}
>
{item.name}
</Link>
)}
</Menu.Item>
))}
</Menu.Items>
</Transition>
</Menu>
</div>
</div>
</div>
</div>
<main className="m-5 min-h-screen">
<div className="m-5 mx-auto max-w-7xl rounded border bg-white p-2 shadow-sm sm:p-4 lg:p-5">
{children}
</div>
</main>
</div>
</div>
);
};
export default DashboardLayout;
+9
View File
@@ -0,0 +1,9 @@
const DashboardPage = () => {
return (
<div>
<span>Dashboard home page</span>
</div>
);
};
export default DashboardPage;
+5
View File
@@ -0,0 +1,5 @@
const ReportsPage = () => {
return <div>Reports page</div>;
};
export default ReportsPage;
+5
View File
@@ -0,0 +1,5 @@
const SafePage = () => {
return <div>Safe page</div>;
};
export default SafePage;
+5
View File
@@ -0,0 +1,5 @@
const SecuritiesPage = () => {
return <div>Securities page</div>;
};
export default SecuritiesPage;
+5
View File
@@ -0,0 +1,5 @@
const SettingsPage = () => {
return <div>Company settings page</div>;
};
export default SettingsPage;
+5
View File
@@ -0,0 +1,5 @@
const StakeholdersPage = () => {
return <div>Stakeholders page</div>;
};
export default StakeholdersPage;
+5
View File
@@ -0,0 +1,5 @@
const TeamPage = () => {
return <div>Team page</div>;
};
export default TeamPage;
+3 -5
View File
@@ -4,10 +4,8 @@ import type { Metadata } from "next";
export const metadata: Metadata = {
title: "OpenCap - Login",
description: "Login to OpenCap",
}
};
export default function AuthPage() {
return (
<LoginForm />
)
};
return <LoginForm />;
}
+1 -3
View File
@@ -1,9 +1,7 @@
import OnboardingCompany from "@/components/onboarding/company";
const OnboardingPage = () => {
return (
<OnboardingCompany />
);
return <OnboardingCompany />;
};
export default OnboardingPage;
+3 -5
View File
@@ -3,10 +3,8 @@ import { type Metadata } from "next";
export const metadata: Metadata = {
title: "OpenCap - Sign Up",
description: "Create an account with OpenCap",
}
};
export default function Onboarding() {
return (
<SignupForm />
)
};
return <SignupForm />;
}
+2 -2
View File
@@ -1,4 +1,4 @@
type IconProps = React.HTMLAttributes<SVGElement>
type IconProps = React.HTMLAttributes<SVGElement>;
export const SpinnerIcon = (props: IconProps) => {
return (
@@ -16,5 +16,5 @@ export const SpinnerIcon = (props: IconProps) => {
>
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
)
);
};
+24 -22
View File
@@ -4,8 +4,8 @@ import { Navbar } from "@/components/navbar";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import * as z from "zod";
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import {
Form,
@@ -14,7 +14,7 @@ import {
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
} from "@/components/ui/form";
const onboardingSchema = z.object({
user: z.object({
@@ -27,9 +27,12 @@ const onboardingSchema = z.object({
companyName: z.string().min(1, {
message: "Company name is required",
}),
title: z.string().min(1, {
message: "Title is required",
}).optional(),
title: z
.string()
.min(1, {
message: "Title is required",
})
.optional(),
}),
company: z.object({
incorporationType: z.string().min(1, {
@@ -78,18 +81,18 @@ const OnboardingCompany = () => {
city: "",
state: "",
zipcode: "",
}
},
},
});
function onSubmit(values: z.infer<typeof onboardingSchema>) {
alert(JSON.stringify(values, null, 2))
};
alert(JSON.stringify(values, null, 2));
}
return (
<div className="min-h-screen flex justify-center bg-gradient-to-br from-indigo-50 via-white to-cyan-100 pt-20 pb-5 px-5">
<div className="flex min-h-screen justify-center bg-gradient-to-br from-indigo-50 via-white to-cyan-100 px-5 pb-5 pt-20">
<Navbar />
<div className="max-w-2xl w-full p-10 border border-rounded bg-white shadow">
<div className="border-rounded w-full max-w-2xl border bg-white p-10 shadow">
<div className="mb-5">
<h1 className="text-2xl font-semibold tracking-tight">
Welcome to OpenCap!
@@ -165,11 +168,10 @@ const OnboardingCompany = () => {
<hr />
<h2 className="text-xl">
Company Incorporation details
</h2>
<p className="text-sm text-muted-foreground -mt-5">
Please provide your company{`'`}s incorporation details. Your certificate of incorporation will come in handy here.
<h2 className="text-xl">Company Incorporation details</h2>
<p className="-mt-5 text-sm text-muted-foreground">
Please provide your company{`'`}s incorporation details. Your
certificate of incorporation will come in handy here.
</p>
<div className="grid grid-cols-2 gap-4">
@@ -233,10 +235,8 @@ const OnboardingCompany = () => {
</div>
{/* */}
<h2 className="text-xl">
Company Address
</h2>
<p className="text-sm text-muted-foreground -mt-5">
<h2 className="text-xl">Company Address</h2>
<p className="-mt-5 text-sm text-muted-foreground">
Please provide your company{`'`}s address.
</p>
@@ -300,13 +300,15 @@ const OnboardingCompany = () => {
/>
</div>
</div>
<Button type="submit" className="mt-5">Complete Setup</Button>
<Button type="submit" className="mt-5">
Complete Setup
</Button>
</div>
</form>
</Form>
</div>
</div>
)
);
};
export default OnboardingCompany;
+26 -25
View File
@@ -1,14 +1,14 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import Link from "next/link";
import { useState } from "react";
import { signIn } from "next-auth/react"
import { signIn } from "next-auth/react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { RiGoogleFill } from "@remixicon/react";
import * as z from "zod"
import * as z from "zod";
import {
Form,
FormControl,
@@ -16,11 +16,11 @@ import {
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
} from "@/components/ui/form";
const loginSchema = z.object({
email: z.string().email()
})
email: z.string().email(),
});
const LoginForm = () => {
const form = useForm<z.infer<typeof loginSchema>>({
@@ -37,19 +37,18 @@ const LoginForm = () => {
const email = values.email;
await signIn("email", { email, callbackUrl: "/onboarding" });
};
}
async function signInWithGoogle() {
setLoading(true)
await signIn("google", { callbackUrl: "/onboarding" })
setLoading(false)
};
setLoading(true);
await signIn("google", { callbackUrl: "/onboarding" });
setLoading(false);
}
return (
<div className="h-screen flex items-center justify-center bg-gradient-to-br from-indigo-50 via-white to-cyan-100">
<div className="grid grid-cols-1 gap-8 max-w-md w-full p-10 border border-rounded bg-white shadow">
<div className="text-center mb-5">
<div className="flex h-screen items-center justify-center bg-gradient-to-br from-indigo-50 via-white to-cyan-100">
<div className="border-rounded grid w-full max-w-md grid-cols-1 gap-8 border bg-white p-10 shadow">
<div className="mb-5 text-center">
<h1 className="text-2xl font-semibold tracking-tight">
Login to OpenCap
</h1>
@@ -67,7 +66,9 @@ const LoginForm = () => {
render={({ field }) => (
<FormItem>
<div className="grid gap-1">
<FormLabel className="sr-only" htmlFor="email">Email</FormLabel>
<FormLabel className="sr-only" htmlFor="email">
Email
</FormLabel>
<FormControl>
<Input
id="email"
@@ -86,15 +87,15 @@ const LoginForm = () => {
</FormItem>
)}
/>
<Button type="submit">
Login with Email
</Button>
<Button type="submit">Login with Email</Button>
</div>
</form>
</Form>
<div className="relative">
<div className="absolute inset-0 flex items-center"><span className="w-full border-t"></span></div>
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t"></span>
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Or continue with
@@ -107,8 +108,8 @@ const LoginForm = () => {
Google
</Button>
<span className="text-gray-500 text-center text-sm">
Don{`'`}t have an account? {" "}
<span className="text-center text-sm text-gray-500">
Don{`'`}t have an account?{" "}
<Link
href="/signup"
className="underline underline-offset-4 hover:text-primary "
@@ -118,7 +119,7 @@ const LoginForm = () => {
</span>
</div>
</div>
)
);
};
export default LoginForm;
export default LoginForm;
+40 -31
View File
@@ -2,20 +2,20 @@
import Link from "next/link";
import { useState } from "react";
import { signIn } from "next-auth/react"
import { signIn } from "next-auth/react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { RiGoogleFill } from "@remixicon/react";
const SignupForm = () => {
const [loading, setLoading] = useState<boolean>(false)
const [loading, setLoading] = useState<boolean>(false);
return (
<div className="h-screen flex items-center justify-center bg-gradient-to-br from-indigo-50 via-white to-cyan-100">
<div className="grid grid-cols-1 gap-8 max-w-md w-full p-10 border border-rounded bg-white shadow">
<div className="grid grid-cols-1 gap-8 max-w-md w-full ">
<div className="text-center mb-5">
<div className="flex h-screen items-center justify-center bg-gradient-to-br from-indigo-50 via-white to-cyan-100">
<div className="border-rounded grid w-full max-w-md grid-cols-1 gap-8 border bg-white p-10 shadow">
<div className="grid w-full max-w-md grid-cols-1 gap-8 ">
<div className="mb-5 text-center">
<h1 className="text-2xl font-semibold tracking-tight">
Get started with OpenCap
</h1>
@@ -24,15 +24,15 @@ const SignupForm = () => {
</p>
</div>
<form onSubmit={
async (e) => {
<form
onSubmit={async (e) => {
e.preventDefault();
setLoading(true)
const email = (e.currentTarget.elements as any).email.value as string; // eslint-disable-line
await signIn("email", { email, callbackUrl: "/onboarding" })
}
} >
setLoading(true);
const email = (e.currentTarget.elements as any).email // eslint-disable-line
.value as string;
await signIn("email", { email, callbackUrl: "/onboarding" });
}}
>
<div className="grid gap-2">
<div className="grid gap-1">
<Label className="sr-only" htmlFor="email">
@@ -49,43 +49,52 @@ const SignupForm = () => {
disabled={loading}
/>
</div>
<Button >
Sign up with Email
</Button>
<Button>Sign up with Email</Button>
</div>
</form>
<div className="relative">
<div className="absolute inset-0 flex items-center"><span className="w-full border-t"></span></div>
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t"></span>
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Or continue with
</span>
</div>
</div>
<Button variant="outline" type="button" onClick={
async () => {
setLoading(true)
await signIn("google", { callbackUrl: "/onboarding" })
setLoading(false)
}
}>
<Button
variant="outline"
type="button"
onClick={async () => {
setLoading(true);
await signIn("google", { callbackUrl: "/onboarding" });
setLoading(false);
}}
>
<RiGoogleFill className="mr-2 h-4 w-4" />
Google
</Button>
<p className="px-8 text-center text-sm text-muted-foreground">
By clicking continue, you agree to our{" "}
<Link className="underline underline-offset-4 hover:text-primary" href="/terms">
<Link
className="underline underline-offset-4 hover:text-primary"
href="/terms"
>
Terms of Service
</Link>
{" and "}
<Link className="underline underline-offset-4 hover:text-primary" href="/privacy">
<Link
className="underline underline-offset-4 hover:text-primary"
href="/privacy"
>
Privacy Policy
</Link>.
</Link>
.
</p>
<span className="text-gray-500 text-center text-sm">
Already have an account? {" "}
<span className="text-center text-sm text-gray-500">
Already have an account?{" "}
<Link
href="/login"
className="underline underline-offset-4 hover:text-primary "
@@ -96,7 +105,7 @@ const SignupForm = () => {
</div>
</div>
</div>
)
);
};
export default SignupForm;
+13 -13
View File
@@ -1,8 +1,8 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
@@ -30,27 +30,27 @@ const buttonVariants = cva(
variant: "default",
size: "default",
},
}
)
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants }
export { Button, buttonVariants };
+27 -27
View File
@@ -1,18 +1,18 @@
"use client"
"use client";
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
@@ -22,12 +22,12 @@ const DialogOverlay = React.forwardRef<
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
className,
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
@@ -39,7 +39,7 @@ const DialogContent = React.forwardRef<
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
className,
)}
{...props}
>
@@ -50,8 +50,8 @@ const DialogContent = React.forwardRef<
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
@@ -60,12 +60,12 @@ const DialogHeader = ({
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
className,
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
@@ -74,12 +74,12 @@ const DialogFooter = ({
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
className,
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
@@ -89,12 +89,12 @@ const DialogTitle = React.forwardRef<
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
className,
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
@@ -105,8 +105,8 @@ const DialogDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
@@ -119,4 +119,4 @@ export {
DialogFooter,
DialogTitle,
DialogDescription,
}
};
+51 -50
View File
@@ -1,6 +1,6 @@
import * as React from "react"
import type * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import * as React from "react";
import type * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import {
Controller,
type ControllerProps,
@@ -8,27 +8,27 @@ import {
type FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
} from "react-hook-form";
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { cn } from "@/lib/utils";
import { Label } from "@/components/ui/label";
const Form = FormProvider
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
}
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
{} as FormFieldContextValue,
);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
@@ -36,21 +36,21 @@ const FormField = <
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState)
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
throw new Error("useFormField should be used within <FormField>");
}
const { id } = itemContext
const { id } = itemContext;
return {
id,
@@ -59,36 +59,36 @@ const useFormField = () => {
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
};
};
type FormItemContextValue = {
id: string
}
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
{} as FormItemContextValue,
);
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
);
});
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
const { error, formItemId } = useFormField();
return (
<Label
@@ -97,15 +97,16 @@ const FormLabel = React.forwardRef<
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
);
});
FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<Slot
@@ -119,15 +120,15 @@ const FormControl = React.forwardRef<
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
);
});
FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
const { formDescriptionId } = useFormField();
return (
<p
@@ -136,19 +137,19 @@ const FormDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
);
});
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null
return null;
}
return (
@@ -160,9 +161,9 @@ const FormMessage = React.forwardRef<
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
);
});
FormMessage.displayName = "FormMessage";
export {
useFormField,
@@ -173,4 +174,4 @@ export {
FormDescription,
FormMessage,
FormField,
}
};
+10 -10
View File
@@ -1,14 +1,14 @@
"use client"
"use client";
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
);
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
@@ -20,7 +20,7 @@ const Label = React.forwardRef<
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label }
export { Label };
+160
View File
@@ -0,0 +1,160 @@
"use client";
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import { cn } from "@/lib/utils";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};
+27 -27
View File
@@ -1,11 +1,11 @@
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import * as React from "react";
import * as ToastPrimitives from "@radix-ui/react-toast";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const ToastProvider = ToastPrimitives.Provider
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
@@ -15,12 +15,12 @@ const ToastViewport = React.forwardRef<
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
className,
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
@@ -35,8 +35,8 @@ const toastVariants = cva(
defaultVariants: {
variant: "default",
},
}
)
},
);
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
@@ -49,9 +49,9 @@ const Toast = React.forwardRef<
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
@@ -61,12 +61,12 @@ const ToastAction = React.forwardRef<
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
className,
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
@@ -76,15 +76,15 @@ const ToastClose = React.forwardRef<
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
className,
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
@@ -95,8 +95,8 @@ const ToastTitle = React.forwardRef<
className={cn("text-sm font-semibold", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
@@ -107,12 +107,12 @@ const ToastDescription = React.forwardRef<
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
type ToastProps,
@@ -124,4 +124,4 @@ export {
ToastDescription,
ToastClose,
ToastAction,
}
};
+6 -6
View File
@@ -1,4 +1,4 @@
"use client"
"use client";
import {
Toast,
@@ -7,11 +7,11 @@ import {
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
import { useToast } from "@/components/ui/use-toast"
} from "@/components/ui/toast";
import { useToast } from "@/components/ui/use-toast";
export function Toaster() {
const { toasts } = useToast()
const { toasts } = useToast();
return (
<ToastProvider>
@@ -27,9 +27,9 @@ export function Toaster() {
{action}
<ToastClose />
</Toast>
)
);
})}
<ToastViewport />
</ToastProvider>
)
);
}
+63 -66
View File
@@ -1,76 +1,73 @@
// Inspired by react-hot-toast library
import * as React from "react"
import * as React from "react";
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
} as const;
let count = 0
let count = 0;
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
count = (count + 1) % Number.MAX_SAFE_INTEGER;
return count.toString();
}
type ActionType = typeof actionTypes
type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[]
toasts: ToasterToast[];
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
});
}, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout)
}
toastTimeouts.set(toastId, timeout);
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
@@ -78,27 +75,27 @@ export const reducer = (state: State, action: Action): State => {
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
};
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
t.id === action.toast.id ? { ...t, ...action.toast } : t,
),
}
};
case "DISMISS_TOAST": {
const { toastId } = action
const { toastId } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
addToRemoveQueue(toast.id);
});
}
return {
@@ -109,46 +106,46 @@ export const reducer = (state: State, action: Action): State => {
...t,
open: false,
}
: t
: t,
),
}
};
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
};
}
}
};
const listeners: Array<(state: State) => void> = []
const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [] }
let memoryState: State = { toasts: [] };
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState)
})
listener(memoryState);
});
}
type Toast = Omit<ToasterToast, "id">
type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) {
const id = genId()
const id = genId();
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
dispatch({
type: "ADD_TOAST",
@@ -157,36 +154,36 @@ function toast({ ...props }: Toast) {
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
if (!open) dismiss();
},
},
})
});
return {
id: id,
dismiss,
update,
}
};
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState)
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState)
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1)
listeners.splice(index, 1);
}
}
}, [state])
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
};
}
export { useToast, toast }
export { useToast, toast };
+3 -3
View File
@@ -1,6 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs));
}
+1 -2
View File
@@ -5,8 +5,7 @@ import { createTRPCRouter } from "@/server/api/trpc";
*
* All routers added in /api/routers should be manually added here.
*/
export const appRouter = createTRPCRouter({
});
export const appRouter = createTRPCRouter({});
// export type definition of API
export type AppRouter = typeof appRouter;
+1 -1
View File
@@ -49,7 +49,7 @@ export const authOptions: NextAuthOptions = {
providers: [
EmailProvider({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM
from: process.env.EMAIL_FROM,
}),
/**
* ...add more providers here.
+1 -1
View File
@@ -35,7 +35,7 @@ export function TRPCReactProvider(props: {
},
}),
],
})
}),
);
return (
+8 -8
View File
@@ -1,13 +1,13 @@
import type { Config } from "tailwindcss"
import type { Config } from "tailwindcss";
const config = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
],
prefix: "",
theme: {
container: {
@@ -75,6 +75,6 @@ const config = {
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config
} satisfies Config;
export default config
export default config;