From 759e3369562b7922ebabdaca0fa6615897bfd84a Mon Sep 17 00:00:00 2001 From: Jonatan Granqvist Date: Tue, 10 Feb 2026 19:40:51 +0100 Subject: [PATCH] initial commit --- .claude/CLAUDE.md | 126 + .claude/settings.json | 15 + .gitignore | 50 + .vscode/settings.json | 11 + README.md | 64 + apps/web/.gitignore | 60 + apps/web/components.json | 24 + apps/web/package.json | 50 + apps/web/public/robots.txt | 3 + apps/web/src/components/header.tsx | 30 + apps/web/src/components/loader.tsx | 9 + apps/web/src/components/sign-in-form.tsx | 137 + apps/web/src/components/sign-up-form.tsx | 162 + apps/web/src/components/ui/button.tsx | 58 + apps/web/src/components/ui/card.tsx | 89 + apps/web/src/components/ui/checkbox.tsx | 26 + apps/web/src/components/ui/dropdown-menu.tsx | 241 + apps/web/src/components/ui/input.tsx | 20 + apps/web/src/components/ui/label.tsx | 20 + apps/web/src/components/ui/skeleton.tsx | 13 + apps/web/src/components/ui/sonner.tsx | 45 + apps/web/src/components/user-menu.tsx | 63 + apps/web/src/functions/get-user.ts | 9 + apps/web/src/index.css | 128 + apps/web/src/lib/auth-client.ts | 3 + apps/web/src/lib/utils.ts | 6 + apps/web/src/middleware/auth.ts | 11 + apps/web/src/router.tsx | 24 + apps/web/src/routes/__root.tsx | 53 + apps/web/src/routes/api/auth/$.ts | 15 + apps/web/src/routes/dashboard.tsx | 29 + apps/web/src/routes/index.tsx | 34 + apps/web/src/routes/login.tsx | 19 + apps/web/tsconfig.json | 28 + apps/web/vite.config.ts | 12 + bts.jsonc | 31 + eslint.config.mjs | 4 + package.json | 42 + packages/auth/.gitignore | 34 + packages/auth/package.json | 24 + packages/auth/src/index.ts | 19 + packages/auth/tsconfig.json | 11 + packages/config/package.json | 5 + packages/config/tsconfig.base.json | 22 + packages/db/.gitignore | 35 + packages/db/docker-compose.yml | 23 + packages/db/drizzle.config.ts | 15 + packages/db/package.json | 35 + packages/db/src/index.ts | 6 + packages/db/src/schema/auth.ts | 93 + packages/db/src/schema/index.ts | 2 + packages/db/tsconfig.json | 11 + packages/env/package.json | 20 + packages/env/src/server.ts | 15 + packages/env/src/web.ts | 9 + packages/env/tsconfig.json | 6 + pnpm-lock.yaml | 7467 ++++++++++++++++++ pnpm-workspace.yaml | 9 + prettier.config.mjs | 1 + stylelint.config.mjs | 1 + tsconfig.json | 6 + turbo.json | 49 + 62 files changed, 9682 insertions(+) create mode 100644 .claude/CLAUDE.md create mode 100644 .claude/settings.json create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 README.md create mode 100644 apps/web/.gitignore create mode 100644 apps/web/components.json create mode 100644 apps/web/package.json create mode 100644 apps/web/public/robots.txt create mode 100644 apps/web/src/components/header.tsx create mode 100644 apps/web/src/components/loader.tsx create mode 100644 apps/web/src/components/sign-in-form.tsx create mode 100644 apps/web/src/components/sign-up-form.tsx create mode 100644 apps/web/src/components/ui/button.tsx create mode 100644 apps/web/src/components/ui/card.tsx create mode 100644 apps/web/src/components/ui/checkbox.tsx create mode 100644 apps/web/src/components/ui/dropdown-menu.tsx create mode 100644 apps/web/src/components/ui/input.tsx create mode 100644 apps/web/src/components/ui/label.tsx create mode 100644 apps/web/src/components/ui/skeleton.tsx create mode 100644 apps/web/src/components/ui/sonner.tsx create mode 100644 apps/web/src/components/user-menu.tsx create mode 100644 apps/web/src/functions/get-user.ts create mode 100644 apps/web/src/index.css create mode 100644 apps/web/src/lib/auth-client.ts create mode 100644 apps/web/src/lib/utils.ts create mode 100644 apps/web/src/middleware/auth.ts create mode 100644 apps/web/src/router.tsx create mode 100644 apps/web/src/routes/__root.tsx create mode 100644 apps/web/src/routes/api/auth/$.ts create mode 100644 apps/web/src/routes/dashboard.tsx create mode 100644 apps/web/src/routes/index.tsx create mode 100644 apps/web/src/routes/login.tsx create mode 100644 apps/web/tsconfig.json create mode 100644 apps/web/vite.config.ts create mode 100644 bts.jsonc create mode 100644 eslint.config.mjs create mode 100644 package.json create mode 100644 packages/auth/.gitignore create mode 100644 packages/auth/package.json create mode 100644 packages/auth/src/index.ts create mode 100644 packages/auth/tsconfig.json create mode 100644 packages/config/package.json create mode 100644 packages/config/tsconfig.base.json create mode 100644 packages/db/.gitignore create mode 100644 packages/db/docker-compose.yml create mode 100644 packages/db/drizzle.config.ts create mode 100644 packages/db/package.json create mode 100644 packages/db/src/index.ts create mode 100644 packages/db/src/schema/auth.ts create mode 100644 packages/db/src/schema/index.ts create mode 100644 packages/db/tsconfig.json create mode 100644 packages/env/package.json create mode 100644 packages/env/src/server.ts create mode 100644 packages/env/src/web.ts create mode 100644 packages/env/tsconfig.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 prettier.config.mjs create mode 100644 stylelint.config.mjs create mode 100644 tsconfig.json create mode 100644 turbo.json diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000..89bd250 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,126 @@ +# Ultracite Code Standards + +This project uses **Ultracite**, a zero-config preset that enforces strict code quality standards through automated formatting and linting. + +## Quick Reference + +- **Format code**: `pnpm dlx ultracite fix` +- **Check for issues**: `pnpm dlx ultracite check` +- **Diagnose setup**: `pnpm dlx ultracite doctor` + +ESLint + Prettier + Stylelint (the underlying engine) provides robust linting and formatting. Most issues are automatically fixable. + +--- + +## Core Principles + +Write code that is **accessible, performant, type-safe, and maintainable**. Focus on clarity and explicit intent over brevity. + +### Type Safety & Explicitness + +- Use explicit types for function parameters and return values when they enhance clarity +- Prefer `unknown` over `any` when the type is genuinely unknown +- Use const assertions (`as const`) for immutable values and literal types +- Leverage TypeScript's type narrowing instead of type assertions +- Use meaningful variable names instead of magic numbers - extract constants with descriptive names + +### Modern JavaScript/TypeScript + +- Use arrow functions for callbacks and short functions +- Prefer `for...of` loops over `.forEach()` and indexed `for` loops +- Use optional chaining (`?.`) and nullish coalescing (`??`) for safer property access +- Prefer template literals over string concatenation +- Use destructuring for object and array assignments +- Use `const` by default, `let` only when reassignment is needed, never `var` + +### Async & Promises + +- Always `await` promises in async functions - don't forget to use the return value +- Use `async/await` syntax instead of promise chains for better readability +- Handle errors appropriately in async code with try-catch blocks +- Don't use async functions as Promise executors + +### React & JSX + +- Use function components over class components +- Call hooks at the top level only, never conditionally +- Specify all dependencies in hook dependency arrays correctly +- Use the `key` prop for elements in iterables (prefer unique IDs over array indices) +- Nest children between opening and closing tags instead of passing as props +- Don't define components inside other components +- Use semantic HTML and ARIA attributes for accessibility: + - Provide meaningful alt text for images + - Use proper heading hierarchy + - Add labels for form inputs + - Include keyboard event handlers alongside mouse events + - Use semantic elements (` + )} + + + +
+ +
+ + ); +} diff --git a/apps/web/src/components/sign-up-form.tsx b/apps/web/src/components/sign-up-form.tsx new file mode 100644 index 0000000..60d8958 --- /dev/null +++ b/apps/web/src/components/sign-up-form.tsx @@ -0,0 +1,162 @@ +import { useForm } from "@tanstack/react-form"; +import { useNavigate } from "@tanstack/react-router"; +import { toast } from "sonner"; +import z from "zod"; + +import { authClient } from "@/lib/auth-client"; + +import Loader from "./loader"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { Label } from "./ui/label"; + +export default function SignUpForm({ onSwitchToSignIn }: { onSwitchToSignIn: () => void }) { + const navigate = useNavigate({ + from: "/", + }); + const { isPending } = authClient.useSession(); + + const form = useForm({ + defaultValues: { + email: "", + password: "", + name: "", + }, + onSubmit: async ({ value }) => { + await authClient.signUp.email( + { + email: value.email, + password: value.password, + name: value.name, + }, + { + onSuccess: () => { + navigate({ + to: "/dashboard", + }); + toast.success("Sign up successful"); + }, + onError: (error) => { + toast.error(error.error.message || error.error.statusText); + }, + }, + ); + }, + validators: { + onSubmit: z.object({ + name: z.string().min(2, "Name must be at least 2 characters"), + email: z.email("Invalid email address"), + password: z.string().min(8, "Password must be at least 8 characters"), + }), + }, + }); + + if (isPending) { + return ; + } + + return ( +
+

Create Account

+ +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + className="space-y-4" + > +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
+ +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
+ +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
+ + + {(state) => ( + + )} + +
+ +
+ +
+
+ ); +} diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx new file mode 100644 index 0000000..72db4d7 --- /dev/null +++ b/apps/web/src/components/ui/button.tsx @@ -0,0 +1,58 @@ +import type { VariantProps } from "class-variance-authority"; + +import { Button as ButtonPrimitive } from "@base-ui/react/button"; +import { cva } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-none border border-transparent bg-clip-padding text-xs font-medium focus-visible:ring-1 aria-invalid:ring-1 [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", + outline: + "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", + ghost: + "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground", + destructive: + "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: + "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + xs: "h-6 gap-1 rounded-none px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-7 gap-1 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", + lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3", + icon: "size-8", + "icon-xs": "size-6 rounded-none [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-7 rounded-none", + "icon-lg": "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +function Button({ + className, + variant = "default", + size = "default", + ...props +}: ButtonPrimitive.Props & VariantProps) { + return ( + + ); +} + +export { Button, buttonVariants }; diff --git a/apps/web/src/components/ui/card.tsx b/apps/web/src/components/ui/card.tsx new file mode 100644 index 0000000..cc517c4 --- /dev/null +++ b/apps/web/src/components/ui/card.tsx @@ -0,0 +1,89 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Card({ + className, + size = "default", + ...props +}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) { + return ( +
img:first-child]:pt-0 data-[size=sm]:gap-2 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-none *:[img:last-child]:rounded-none group/card flex flex-col", + className, + )} + {...props} + /> + ); +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }; diff --git a/apps/web/src/components/ui/checkbox.tsx b/apps/web/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..af3f70a --- /dev/null +++ b/apps/web/src/components/ui/checkbox.tsx @@ -0,0 +1,26 @@ +import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"; +import { CheckIcon } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) { + return ( + + + + + + ); +} + +export { Checkbox }; diff --git a/apps/web/src/components/ui/dropdown-menu.tsx b/apps/web/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..234a85d --- /dev/null +++ b/apps/web/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,241 @@ +import { Menu as MenuPrimitive } from "@base-ui/react/menu"; +import { CheckIcon, ChevronRightIcon } from "lucide-react"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) { + return ; +} + +function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) { + return ; +} + +function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) { + return ; +} + +function DropdownMenuContent({ + align = "start", + alignOffset = 0, + side = "bottom", + sideOffset = 4, + className, + ...props +}: MenuPrimitive.Popup.Props & + Pick) { + return ( + + + + + + ); +} + +function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) { + return ; +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: MenuPrimitive.GroupLabel.Props & { + inset?: boolean; +}) { + return ( + + ); +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: MenuPrimitive.Item.Props & { + inset?: boolean; + variant?: "default" | "destructive"; +}) { + return ( + + ); +} + +function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) { + return ; +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: MenuPrimitive.SubmenuTrigger.Props & { + inset?: boolean; +}) { + return ( + + {children} + + + ); +} + +function DropdownMenuSubContent({ + align = "start", + alignOffset = -3, + side = "right", + sideOffset = 0, + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: MenuPrimitive.CheckboxItem.Props) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) { + return ; +} + +function DropdownMenuRadioItem({ className, children, ...props }: MenuPrimitive.RadioItem.Props) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuSeparator({ className, ...props }: MenuPrimitive.Separator.Props) { + return ( + + ); +} + +function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) { + return ( + + ); +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +}; diff --git a/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx new file mode 100644 index 0000000..3de26af --- /dev/null +++ b/apps/web/src/components/ui/input.tsx @@ -0,0 +1,20 @@ +import { Input as InputPrimitive } from "@base-ui/react/input"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ); +} + +export { Input }; diff --git a/apps/web/src/components/ui/label.tsx b/apps/web/src/components/ui/label.tsx new file mode 100644 index 0000000..7ee9a68 --- /dev/null +++ b/apps/web/src/components/ui/label.tsx @@ -0,0 +1,20 @@ +"use client"; + +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Label({ className, ...props }: React.ComponentProps<"label">) { + return ( +