diff --git a/apps/web/.cta.json b/apps/web/.cta.json new file mode 100644 index 00000000..dddfb016 --- /dev/null +++ b/apps/web/.cta.json @@ -0,0 +1,19 @@ +{ + "projectName": "web", + "mode": "file-router", + "typescript": true, + "packageManager": "npm", + "includeExamples": false, + "tailwind": true, + "addOnOptions": {}, + "envVarValues": {}, + "git": false, + "install": true, + "routerOnly": false, + "version": 1, + "framework": "react", + "chosenAddOns": [ + "cloudflare", + "shadcn" + ] +} \ No newline at end of file diff --git a/apps/web/.cursorrules b/apps/web/.cursorrules new file mode 100644 index 00000000..20159602 --- /dev/null +++ b/apps/web/.cursorrules @@ -0,0 +1,7 @@ +# shadcn instructions + +Use the latest version of Shadcn to install new components, like this command to add a button component: + +```bash +pnpm dlx shadcn@latest add button +``` diff --git a/apps/web/.gitignore b/apps/web/.gitignore new file mode 100644 index 00000000..8b25bb54 --- /dev/null +++ b/apps/web/.gitignore @@ -0,0 +1,13 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local +.env +.nitro +.tanstack +.wrangler +.output +.vinxi +__unconfig* +todos.json diff --git a/apps/web/.vscode/settings.json b/apps/web/.vscode/settings.json new file mode 100644 index 00000000..00b5278e --- /dev/null +++ b/apps/web/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "files.watcherExclude": { + "**/routeTree.gen.ts": true + }, + "search.exclude": { + "**/routeTree.gen.ts": true + }, + "files.readonlyInclude": { + "**/routeTree.gen.ts": true + } +} diff --git a/apps/web/README.md b/apps/web/README.md new file mode 100644 index 00000000..f6caa1e5 --- /dev/null +++ b/apps/web/README.md @@ -0,0 +1,215 @@ +Welcome to your new TanStack Start app! + +# Getting Started + +To run this application: + +```bash +npm install +npm run dev +``` + +# Building For Production + +To build this application for production: + +```bash +npm run build +``` + +## Testing + +This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with: + +```bash +npm run test +``` + +## Styling + +This project uses [Tailwind CSS](https://tailwindcss.com/) for styling. + +### Removing Tailwind CSS + +If you prefer not to use Tailwind CSS: + +1. Remove the demo pages in `src/routes/demo/` +2. Replace the Tailwind import in `src/styles.css` with your own styles +3. Remove `tailwindcss()` from the plugins array in `vite.config.ts` +4. Uninstall the packages: `npm install @tailwindcss/vite tailwindcss -D` + + +## Deploy to Cloudflare Workers + +This project uses the Cloudflare Vite plugin (configured in `vite.config.ts`) and `wrangler.jsonc`: + +1. Install Wrangler: `npm install -g wrangler` +2. Authenticate: `wrangler login` +3. Deploy: `npx wrangler deploy` + +For production env vars, run `wrangler secret put MY_VAR` for each secret listed in `.env.example`. Public (non-secret) vars go in `wrangler.jsonc` under `vars`. + +KV, D1, R2, and Durable Object bindings are configured in `wrangler.jsonc` — see https://developers.cloudflare.com/workers/wrangler/configuration/. + + +## Shadcn + +Add components using the latest version of [Shadcn](https://ui.shadcn.com/). + +```bash +pnpm dlx shadcn@latest add button +``` + + + +## Routing + +This project uses [TanStack Router](https://tanstack.com/router) with file-based routing. Routes are managed as files in `src/routes`. + +### Adding A Route + +To add a new route to your application just add a new file in the `./src/routes` directory. + +TanStack will automatically generate the content of the route file for you. + +Now that you have two routes you can use a `Link` component to navigate between them. + +### Adding Links + +To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`. + +```tsx +import { Link } from "@tanstack/react-router"; +``` + +Then anywhere in your JSX you can use it like so: + +```tsx +About +``` + +This will create a link that will navigate to the `/about` route. + +More information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/react/api/router/linkComponent). + +### Using A Layout + +In the File Based Routing setup the layout is located in `src/routes/__root.tsx`. Anything you add to the root route will appear in all the routes. The route content will appear in the JSX where you render `{children}` in the `shellComponent`. + +Here is an example layout that includes a header: + +```tsx +import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { title: 'My App' }, + ], + }), + shellComponent: ({ children }) => ( + + + + + +
+ +
+ {children} + + + + ), +}) +``` + +More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts). + +## Server Functions + +TanStack Start provides server functions that allow you to write server-side code that seamlessly integrates with your client components. + +```tsx +import { createServerFn } from '@tanstack/react-start' + +const getServerTime = createServerFn({ + method: 'GET', +}).handler(async () => { + return new Date().toISOString() +}) + +// Use in a component +function MyComponent() { + const [time, setTime] = useState('') + + useEffect(() => { + getServerTime().then(setTime) + }, []) + + return
Server time: {time}
+} +``` + +## API Routes + +You can create API routes by using the `server` property in your route definitions: + +```tsx +import { createFileRoute } from '@tanstack/react-router' +import { json } from '@tanstack/react-start' + +export const Route = createFileRoute('/api/hello')({ + server: { + handlers: { + GET: () => json({ message: 'Hello, World!' }), + }, + }, +}) +``` + +## Data Fetching + +There are multiple ways to fetch data in your application. You can use TanStack Query to fetch data from a server. But you can also use the `loader` functionality built into TanStack Router to load the data for a route before it's rendered. + +For example: + +```tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/people')({ + loader: async () => { + const response = await fetch('https://swapi.dev/api/people') + return response.json() + }, + component: PeopleComponent, +}) + +function PeopleComponent() { + const data = Route.useLoaderData() + return ( + + ) +} +``` + +Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#loader-parameters). + +# Demo files + +Files prefixed with `demo` can be safely deleted. They are there to provide a starting point for you to play around with the features you've installed. + +# Learn More + +You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com). + +For TanStack Start specific documentation, visit [TanStack Start](https://tanstack.com/start). diff --git a/apps/web/components.json b/apps/web/components.json new file mode 100644 index 00000000..cdcadc69 --- /dev/null +++ b/apps/web/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "#/components", + "utils": "#/lib/utils", + "ui": "#/components/ui", + "lib": "#/lib", + "hooks": "#/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 00000000..785ec2ce --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,69 @@ +{ + "name": "@opencut/web", + "private": true, + "type": "module", + "imports": { + "#/*": "./src/*" + }, + "scripts": { + "dev": "vite dev --port 5173", + "build": "vite build", + "preview": "vite preview", + "test": "vitest run", + "deploy": "bun run build && wrangler deploy" + }, + "dependencies": { + "@base-ui/react": "^1.4.1", + "@cloudflare/vite-plugin": "^1.26.0", + "@hookform/resolvers": "^5.2.2", + "@tailwindcss/vite": "^4.1.18", + "@tanstack/react-devtools": "latest", + "@tanstack/react-router": "latest", + "@tanstack/react-router-devtools": "latest", + "@tanstack/react-router-ssr-query": "latest", + "@tanstack/react-start": "latest", + "@tanstack/router-plugin": "^1.132.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^4.1.0", + "embla-carousel-react": "^8.6.0", + "input-otp": "^1.4.2", + "lucide-react": "^1.14.0", + "next-themes": "^0.4.6", + "radix-ui": "^1.4.3", + "react": "^19.2.0", + "react-day-picker": "^10.0.0", + "react-dom": "^19.2.0", + "react-hook-form": "^7.75.0", + "react-resizable-panels": "^4", + "recharts": "3.8.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.0.2", + "tailwindcss": "^4.1.18", + "tw-animate-css": "^1.3.6", + "vaul": "^1.1.2", + "zod": "^4.4.3" + }, + "devDependencies": { + "@tailwindcss/typography": "^0.5.16", + "@tanstack/devtools-vite": "latest", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.0", + "@types/node": "^22.10.2", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^6.0.1", + "jsdom": "^28.1.0", + "typescript": "^6.0.2", + "vite": "^8.0.0", + "vitest": "^4.1.5", + "wrangler": "^4.70.0" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "esbuild", + "lightningcss" + ] + } +} \ No newline at end of file diff --git a/apps/web/public/favicon.ico b/apps/web/public/favicon.ico new file mode 100644 index 00000000..a11777cc Binary files /dev/null and b/apps/web/public/favicon.ico differ diff --git a/apps/web/public/logo192.png b/apps/web/public/logo192.png new file mode 100644 index 00000000..fc44b0a3 Binary files /dev/null and b/apps/web/public/logo192.png differ diff --git a/apps/web/public/logo512.png b/apps/web/public/logo512.png new file mode 100644 index 00000000..a4e47a65 Binary files /dev/null and b/apps/web/public/logo512.png differ diff --git a/apps/web/public/manifest.json b/apps/web/public/manifest.json new file mode 100644 index 00000000..078ef501 --- /dev/null +++ b/apps/web/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "TanStack App", + "name": "Create TanStack App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/apps/web/public/robots.txt b/apps/web/public/robots.txt new file mode 100644 index 00000000..e9e57dc4 --- /dev/null +++ b/apps/web/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/apps/web/src/components/ui/accordion.tsx b/apps/web/src/components/ui/accordion.tsx new file mode 100644 index 00000000..b16de2af --- /dev/null +++ b/apps/web/src/components/ui/accordion.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import { ChevronDownIcon } from "lucide-react" +import { Accordion as AccordionPrimitive } from "radix-ui" + +import { cn } from "#/lib/utils.ts" + +function Accordion({ + ...props +}: React.ComponentProps) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/apps/web/src/components/ui/alert-dialog.tsx b/apps/web/src/components/ui/alert-dialog.tsx new file mode 100644 index 00000000..c113a0e5 --- /dev/null +++ b/apps/web/src/components/ui/alert-dialog.tsx @@ -0,0 +1,196 @@ +"use client" + +import * as React from "react" +import { AlertDialog as AlertDialogPrimitive } from "radix-ui" + +import { cn } from "#/lib/utils.ts" +import { Button } from "#/components/ui/button.tsx" + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "default" | "sm" +}) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogMedia({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogAction({ + className, + variant = "default", + size = "default", + ...props +}: React.ComponentProps & + Pick, "variant" | "size">) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + variant = "outline", + size = "default", + ...props +}: React.ComponentProps & + Pick, "variant" | "size">) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogMedia, + AlertDialogOverlay, + AlertDialogPortal, + AlertDialogTitle, + AlertDialogTrigger, +} diff --git a/apps/web/src/components/ui/alert.tsx b/apps/web/src/components/ui/alert.tsx new file mode 100644 index 00000000..c485b70f --- /dev/null +++ b/apps/web/src/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "#/lib/utils.ts" + +const alertVariants = cva( + "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/apps/web/src/components/ui/aspect-ratio.tsx b/apps/web/src/components/ui/aspect-ratio.tsx new file mode 100644 index 00000000..57e38fa9 --- /dev/null +++ b/apps/web/src/components/ui/aspect-ratio.tsx @@ -0,0 +1,11 @@ +"use client" + +import { AspectRatio as AspectRatioPrimitive } from "radix-ui" + +function AspectRatio({ + ...props +}: React.ComponentProps) { + return +} + +export { AspectRatio } diff --git a/apps/web/src/components/ui/avatar.tsx b/apps/web/src/components/ui/avatar.tsx new file mode 100644 index 00000000..7d50ceaa --- /dev/null +++ b/apps/web/src/components/ui/avatar.tsx @@ -0,0 +1,107 @@ +import * as React from "react" +import { Avatar as AvatarPrimitive } from "radix-ui" + +import { cn } from "#/lib/utils.ts" + +function Avatar({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "default" | "sm" | "lg" +}) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) { + return ( + svg]:hidden", + "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", + "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", + className + )} + {...props} + /> + ) +} + +function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AvatarGroupCount({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3", + className + )} + {...props} + /> + ) +} + +export { + Avatar, + AvatarImage, + AvatarFallback, + AvatarBadge, + AvatarGroup, + AvatarGroupCount, +} diff --git a/apps/web/src/components/ui/badge.tsx b/apps/web/src/components/ui/badge.tsx new file mode 100644 index 00000000..cd79adb6 --- /dev/null +++ b/apps/web/src/components/ui/badge.tsx @@ -0,0 +1,48 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "#/lib/utils.ts" + +const badgeVariants = cva( + "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90", + outline: + "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + link: "text-primary underline-offset-4 [a&]:hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant = "default", + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot.Root : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/apps/web/src/components/ui/breadcrumb.tsx b/apps/web/src/components/ui/breadcrumb.tsx new file mode 100644 index 00000000..18d5124b --- /dev/null +++ b/apps/web/src/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import * as React from "react" +import { ChevronRight, MoreHorizontal } from "lucide-react" +import { Slot } from "radix-ui" + +import { cn } from "#/lib/utils.ts" + +function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { + return