Biome format

This commit is contained in:
2025-09-03 12:37:29 -03:00
parent 55fab87db3
commit 5f6136a378
43 changed files with 1473 additions and 1379 deletions

View File

@@ -1,10 +1,10 @@
import { defineConfig } from "drizzle-kit"; import { defineConfig } from "drizzle-kit";
export default defineConfig({ export default defineConfig({
schema: "./src/db/schema", schema: "./src/db/schema",
out: "./src/db/migrations", out: "./src/db/migrations",
dialect: "postgresql", dialect: "postgresql",
dbCredentials: { dbCredentials: {
url: process.env.DATABASE_URL || "", url: process.env.DATABASE_URL || "",
}, },
}); });

View File

@@ -1,38 +1,38 @@
{ {
"name": "server", "name": "server",
"main": "src/index.ts", "main": "src/index.ts",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "tsdown", "build": "tsdown",
"check-types": "tsc -b", "check-types": "tsc -b",
"compile": "bun build --compile --minify --sourcemap --bytecode ./src/index.ts --outfile server", "compile": "bun build --compile --minify --sourcemap --bytecode ./src/index.ts --outfile server",
"dev": "bun run --hot src/index.ts", "dev": "bun run --hot src/index.ts",
"start": "bun run dist/index.js", "start": "bun run dist/index.js",
"db:push": "drizzle-kit push", "db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio", "db:studio": "drizzle-kit studio",
"db:generate": "drizzle-kit generate", "db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate", "db:migrate": "drizzle-kit migrate",
"db:start": "docker compose up -d", "db:start": "docker compose up -d",
"db:watch": "docker compose up", "db:watch": "docker compose up",
"db:stop": "docker compose stop", "db:stop": "docker compose stop",
"db:down": "docker compose down" "db:down": "docker compose down"
}, },
"dependencies": { "dependencies": {
"dotenv": "^17.2.1", "dotenv": "^17.2.1",
"zod": "^4.0.2", "zod": "^4.0.2",
"@trpc/server": "^11.5.0", "@trpc/server": "^11.5.0",
"@trpc/client": "^11.5.0", "@trpc/client": "^11.5.0",
"@hono/trpc-server": "^0.4.0", "@hono/trpc-server": "^0.4.0",
"hono": "^4.8.2", "hono": "^4.8.2",
"drizzle-orm": "^0.44.2", "drizzle-orm": "^0.44.2",
"pg": "^8.14.1", "pg": "^8.14.1",
"better-auth": "^1.3.7" "better-auth": "^1.3.7"
}, },
"devDependencies": { "devDependencies": {
"tsdown": "^0.14.1", "tsdown": "^0.14.1",
"typescript": "^5.8.2", "typescript": "^5.8.2",
"@types/bun": "^1.2.6", "@types/bun": "^1.2.6",
"drizzle-kit": "^0.31.2", "drizzle-kit": "^0.31.2",
"@types/pg": "^8.11.11" "@types/pg": "^8.11.11"
} }
} }

View File

@@ -1,51 +1,51 @@
import { pgTable, text, timestamp, boolean, serial } from "drizzle-orm/pg-core"; import { pgTable, text, timestamp, boolean, serial } from "drizzle-orm/pg-core";
export const user = pgTable("user", { export const user = pgTable("user", {
id: text("id").primaryKey(), id: text("id").primaryKey(),
name: text("name").notNull(), name: text("name").notNull(),
email: text("email").notNull().unique(), email: text("email").notNull().unique(),
emailVerified: boolean("email_verified").notNull(), emailVerified: boolean("email_verified").notNull(),
image: text("image"), image: text("image"),
createdAt: timestamp("created_at").notNull(), createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_at").notNull(), updatedAt: timestamp("updated_at").notNull(),
}); });
export const session = pgTable("session", { export const session = pgTable("session", {
id: text("id").primaryKey(), id: text("id").primaryKey(),
expiresAt: timestamp("expires_at").notNull(), expiresAt: timestamp("expires_at").notNull(),
token: text("token").notNull().unique(), token: text("token").notNull().unique(),
createdAt: timestamp("created_at").notNull(), createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_at").notNull(), updatedAt: timestamp("updated_at").notNull(),
ipAddress: text("ip_address"), ipAddress: text("ip_address"),
userAgent: text("user_agent"), userAgent: text("user_agent"),
userId: text("user_id") userId: text("user_id")
.notNull() .notNull()
.references(() => user.id, { onDelete: "cascade" }), .references(() => user.id, { onDelete: "cascade" }),
}); });
export const account = pgTable("account", { export const account = pgTable("account", {
id: text("id").primaryKey(), id: text("id").primaryKey(),
accountId: text("account_id").notNull(), accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(), providerId: text("provider_id").notNull(),
userId: text("user_id") userId: text("user_id")
.notNull() .notNull()
.references(() => user.id, { onDelete: "cascade" }), .references(() => user.id, { onDelete: "cascade" }),
accessToken: text("access_token"), accessToken: text("access_token"),
refreshToken: text("refresh_token"), refreshToken: text("refresh_token"),
idToken: text("id_token"), idToken: text("id_token"),
accessTokenExpiresAt: timestamp("access_token_expires_at"), accessTokenExpiresAt: timestamp("access_token_expires_at"),
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
scope: text("scope"), scope: text("scope"),
password: text("password"), password: text("password"),
createdAt: timestamp("created_at").notNull(), createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_at").notNull(), updatedAt: timestamp("updated_at").notNull(),
}); });
export const verification = pgTable("verification", { export const verification = pgTable("verification", {
id: text("id").primaryKey(), id: text("id").primaryKey(),
identifier: text("identifier").notNull(), identifier: text("identifier").notNull(),
value: text("value").notNull(), value: text("value").notNull(),
expiresAt: timestamp("expires_at").notNull(), expiresAt: timestamp("expires_at").notNull(),
createdAt: timestamp("created_at"), createdAt: timestamp("created_at"),
updatedAt: timestamp("updated_at"), updatedAt: timestamp("updated_at"),
}); });

View File

@@ -14,43 +14,42 @@ import { logger } from "./lib/logger";
const app = new Hono(); const app = new Hono();
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
try { try {
const composePath = join(process.cwd(), "docker-compose.yml"); const composePath = join(process.cwd(), "docker-compose.yml");
execSync(`docker-compose -f ${composePath} up -d`, { stdio: "inherit" }); execSync(`docker-compose -f ${composePath} up -d`, { stdio: "inherit" });
logger.info("Waiting for services to start..."); logger.info("Waiting for services to start...");
execSync("sleep 2"); execSync("sleep 2");
logger.info("Docker Compose started successfully"); logger.info("Docker Compose started successfully");
} catch (error) { } catch (error) {
logger.error("Failed to start Docker Compose:", error); logger.error("Failed to start Docker Compose:", error);
} }
} }
app.use(honoLogger()); app.use(honoLogger());
app.use( app.use(
"/*", "/*",
cors({ cors({
origin: process.env.CORS_ORIGIN || "", origin: process.env.CORS_ORIGIN || "",
allowMethods: ["GET", "POST", "OPTIONS"], allowMethods: ["GET", "POST", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization"], allowHeaders: ["Content-Type", "Authorization"],
credentials: true, credentials: true,
}), })
); );
app.on(["POST", "GET"], "/api/auth/**", (c) => auth.handler(c.req.raw)); app.on(["POST", "GET"], "/api/auth/**", (c) => auth.handler(c.req.raw));
app.use( app.use(
"/trpc/*", "/trpc/*",
trpcServer({ trpcServer({
router: appRouter, router: appRouter,
createContext: (_opts, context) => { createContext: (_opts, context) => {
return createContext({ context }); return createContext({ context });
}, },
}), })
); );
app.get("/", (c) => { app.get("/", (c) => {
return c.text("OK"); return c.text("OK");
}); });
export default app; export default app;

View File

@@ -4,20 +4,20 @@ import { db } from "../db";
import * as schema from "../db/schema/auth"; import * as schema from "../db/schema/auth";
export const auth = betterAuth({ export const auth = betterAuth({
database: drizzleAdapter(db, { database: drizzleAdapter(db, {
provider: "pg", provider: "pg",
schema: schema, schema: schema,
}), }),
trustedOrigins: [process.env.CORS_ORIGIN || ""], trustedOrigins: [process.env.CORS_ORIGIN || ""],
emailAndPassword: { emailAndPassword: {
enabled: true, enabled: true,
}, },
advanced: { advanced: {
defaultCookieAttributes: { defaultCookieAttributes: {
sameSite: "none", sameSite: "none",
secure: true, secure: true,
httpOnly: true, httpOnly: true,
}, },
}, },
}); });

View File

@@ -2,16 +2,16 @@ import type { Context as HonoContext } from "hono";
import { auth } from "./auth"; import { auth } from "./auth";
export type CreateContextOptions = { export type CreateContextOptions = {
context: HonoContext; context: HonoContext;
}; };
export async function createContext({ context }: CreateContextOptions) { export async function createContext({ context }: CreateContextOptions) {
const session = await auth.api.getSession({ const session = await auth.api.getSession({
headers: context.req.raw.headers, headers: context.req.raw.headers,
}); });
return { return {
session, session,
}; };
} }
export type Context = Awaited<ReturnType<typeof createContext>>; export type Context = Awaited<ReturnType<typeof createContext>>;

View File

@@ -8,17 +8,17 @@ export const router = t.router;
export const publicProcedure = t.procedure; export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(({ ctx, next }) => { export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session) { if (!ctx.session) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Authentication required", message: "Authentication required",
cause: "No session", cause: "No session",
}); });
} }
return next({ return next({
ctx: { ctx: {
...ctx, ...ctx,
session: ctx.session, session: ctx.session,
}, },
}); });
}); });

View File

@@ -1,14 +1,14 @@
import { protectedProcedure, publicProcedure, router } from "../lib/trpc"; import { protectedProcedure, publicProcedure, router } from "../lib/trpc";
export const appRouter = router({ export const appRouter = router({
healthCheck: publicProcedure.query(() => { healthCheck: publicProcedure.query(() => {
return "OK"; return "OK";
}), }),
privateData: protectedProcedure.query(({ ctx }) => { privateData: protectedProcedure.query(({ ctx }) => {
return { return {
message: "This is private", message: "This is private",
user: ctx.session.user, user: ctx.session.user,
}; };
}), }),
}); });
export type AppRouter = typeof appRouter; export type AppRouter = typeof appRouter;

View File

@@ -1,19 +1,19 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"module": "ESNext", "module": "ESNext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"baseUrl": "./", "baseUrl": "./",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
}, },
"outDir": "./dist", "outDir": "./dist",
"types": ["bun"], "types": ["bun"],
"composite": true, "composite": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"jsxImportSource": "hono/jsx" "jsxImportSource": "hono/jsx"
} }
} }

View File

@@ -1,21 +1,21 @@
{ {
"$schema": "https://ui.shadcn.com/schema.json", "$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york", "style": "new-york",
"rsc": false, "rsc": false,
"tsx": true, "tsx": true,
"tailwind": { "tailwind": {
"config": "", "config": "",
"css": "src/index.css", "css": "src/index.css",
"baseColor": "neutral", "baseColor": "neutral",
"cssVariables": true, "cssVariables": true,
"prefix": "" "prefix": ""
}, },
"aliases": { "aliases": {
"components": "@/components", "components": "@/components",
"utils": "@/lib/utils", "utils": "@/lib/utils",
"ui": "@/components/ui", "ui": "@/components/ui",
"lib": "@/lib", "lib": "@/lib",
"hooks": "@/hooks" "hooks": "@/hooks"
}, },
"iconLibrary": "lucide" "iconLibrary": "lucide"
} }

View File

@@ -1,55 +1,55 @@
{ {
"name": "web", "name": "web",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --port=3001", "dev": "vite --port=3001",
"build": "vite build", "build": "vite build",
"serve": "vite preview", "serve": "vite preview",
"start": "vite", "start": "vite",
"check-types": "tsc --noEmit", "check-types": "tsc --noEmit",
"generate-pwa-assets": "pwa-assets-generator", "generate-pwa-assets": "pwa-assets-generator",
"tauri": "tauri", "tauri": "tauri",
"desktop:dev": "tauri dev", "desktop:dev": "tauri dev",
"desktop:build": "tauri build" "desktop:build": "tauri build"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^5.1.1", "@hookform/resolvers": "^5.1.1",
"radix-ui": "^1.4.2", "radix-ui": "^1.4.2",
"@tanstack/react-form": "^1.12.3", "@tanstack/react-form": "^1.12.3",
"@tailwindcss/vite": "^4.0.15", "@tailwindcss/vite": "^4.0.15",
"@tanstack/react-router": "^1.114.25", "@tanstack/react-router": "^1.114.25",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.473.0", "lucide-react": "^0.473.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"sonner": "^2.0.5", "sonner": "^2.0.5",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tw-animate-css": "^1.2.5", "tw-animate-css": "^1.2.5",
"zod": "^4.0.2", "zod": "^4.0.2",
"@trpc/tanstack-react-query": "^11.5.0", "@trpc/tanstack-react-query": "^11.5.0",
"@trpc/client": "^11.5.0", "@trpc/client": "^11.5.0",
"@trpc/server": "^11.5.0", "@trpc/server": "^11.5.0",
"@tanstack/react-query": "^5.85.5", "@tanstack/react-query": "^5.85.5",
"vite-plugin-pwa": "^1.0.1", "vite-plugin-pwa": "^1.0.1",
"better-auth": "^1.3.7" "better-auth": "^1.3.7"
}, },
"devDependencies": { "devDependencies": {
"@tanstack/react-router-devtools": "^1.114.27", "@tanstack/react-router-devtools": "^1.114.27",
"@tanstack/router-plugin": "^1.114.27", "@tanstack/router-plugin": "^1.114.27",
"@types/node": "^22.13.13", "@types/node": "^22.13.13",
"@types/react": "^19.0.12", "@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"tailwindcss": "^4.0.15", "tailwindcss": "^4.0.15",
"vite": "^6.2.2", "vite": "^6.2.2",
"@tanstack/react-query-devtools": "^5.85.5", "@tanstack/react-query-devtools": "^5.85.5",
"@vite-pwa/assets-generator": "^1.0.0", "@vite-pwa/assets-generator": "^1.0.0",
"@tauri-apps/cli": "^2.4.0" "@tauri-apps/cli": "^2.4.0"
} }
} }

View File

@@ -1,12 +1,12 @@
import { import {
defineConfig, defineConfig,
minimal2023Preset as preset, minimal2023Preset as preset,
} from "@vite-pwa/assets-generator/config"; } from "@vite-pwa/assets-generator/config";
export default defineConfig({ export default defineConfig({
headLinkOptions: { headLinkOptions: {
preset: "2023", preset: "2023",
}, },
preset, preset,
images: ["public/logo.png"], images: ["public/logo.png"],
}); });

View File

@@ -3,29 +3,29 @@ import { ModeToggle } from "./mode-toggle";
import UserMenu from "./user-menu"; import UserMenu from "./user-menu";
export default function Header() { export default function Header() {
const links = [ const links = [
{ to: "/", label: "Home" }, { to: "/", label: "Home" },
{ to: "/dashboard", label: "Dashboard" }, { to: "/dashboard", label: "Dashboard" },
] as const; ] as const;
return ( return (
<div> <div>
<div className="flex flex-row items-center justify-between px-2 py-1"> <div className="flex flex-row items-center justify-between px-2 py-1">
<nav className="flex gap-4 text-lg"> <nav className="flex gap-4 text-lg">
{links.map(({ to, label }) => { {links.map(({ to, label }) => {
return ( return (
<Link key={to} to={to}> <Link key={to} to={to}>
{label} {label}
</Link> </Link>
); );
})} })}
</nav> </nav>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ModeToggle /> <ModeToggle />
<UserMenu /> <UserMenu />
</div> </div>
</div> </div>
<hr /> <hr />
</div> </div>
); );
} }

View File

@@ -1,9 +1,9 @@
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
export default function Loader() { export default function Loader() {
return ( return (
<div className="flex h-full items-center justify-center pt-8"> <div className="flex h-full items-center justify-center pt-8">
<Loader2 className="animate-spin" /> <Loader2 className="animate-spin" />
</div> </div>
); );
} }

View File

@@ -2,36 +2,36 @@ import { Moon, Sun } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { useTheme } from "@/components/theme-provider"; import { useTheme } from "@/components/theme-provider";
export function ModeToggle() { export function ModeToggle() {
const { setTheme } = useTheme(); const { setTheme } = useTheme();
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="outline" size="icon"> <Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" /> <Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" /> <Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
<span className="sr-only">Toggle theme</span> <span className="sr-only">Toggle theme</span>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}> <DropdownMenuItem onClick={() => setTheme("light")}>
Light Light
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}> <DropdownMenuItem onClick={() => setTheme("dark")}>
Dark Dark
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}> <DropdownMenuItem onClick={() => setTheme("system")}>
System System
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
); );
} }

View File

@@ -9,131 +9,131 @@ import { Input } from "./ui/input";
import { Label } from "./ui/label"; import { Label } from "./ui/label";
export default function SignInForm({ export default function SignInForm({
onSwitchToSignUp, onSwitchToSignUp,
}: { }: {
onSwitchToSignUp: () => void; onSwitchToSignUp: () => void;
}) { }) {
const navigate = useNavigate({ const navigate = useNavigate({
from: "/", from: "/",
}); });
const { isPending } = authClient.useSession(); const { isPending } = authClient.useSession();
const form = useForm({ const form = useForm({
defaultValues: { defaultValues: {
email: "", email: "",
password: "", password: "",
}, },
onSubmit: async ({ value }) => { onSubmit: async ({ value }) => {
await authClient.signIn.email( await authClient.signIn.email(
{ {
email: value.email, email: value.email,
password: value.password, password: value.password,
}, },
{ {
onSuccess: () => { onSuccess: () => {
navigate({ navigate({
to: "/dashboard", to: "/dashboard",
}); });
toast.success("Sign in successful"); toast.success("Sign in successful");
}, },
onError: (error) => { onError: (error) => {
toast.error(error.error.message || error.error.statusText); toast.error(error.error.message || error.error.statusText);
}, },
}, }
); );
}, },
validators: { validators: {
onSubmit: z.object({ onSubmit: z.object({
email: z.email("Invalid email address"), email: z.email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"), password: z.string().min(8, "Password must be at least 8 characters"),
}), }),
}, },
}); });
if (isPending) { if (isPending) {
return <Loader />; return <Loader />;
} }
return ( return (
<div className="mx-auto w-full mt-10 max-w-md p-6"> <div className="mx-auto w-full mt-10 max-w-md p-6">
<h1 className="mb-6 text-center text-3xl font-bold">Welcome Back</h1> <h1 className="mb-6 text-center text-3xl font-bold">Welcome Back</h1>
<form <form
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
form.handleSubmit(); form.handleSubmit();
}} }}
className="space-y-4" className="space-y-4"
> >
<div> <div>
<form.Field name="email"> <form.Field name="email">
{(field) => ( {(field) => (
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={field.name}>Email</Label> <Label htmlFor={field.name}>Email</Label>
<Input <Input
id={field.name} id={field.name}
name={field.name} name={field.name}
type="email" type="email"
value={field.state.value} value={field.state.value}
onBlur={field.handleBlur} onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)} onChange={(e) => field.handleChange(e.target.value)}
/> />
{field.state.meta.errors.map((error) => ( {field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500"> <p key={error?.message} className="text-red-500">
{error?.message} {error?.message}
</p> </p>
))} ))}
</div> </div>
)} )}
</form.Field> </form.Field>
</div> </div>
<div> <div>
<form.Field name="password"> <form.Field name="password">
{(field) => ( {(field) => (
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={field.name}>Password</Label> <Label htmlFor={field.name}>Password</Label>
<Input <Input
id={field.name} id={field.name}
name={field.name} name={field.name}
type="password" type="password"
value={field.state.value} value={field.state.value}
onBlur={field.handleBlur} onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)} onChange={(e) => field.handleChange(e.target.value)}
/> />
{field.state.meta.errors.map((error) => ( {field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500"> <p key={error?.message} className="text-red-500">
{error?.message} {error?.message}
</p> </p>
))} ))}
</div> </div>
)} )}
</form.Field> </form.Field>
</div> </div>
<form.Subscribe> <form.Subscribe>
{(state) => ( {(state) => (
<Button <Button
type="submit" type="submit"
className="w-full" className="w-full"
disabled={!state.canSubmit || state.isSubmitting} disabled={!state.canSubmit || state.isSubmitting}
> >
{state.isSubmitting ? "Submitting..." : "Sign In"} {state.isSubmitting ? "Submitting..." : "Sign In"}
</Button> </Button>
)} )}
</form.Subscribe> </form.Subscribe>
</form> </form>
<div className="mt-4 text-center"> <div className="mt-4 text-center">
<Button <Button
variant="link" variant="link"
onClick={onSwitchToSignUp} onClick={onSwitchToSignUp}
className="text-indigo-600 hover:text-indigo-800" className="text-indigo-600 hover:text-indigo-800"
> >
Need an account? Sign Up Need an account? Sign Up
</Button> </Button>
</div> </div>
</div> </div>
); );
} }

View File

@@ -9,156 +9,156 @@ import { Input } from "./ui/input";
import { Label } from "./ui/label"; import { Label } from "./ui/label";
export default function SignUpForm({ export default function SignUpForm({
onSwitchToSignIn, onSwitchToSignIn,
}: { }: {
onSwitchToSignIn: () => void; onSwitchToSignIn: () => void;
}) { }) {
const navigate = useNavigate({ const navigate = useNavigate({
from: "/", from: "/",
}); });
const { isPending } = authClient.useSession(); const { isPending } = authClient.useSession();
const form = useForm({ const form = useForm({
defaultValues: { defaultValues: {
email: "", email: "",
password: "", password: "",
name: "", name: "",
}, },
onSubmit: async ({ value }) => { onSubmit: async ({ value }) => {
await authClient.signUp.email( await authClient.signUp.email(
{ {
email: value.email, email: value.email,
password: value.password, password: value.password,
name: value.name, name: value.name,
}, },
{ {
onSuccess: () => { onSuccess: () => {
navigate({ navigate({
to: "/dashboard", to: "/dashboard",
}); });
toast.success("Sign up successful"); toast.success("Sign up successful");
}, },
onError: (error) => { onError: (error) => {
toast.error(error.error.message || error.error.statusText); toast.error(error.error.message || error.error.statusText);
}, },
}, }
); );
}, },
validators: { validators: {
onSubmit: z.object({ onSubmit: z.object({
name: z.string().min(2, "Name must be at least 2 characters"), name: z.string().min(2, "Name must be at least 2 characters"),
email: z.email("Invalid email address"), email: z.email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"), password: z.string().min(8, "Password must be at least 8 characters"),
}), }),
}, },
}); });
if (isPending) { if (isPending) {
return <Loader />; return <Loader />;
} }
return ( return (
<div className="mx-auto w-full mt-10 max-w-md p-6"> <div className="mx-auto w-full mt-10 max-w-md p-6">
<h1 className="mb-6 text-center text-3xl font-bold">Create Account</h1> <h1 className="mb-6 text-center text-3xl font-bold">Create Account</h1>
<form <form
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
form.handleSubmit(); form.handleSubmit();
}} }}
className="space-y-4" className="space-y-4"
> >
<div> <div>
<form.Field name="name"> <form.Field name="name">
{(field) => ( {(field) => (
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={field.name}>Name</Label> <Label htmlFor={field.name}>Name</Label>
<Input <Input
id={field.name} id={field.name}
name={field.name} name={field.name}
value={field.state.value} value={field.state.value}
onBlur={field.handleBlur} onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)} onChange={(e) => field.handleChange(e.target.value)}
/> />
{field.state.meta.errors.map((error) => ( {field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500"> <p key={error?.message} className="text-red-500">
{error?.message} {error?.message}
</p> </p>
))} ))}
</div> </div>
)} )}
</form.Field> </form.Field>
</div> </div>
<div> <div>
<form.Field name="email"> <form.Field name="email">
{(field) => ( {(field) => (
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={field.name}>Email</Label> <Label htmlFor={field.name}>Email</Label>
<Input <Input
id={field.name} id={field.name}
name={field.name} name={field.name}
type="email" type="email"
value={field.state.value} value={field.state.value}
onBlur={field.handleBlur} onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)} onChange={(e) => field.handleChange(e.target.value)}
/> />
{field.state.meta.errors.map((error) => ( {field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500"> <p key={error?.message} className="text-red-500">
{error?.message} {error?.message}
</p> </p>
))} ))}
</div> </div>
)} )}
</form.Field> </form.Field>
</div> </div>
<div> <div>
<form.Field name="password"> <form.Field name="password">
{(field) => ( {(field) => (
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={field.name}>Password</Label> <Label htmlFor={field.name}>Password</Label>
<Input <Input
id={field.name} id={field.name}
name={field.name} name={field.name}
type="password" type="password"
value={field.state.value} value={field.state.value}
onBlur={field.handleBlur} onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)} onChange={(e) => field.handleChange(e.target.value)}
/> />
{field.state.meta.errors.map((error) => ( {field.state.meta.errors.map((error) => (
<p key={error?.message} className="text-red-500"> <p key={error?.message} className="text-red-500">
{error?.message} {error?.message}
</p> </p>
))} ))}
</div> </div>
)} )}
</form.Field> </form.Field>
</div> </div>
<form.Subscribe> <form.Subscribe>
{(state) => ( {(state) => (
<Button <Button
type="submit" type="submit"
className="w-full" className="w-full"
disabled={!state.canSubmit || state.isSubmitting} disabled={!state.canSubmit || state.isSubmitting}
> >
{state.isSubmitting ? "Submitting..." : "Sign Up"} {state.isSubmitting ? "Submitting..." : "Sign Up"}
</Button> </Button>
)} )}
</form.Subscribe> </form.Subscribe>
</form> </form>
<div className="mt-4 text-center"> <div className="mt-4 text-center">
<Button <Button
variant="link" variant="link"
onClick={onSwitchToSignIn} onClick={onSwitchToSignIn}
className="text-indigo-600 hover:text-indigo-800" className="text-indigo-600 hover:text-indigo-800"
> >
Already have an account? Sign In Already have an account? Sign In
</Button> </Button>
</div> </div>
</div> </div>
); );
} }

View File

@@ -2,10 +2,10 @@ import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes"; import { ThemeProvider as NextThemesProvider } from "next-themes";
export function ThemeProvider({ export function ThemeProvider({
children, children,
...props ...props
}: React.ComponentProps<typeof NextThemesProvider>) { }: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>; return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
} }
export { useTheme } from "next-themes"; export { useTheme } from "next-themes";

View File

@@ -5,55 +5,55 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{ {
variants: { variants: {
variant: { variant: {
default: default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive: destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline: outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary: secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost: ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3", default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4", lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9", icon: "size-9",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
size: "default", size: "default",
}, },
}, }
); );
function Button({ function Button({
className, className,
variant, variant,
size, size,
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<"button"> & }: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & { VariantProps<typeof buttonVariants> & {
asChild?: boolean; asChild?: boolean;
}) { }) {
const Comp = asChild ? SlotPrimitive.Slot : "button"; const Comp = asChild ? SlotPrimitive.Slot : "button";
return ( return (
<Comp <Comp
data-slot="button" data-slot="button"
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
{...props} {...props}
/> />
); );
} }
export { Button, buttonVariants }; export { Button, buttonVariants };

View File

@@ -3,90 +3,90 @@ import * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) { function Card({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card" data-slot="card"
className={cn( className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className, className
)} )}
{...props} {...props}
/> />
); );
} }
function CardHeader({ className, ...props }: React.ComponentProps<"div">) { function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-header" data-slot="card-header"
className={cn( className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className, className
)} )}
{...props} {...props}
/> />
); );
} }
function CardTitle({ className, ...props }: React.ComponentProps<"div">) { function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-title" data-slot="card-title"
className={cn("leading-none font-semibold", className)} className={cn("leading-none font-semibold", className)}
{...props} {...props}
/> />
); );
} }
function CardDescription({ className, ...props }: React.ComponentProps<"div">) { function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-description" data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...props}
/> />
); );
} }
function CardAction({ className, ...props }: React.ComponentProps<"div">) { function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-action" data-slot="card-action"
className={cn( className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end", "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className, className
)} )}
{...props} {...props}
/> />
); );
} }
function CardContent({ className, ...props }: React.ComponentProps<"div">) { function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-content" data-slot="card-content"
className={cn("px-6", className)} className={cn("px-6", className)}
{...props} {...props}
/> />
); );
} }
function CardFooter({ className, ...props }: React.ComponentProps<"div">) { function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-footer" data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)} className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props} {...props}
/> />
); );
} }
export { export {
Card, Card,
CardHeader, CardHeader,
CardFooter, CardFooter,
CardTitle, CardTitle,
CardAction, CardAction,
CardDescription, CardDescription,
CardContent, CardContent,
}; };

View File

@@ -5,26 +5,26 @@ import { CheckIcon } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function Checkbox({ function Checkbox({
className, className,
...props ...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) { }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return ( return (
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
data-slot="checkbox" data-slot="checkbox"
className={cn( className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", "peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className, className
)} )}
{...props} {...props}
> >
<CheckboxPrimitive.Indicator <CheckboxPrimitive.Indicator
data-slot="checkbox-indicator" data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none" className="flex items-center justify-center text-current transition-none"
> >
<CheckIcon className="size-3.5" /> <CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator> </CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root> </CheckboxPrimitive.Root>
); );
} }
export { Checkbox }; export { Checkbox };

View File

@@ -7,251 +7,251 @@ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function DropdownMenu({ function DropdownMenu({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />; return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
} }
function DropdownMenuPortal({ function DropdownMenuPortal({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return ( return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} /> <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
); );
} }
function DropdownMenuTrigger({ function DropdownMenuTrigger({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return ( return (
<DropdownMenuPrimitive.Trigger <DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger" data-slot="dropdown-menu-trigger"
{...props} {...props}
/> />
); );
} }
function DropdownMenuContent({ function DropdownMenuContent({
className, className,
sideOffset = 4, sideOffset = 4,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return ( return (
<DropdownMenuPrimitive.Portal> <DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content <DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content" data-slot="dropdown-menu-content"
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"bg-popover text-popover-foreground 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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md", "bg-popover text-popover-foreground 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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className, className
)} )}
{...props} {...props}
/> />
</DropdownMenuPrimitive.Portal> </DropdownMenuPrimitive.Portal>
); );
} }
function DropdownMenuGroup({ function DropdownMenuGroup({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return ( return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} /> <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
); );
} }
function DropdownMenuItem({ function DropdownMenuItem({
className, className,
inset, inset,
variant = "default", variant = "default",
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean; inset?: boolean;
variant?: "default" | "destructive"; variant?: "default" | "destructive";
}) { }) {
return ( return (
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item" data-slot="dropdown-menu-item"
data-inset={inset} data-inset={inset}
data-variant={variant} data-variant={variant}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className, className
)} )}
{...props} {...props}
/> />
); );
} }
function DropdownMenuCheckboxItem({ function DropdownMenuCheckboxItem({
className, className,
children, children,
checked, checked,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return ( return (
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item" data-slot="dropdown-menu-checkbox-item"
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className, className
)} )}
checked={checked} checked={checked}
{...props} {...props}
> >
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator> <DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" /> <CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator> </DropdownMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>
); );
} }
function DropdownMenuRadioGroup({ function DropdownMenuRadioGroup({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return ( return (
<DropdownMenuPrimitive.RadioGroup <DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group" data-slot="dropdown-menu-radio-group"
{...props} {...props}
/> />
); );
} }
function DropdownMenuRadioItem({ function DropdownMenuRadioItem({
className, className,
children, children,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return ( return (
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item" data-slot="dropdown-menu-radio-item"
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className, className
)} )}
{...props} {...props}
> >
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator> <DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" /> <CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator> </DropdownMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</DropdownMenuPrimitive.RadioItem> </DropdownMenuPrimitive.RadioItem>
); );
} }
function DropdownMenuLabel({ function DropdownMenuLabel({
className, className,
inset, inset,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean; inset?: boolean;
}) { }) {
return ( return (
<DropdownMenuPrimitive.Label <DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label" data-slot="dropdown-menu-label"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className, className
)} )}
{...props} {...props}
/> />
); );
} }
function DropdownMenuSeparator({ function DropdownMenuSeparator({
className, className,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return ( return (
<DropdownMenuPrimitive.Separator <DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator" data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)} className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props} {...props}
/> />
); );
} }
function DropdownMenuShortcut({ function DropdownMenuShortcut({
className, className,
...props ...props
}: React.ComponentProps<"span">) { }: React.ComponentProps<"span">) {
return ( return (
<span <span
data-slot="dropdown-menu-shortcut" data-slot="dropdown-menu-shortcut"
className={cn( className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest", "text-muted-foreground ml-auto text-xs tracking-widest",
className, className
)} )}
{...props} {...props}
/> />
); );
} }
function DropdownMenuSub({ function DropdownMenuSub({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />; return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
} }
function DropdownMenuSubTrigger({ function DropdownMenuSubTrigger({
className, className,
inset, inset,
children, children,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean; inset?: boolean;
}) { }) {
return ( return (
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger" data-slot="dropdown-menu-sub-trigger"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8", "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className, className
)} )}
{...props} {...props}
> >
{children} {children}
<ChevronRightIcon className="ml-auto size-4" /> <ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
); );
} }
function DropdownMenuSubContent({ function DropdownMenuSubContent({
className, className,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return ( return (
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content" data-slot="dropdown-menu-sub-content"
className={cn( className={cn(
"bg-popover text-popover-foreground 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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg", "bg-popover text-popover-foreground 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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className, className
)} )}
{...props} {...props}
/> />
); );
} }
export { export {
DropdownMenu, DropdownMenu,
DropdownMenuPortal, DropdownMenuPortal,
DropdownMenuTrigger, DropdownMenuTrigger,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuGroup, DropdownMenuGroup,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
DropdownMenuRadioGroup, DropdownMenuRadioGroup,
DropdownMenuRadioItem, DropdownMenuRadioItem,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuShortcut, DropdownMenuShortcut,
DropdownMenuSub, DropdownMenuSub,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuSubContent, DropdownMenuSubContent,
}; };

View File

@@ -3,19 +3,19 @@ import * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) { function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return ( return (
<input <input
type={type} type={type}
data-slot="input" data-slot="input"
className={cn( className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className, className
)} )}
{...props} {...props}
/> />
); );
} }
export { Input }; export { Input };

View File

@@ -4,19 +4,19 @@ import { Label as LabelPrimitive } from "radix-ui";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function Label({ function Label({
className, className,
...props ...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) { }: React.ComponentProps<typeof LabelPrimitive.Root>) {
return ( return (
<LabelPrimitive.Root <LabelPrimitive.Root
data-slot="label" data-slot="label"
className={cn( className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className, className
)} )}
{...props} {...props}
/> />
); );
} }
export { Label }; export { Label };

View File

@@ -1,13 +1,13 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) { function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="skeleton" data-slot="skeleton"
className={cn("bg-accent animate-pulse rounded-md", className)} className={cn("bg-accent animate-pulse rounded-md", className)}
{...props} {...props}
/> />
); );
} }
export { Skeleton }; export { Skeleton };

View File

@@ -4,22 +4,22 @@ import { useTheme } from "next-themes";
import { Toaster as Sonner, type ToasterProps } from "sonner"; import { Toaster as Sonner, type ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => { const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme(); const { theme = "system" } = useTheme();
return ( return (
<Sonner <Sonner
theme={theme as ToasterProps["theme"]} theme={theme as ToasterProps["theme"]}
className="toaster group" className="toaster group"
style={ style={
{ {
"--normal-bg": "var(--popover)", "--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)", "--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)", "--normal-border": "var(--border)",
} as React.CSSProperties } as React.CSSProperties
} }
{...props} {...props}
/> />
); );
}; };
export { Toaster }; export { Toaster };

View File

@@ -1,10 +1,10 @@
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
@@ -13,50 +13,50 @@ import { Skeleton } from "./ui/skeleton";
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
export default function UserMenu() { export default function UserMenu() {
const navigate = useNavigate(); const navigate = useNavigate();
const { data: session, isPending } = authClient.useSession(); const { data: session, isPending } = authClient.useSession();
if (isPending) { if (isPending) {
return <Skeleton className="h-9 w-24" />; return <Skeleton className="h-9 w-24" />;
} }
if (!session) { if (!session) {
return ( return (
<Button variant="outline" asChild> <Button variant="outline" asChild>
<Link to="/login">Sign In</Link> <Link to="/login">Sign In</Link>
</Button> </Button>
); );
} }
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="outline">{session.user.name}</Button> <Button variant="outline">{session.user.name}</Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="bg-card"> <DropdownMenuContent className="bg-card">
<DropdownMenuLabel>My Account</DropdownMenuLabel> <DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem>{session.user.email}</DropdownMenuItem> <DropdownMenuItem>{session.user.email}</DropdownMenuItem>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Button <Button
variant="destructive" variant="destructive"
className="w-full" className="w-full"
onClick={() => { onClick={() => {
authClient.signOut({ authClient.signOut({
fetchOptions: { fetchOptions: {
onSuccess: () => { onSuccess: () => {
navigate({ navigate({
to: "/", to: "/",
}); });
}, },
}, },
}); });
}} }}
> >
Sign Out Sign Out
</Button> </Button>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
); );
} }

View File

@@ -4,132 +4,132 @@
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:where(.dark, .dark *));
@theme { @theme {
--font-sans: --font-sans:
"Inter", "Geist", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Inter", "Geist", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
} }
html, html,
body { body {
@apply bg-white dark:bg-gray-950; @apply bg-white dark:bg-gray-950;
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
color-scheme: dark; color-scheme: dark;
} }
} }
:root { :root {
--radius: 0.625rem; --radius: 0.625rem;
--background: oklch(1 0 0); --background: oklch(1 0 0);
--foreground: oklch(0.145 0 0); --foreground: oklch(0.145 0 0);
--card: oklch(1 0 0); --card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0); --card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0); --popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0); --primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0); --primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0); --secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0); --secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0); --muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0); --muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0); --accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0); --accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0); --border: oklch(0.922 0 0);
--input: oklch(0.922 0 0); --input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0); --ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116); --chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704); --chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392); --chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429); --chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08); --chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0); --sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0); --sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0); --sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0); --sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0); --sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0); --sidebar-ring: oklch(0.708 0 0);
} }
.dark { .dark {
--background: oklch(0.145 0 0); --background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0); --foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0); --card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0); --card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0); --popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0); --popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0); --primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0); --primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0); --secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0); --secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0); --muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0); --muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0); --accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216); --destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%); --border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%); --input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0); --ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376); --chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48); --chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08); --chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9); --chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439); --chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0); --sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0); --sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376); --sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0); --sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%); --sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0); --sidebar-ring: oklch(0.556 0 0);
} }
@theme inline { @theme inline {
--radius-sm: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px); --radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius); --radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px); --radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background); --color-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
--color-card: var(--card); --color-card: var(--card);
--color-card-foreground: var(--card-foreground); --color-card-foreground: var(--card-foreground);
--color-popover: var(--popover); --color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground); --color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary); --color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground); --color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary); --color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground); --color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted); --color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground); --color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent); --color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground); --color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive); --color-destructive: var(--destructive);
--color-border: var(--border); --color-border: var(--border);
--color-input: var(--input); --color-input: var(--input);
--color-ring: var(--ring); --color-ring: var(--ring);
--color-chart-1: var(--chart-1); --color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2); --color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3); --color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4); --color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5); --color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar); --color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground); --color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary); --color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground); --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent); --color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border); --color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring); --color-sidebar-ring: var(--sidebar-ring);
} }
@layer base { @layer base {
* { * {
@apply border-border outline-ring/50; @apply border-border outline-ring/50;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }

View File

@@ -1,5 +1,5 @@
import { createAuthClient } from "better-auth/react"; import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({ export const authClient = createAuthClient({
baseURL: import.meta.env.VITE_SERVER_URL, baseURL: import.meta.env.VITE_SERVER_URL,
}); });

View File

@@ -2,5 +2,5 @@ import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
} }

View File

@@ -7,30 +7,30 @@ import { QueryClientProvider } from "@tanstack/react-query";
import { queryClient, trpc } from "./utils/trpc"; import { queryClient, trpc } from "./utils/trpc";
const router = createRouter({ const router = createRouter({
routeTree, routeTree,
defaultPreload: "intent", defaultPreload: "intent",
defaultPendingComponent: () => <Loader />, defaultPendingComponent: () => <Loader />,
context: { trpc, queryClient }, context: { trpc, queryClient },
Wrap: function WrapComponent({ children }: { children: React.ReactNode }) { Wrap: function WrapComponent({ children }: { children: React.ReactNode }) {
return ( return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider> <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
); );
}, },
}); });
declare module "@tanstack/react-router" { declare module "@tanstack/react-router" {
interface Register { interface Register {
router: typeof router; router: typeof router;
} }
} }
const rootElement = document.getElementById("app"); const rootElement = document.getElementById("app");
if (!rootElement) { if (!rootElement) {
throw new Error("Root element not found"); throw new Error("Root element not found");
} }
if (!rootElement.innerHTML) { if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement); const root = ReactDOM.createRoot(rootElement);
root.render(<RouterProvider router={router} />); root.render(<RouterProvider router={router} />);
} }

View File

@@ -0,0 +1,95 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as LoginRouteImport } from './routes/login'
import { Route as DashboardRouteImport } from './routes/dashboard'
import { Route as IndexRouteImport } from './routes/index'
const LoginRoute = LoginRouteImport.update({
id: '/login',
path: '/login',
getParentRoute: () => rootRouteImport,
} as any)
const DashboardRoute = DashboardRouteImport.update({
id: '/dashboard',
path: '/dashboard',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/dashboard': typeof DashboardRoute
'/login': typeof LoginRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/dashboard': typeof DashboardRoute
'/login': typeof LoginRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/dashboard': typeof DashboardRoute
'/login': typeof LoginRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/dashboard' | '/login'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/dashboard' | '/login'
id: '__root__' | '/' | '/dashboard' | '/login'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
DashboardRoute: typeof DashboardRoute
LoginRoute: typeof LoginRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/login': {
id: '/login'
path: '/login'
fullPath: '/login'
preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRouteImport
}
'/dashboard': {
id: '/dashboard'
path: '/dashboard'
fullPath: '/dashboard'
preLoaderRoute: typeof DashboardRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
DashboardRoute: DashboardRoute,
LoginRoute: LoginRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()

View File

@@ -6,62 +6,62 @@ import type { trpc } from "@/utils/trpc";
import type { QueryClient } from "@tanstack/react-query"; import type { QueryClient } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { import {
HeadContent, HeadContent,
Outlet, Outlet,
createRootRouteWithContext, createRootRouteWithContext,
useRouterState, useRouterState,
} from "@tanstack/react-router"; } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import "../index.css"; import "../index.css";
export interface RouterAppContext { export interface RouterAppContext {
trpc: typeof trpc; trpc: typeof trpc;
queryClient: QueryClient; queryClient: QueryClient;
} }
export const Route = createRootRouteWithContext<RouterAppContext>()({ export const Route = createRootRouteWithContext<RouterAppContext>()({
component: RootComponent, component: RootComponent,
head: () => ({ head: () => ({
meta: [ meta: [
{ {
title: "Reflecto", title: "Reflecto",
}, },
{ {
name: "description", name: "description",
content: "Reflecto is a web application", content: "Reflecto is a web application",
}, },
], ],
links: [ links: [
{ {
rel: "icon", rel: "icon",
href: "/favicon.ico", href: "/favicon.ico",
}, },
], ],
}), }),
}); });
function RootComponent() { function RootComponent() {
const isFetching = useRouterState({ const isFetching = useRouterState({
select: (s) => s.isLoading, select: (s) => s.isLoading,
}); });
return ( return (
<> <>
<HeadContent /> <HeadContent />
<ThemeProvider <ThemeProvider
attribute="class" attribute="class"
defaultTheme="dark" defaultTheme="dark"
disableTransitionOnChange disableTransitionOnChange
storageKey="vite-ui-theme" storageKey="vite-ui-theme"
> >
<div className="grid grid-rows-[auto_1fr] h-svh"> <div className="grid grid-rows-[auto_1fr] h-svh">
<Header /> <Header />
{isFetching ? <Loader /> : <Outlet />} {isFetching ? <Loader /> : <Outlet />}
</div> </div>
<Toaster richColors /> <Toaster richColors />
</ThemeProvider> </ThemeProvider>
<TanStackRouterDevtools position="bottom-left" /> <TanStackRouterDevtools position="bottom-left" />
<ReactQueryDevtools position="bottom" buttonPosition="bottom-right" /> <ReactQueryDevtools position="bottom" buttonPosition="bottom-right" />
</> </>
); );
} }

View File

@@ -5,33 +5,33 @@ import { createFileRoute } from "@tanstack/react-router";
import { useEffect } from "react"; import { useEffect } from "react";
export const Route = createFileRoute("/dashboard")({ export const Route = createFileRoute("/dashboard")({
component: RouteComponent, component: RouteComponent,
}); });
function RouteComponent() { function RouteComponent() {
const { data: session, isPending } = authClient.useSession(); const { data: session, isPending } = authClient.useSession();
const navigate = Route.useNavigate(); const navigate = Route.useNavigate();
const privateData = useQuery(trpc.privateData.queryOptions()); const privateData = useQuery(trpc.privateData.queryOptions());
useEffect(() => { useEffect(() => {
if (!session && !isPending) { if (!session && !isPending) {
navigate({ navigate({
to: "/login", to: "/login",
}); });
} }
}, [session, isPending]); }, [session, isPending]);
if (isPending) { if (isPending) {
return <div>Loading...</div>; return <div>Loading...</div>;
} }
return ( return (
<div> <div>
<h1>Dashboard</h1> <h1>Dashboard</h1>
<p>Welcome {session?.user.name}</p> <p>Welcome {session?.user.name}</p>
<p>privateData: {privateData.data?.message}</p> <p>privateData: {privateData.data?.message}</p>
</div> </div>
); );
} }

View File

@@ -3,7 +3,7 @@ import { trpc } from "@/utils/trpc";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/")({
component: HomeComponent, component: HomeComponent,
}); });
const TITLE_TEXT = ` const TITLE_TEXT = `
@@ -23,28 +23,28 @@ const TITLE_TEXT = `
`; `;
function HomeComponent() { function HomeComponent() {
const healthCheck = useQuery(trpc.healthCheck.queryOptions()); const healthCheck = useQuery(trpc.healthCheck.queryOptions());
return ( return (
<div className="container mx-auto max-w-3xl px-4 py-2"> <div className="container mx-auto max-w-3xl px-4 py-2">
<pre className="overflow-x-auto font-mono text-sm">{TITLE_TEXT}</pre> <pre className="overflow-x-auto font-mono text-sm">{TITLE_TEXT}</pre>
<div className="grid gap-6"> <div className="grid gap-6">
<section className="rounded-lg border p-4"> <section className="rounded-lg border p-4">
<h2 className="mb-2 font-medium">API Status</h2> <h2 className="mb-2 font-medium">API Status</h2>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div <div
className={`h-2 w-2 rounded-full ${healthCheck.data ? "bg-green-500" : "bg-red-500"}`} className={`h-2 w-2 rounded-full ${healthCheck.data ? "bg-green-500" : "bg-red-500"}`}
/> />
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{healthCheck.isLoading {healthCheck.isLoading
? "Checking..." ? "Checking..."
: healthCheck.data : healthCheck.data
? "Connected" ? "Connected"
: "Disconnected"} : "Disconnected"}
</span> </span>
</div> </div>
</section> </section>
</div> </div>
</div> </div>
); );
} }

View File

@@ -4,15 +4,15 @@ import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react"; import { useState } from "react";
export const Route = createFileRoute("/login")({ export const Route = createFileRoute("/login")({
component: RouteComponent, component: RouteComponent,
}); });
function RouteComponent() { function RouteComponent() {
const [showSignIn, setShowSignIn] = useState(false); const [showSignIn, setShowSignIn] = useState(false);
return showSignIn ? ( return showSignIn ? (
<SignInForm onSwitchToSignUp={() => setShowSignIn(false)} /> <SignInForm onSwitchToSignUp={() => setShowSignIn(false)} />
) : ( ) : (
<SignUpForm onSwitchToSignIn={() => setShowSignIn(true)} /> <SignUpForm onSwitchToSignIn={() => setShowSignIn(true)} />
); );
} }

View File

@@ -5,35 +5,35 @@ import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query";
import { toast } from "sonner"; import { toast } from "sonner";
export const queryClient = new QueryClient({ export const queryClient = new QueryClient({
queryCache: new QueryCache({ queryCache: new QueryCache({
onError: (error) => { onError: (error) => {
toast.error(error.message, { toast.error(error.message, {
action: { action: {
label: "retry", label: "retry",
onClick: () => { onClick: () => {
queryClient.invalidateQueries(); queryClient.invalidateQueries();
}, },
}, },
}); });
}, },
}), }),
}); });
export const trpcClient = createTRPCClient<AppRouter>({ export const trpcClient = createTRPCClient<AppRouter>({
links: [ links: [
httpBatchLink({ httpBatchLink({
url: `${import.meta.env.VITE_SERVER_URL}/trpc`, url: `${import.meta.env.VITE_SERVER_URL}/trpc`,
fetch(url, options) { fetch(url, options) {
return fetch(url, { return fetch(url, {
...options, ...options,
credentials: "include", credentials: "include",
}); });
}, },
}), }),
], ],
}); });
export const trpc = createTRPCOptionsProxy<AppRouter>({ export const trpc = createTRPCOptionsProxy<AppRouter>({
client: trpcClient, client: trpcClient,
queryClient, queryClient,
}); });

View File

@@ -1,23 +1,23 @@
{ {
"compilerOptions": { "compilerOptions": {
"strict": true, "strict": true,
"esModuleInterop": true, "esModuleInterop": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"target": "ESNext", "target": "ESNext",
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Bundler", "moduleResolution": "Bundler",
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"skipLibCheck": true, "skipLibCheck": true,
"types": ["vite/client"], "types": ["vite/client"],
"rootDirs": ["."], "rootDirs": ["."],
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
} }
}, },
"references": [ "references": [
{ {
"path": "../server" "path": "../server"
} }
] ]
} }

View File

@@ -6,25 +6,25 @@ import path from "node:path";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
tailwindcss(), tailwindcss(),
tanstackRouter({}), tanstackRouter({}),
react(), react(),
VitePWA({ VitePWA({
registerType: "autoUpdate", registerType: "autoUpdate",
manifest: { manifest: {
name: "Reflecto", name: "Reflecto",
short_name: "Reflecto", short_name: "Reflecto",
description: "Reflecto - PWA Application", description: "Reflecto - PWA Application",
theme_color: "#0c0c0c", theme_color: "#0c0c0c",
}, },
pwaAssets: { disabled: false, config: true }, pwaAssets: { disabled: false, config: true },
devOptions: { enabled: true }, devOptions: { enabled: true },
}), }),
], ],
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": path.resolve(__dirname, "./src"),
}, },
}, },
}); });

View File

@@ -1,25 +1,25 @@
{ {
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"files": { "files": {
"ignoreUnknown": false, "ignoreUnknown": false,
"includes": [ "includes": [
"**", "**",
"!**/.next", "!**/.next",
"!**/dist", "!**/dist",
"!**/.turbo", "!**/.turbo",
"!**/dev-dist", "!**/dev-dist",
"!**/.zed", "!**/.zed",
"!**/.vscode", "!**/.vscode",
"!**/routeTree.gen.ts", "!**/routeTree.gen.ts",
"!**/src-tauri", "!**/src-tauri",
"!**/.nuxt", "!**/.nuxt",
"!bts.jsonc", "!bts.jsonc",
"!**/.expo", "!**/.expo",
"!**/.wrangler", "!**/.wrangler",
"!**/.alchemy", "!**/.alchemy",
"!**/wrangler.jsonc", "!**/wrangler.jsonc",
"!**/.source" "!**/.source"
] ]
}, },
"extends": ["ultracite"] "extends": ["ultracite"]
} }

View File

@@ -1,39 +1,39 @@
{ {
"name": "Reflecto", "name": "Reflecto",
"private": true, "private": true,
"workspaces": [ "workspaces": [
"apps/*", "apps/*",
"packages/*" "packages/*"
], ],
"scripts": { "scripts": {
"check": "biome check --write .", "check": "biome check --write .",
"dev": "turbo dev", "dev": "turbo dev",
"build": "turbo build", "build": "turbo build",
"check-types": "turbo check-types", "check-types": "turbo check-types",
"dev:native": "turbo -F native dev", "dev:native": "turbo -F native dev",
"dev:web": "turbo -F web dev", "dev:web": "turbo -F web dev",
"dev:server": "turbo -F server dev", "dev:server": "turbo -F server dev",
"db:push": "turbo -F server db:push", "db:push": "turbo -F server db:push",
"db:studio": "turbo -F server db:studio", "db:studio": "turbo -F server db:studio",
"db:generate": "turbo -F server db:generate", "db:generate": "turbo -F server db:generate",
"db:migrate": "turbo -F server db:migrate", "db:migrate": "turbo -F server db:migrate",
"db:start": "turbo -F server db:start", "db:start": "turbo -F server db:start",
"db:watch": "turbo -F server db:watch", "db:watch": "turbo -F server db:watch",
"db:stop": "turbo -F server db:stop", "db:stop": "turbo -F server db:stop",
"db:down": "turbo -F server db:down" "db:down": "turbo -F server db:down"
}, },
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {
"turbo": "^2.5.4", "turbo": "^2.5.4",
"@biomejs/biome": "2.2.2", "@biomejs/biome": "2.2.2",
"ultracite": "5.3.2", "ultracite": "5.3.2",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^16.1.2" "lint-staged": "^16.1.2"
}, },
"lint-staged": { "lint-staged": {
"*.{js,jsx,ts,tsx,json,jsonc,css,scss,md,mdx}": [ "*.{js,jsx,ts,tsx,json,jsonc,css,scss,md,mdx}": [
"pnpm dlx ultracite fix" "pnpm dlx ultracite fix"
] ]
}, },
"packageManager": "pnpm@10.14.0" "packageManager": "pnpm@10.14.0"
} }

View File

@@ -1,5 +1,5 @@
{ {
"compilerOptions": { "compilerOptions": {
"strictNullChecks": true "strictNullChecks": true
} }
} }

View File

@@ -1,53 +1,53 @@
{ {
"$schema": "https://turbo.build/schema.json", "$schema": "https://turbo.build/schema.json",
"ui": "tui", "ui": "tui",
"tasks": { "tasks": {
"build": { "build": {
"dependsOn": ["^build"], "dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"], "inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": ["dist/**"] "outputs": ["dist/**"]
}, },
"lint": { "lint": {
"dependsOn": ["^lint"] "dependsOn": ["^lint"]
}, },
"check-types": { "check-types": {
"dependsOn": ["^check-types"] "dependsOn": ["^check-types"]
}, },
"dev": { "dev": {
"cache": false, "cache": false,
"persistent": true "persistent": true
}, },
"db:push": { "db:push": {
"cache": false, "cache": false,
"persistent": true "persistent": true
}, },
"db:studio": { "db:studio": {
"cache": false, "cache": false,
"persistent": true "persistent": true
}, },
"db:migrate": { "db:migrate": {
"cache": false, "cache": false,
"persistent": true "persistent": true
}, },
"db:generate": { "db:generate": {
"cache": false, "cache": false,
"persistent": true "persistent": true
}, },
"db:start": { "db:start": {
"cache": false, "cache": false,
"persistent": true "persistent": true
}, },
"db:stop": { "db:stop": {
"cache": false, "cache": false,
"persistent": true "persistent": true
}, },
"db:watch": { "db:watch": {
"cache": false, "cache": false,
"persistent": true "persistent": true
}, },
"db:down": { "db:down": {
"cache": false, "cache": false,
"persistent": true "persistent": true
} }
} }
} }