diff --git a/.cursorrules b/.cursorrules deleted file mode 100644 index 4a4df7a..0000000 --- a/.cursorrules +++ /dev/null @@ -1,22 +0,0 @@ -We use Sentry for watching for errors in our deployed application, as well as for instrumentation of our application. - -## Error collection - -Error collection is automatic and configured in `src/router.tsx`. - -## Instrumentation - -We want our server functions instrumented. So if you see a function name like `createServerFn`, you can instrument it with Sentry. You'll need to import `Sentry`: - -```tsx -import * as Sentry from '@sentry/tanstackstart-react' -``` - -And then wrap the implementation of the server function with `Sentry.startSpan`, like so: - -```tsx -Sentry.startSpan({ name: 'Requesting all the pokemon' }, async () => { - // Some lengthy operation here - await fetch('https://api.pokemon.com/data/') -}) -``` \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d1bbe19 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,51 @@ +# CLAUDE.md +Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed. +Tradeoff: These guidelines bias toward caution over speed. For trivial tasks, use judgment. +1. Think Before Coding +Don't assume. Don't hide confusion. Surface tradeoffs. +Before implementing: + +* State your assumptions explicitly. If uncertain, ask. +* If multiple interpretations exist, present them - don't pick silently. +* If a simpler approach exists, say so. Push back when warranted. +* If something is unclear, stop. Name what's confusing. Ask. +2. Simplicity First +Minimum code that solves the problem. Nothing speculative. + +* No features beyond what was asked. +* No abstractions for single-use code. +* No "flexibility" or "configurability" that wasn't requested. +* No error handling for impossible scenarios. +* If you write 200 lines and it could be 50, rewrite it. +Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. +3. Surgical Changes +Touch only what you must. Clean up only your own mess. +When editing existing code: + +* Don't "improve" adjacent code, comments, or formatting. +* Don't refactor things that aren't broken. +* Match existing style, even if you'd do it differently. +* If you notice unrelated dead code, mention it - don't delete it. +When your changes create orphans: + +* Remove imports/variables/functions that YOUR changes made unused. +* Don't remove pre-existing dead code unless asked. +The test: Every changed line should trace directly to the user's request. +4. Goal-Driven Execution +Define success criteria. Loop until verified. +Transform tasks into verifiable goals: + +* "Add validation" → "Write tests for invalid inputs, then make them pass" +* "Fix the bug" → "Write a test that reproduces it, then make it pass" +* "Refactor X" → "Ensure tests pass before and after" +For multi-step tasks, state a brief plan: + +``` +1. [Step] → verify: [check] +2. [Step] → verify: [check] +3. [Step] → verify: [check] + +``` + +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. +These guidelines are working if: fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes. diff --git a/README.md b/README.md deleted file mode 100644 index b22c91e..0000000 --- a/README.md +++ /dev/null @@ -1,222 +0,0 @@ -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` - -## Linting & Formatting - -This project uses [Biome](https://biomejs.dev/) for linting and formatting. The following scripts are available: - - -```bash -npm run lint -npm run format -npm run check -``` - - -## Shadcn - -Add components using the latest version of [Shadcn](https://ui.shadcn.com/). - -```bash -pnpm dlx shadcn@latest add button -``` - - -# Paraglide i18n - -This add-on wires up ParaglideJS for localized routing and message formatting. - -- Messages live in `project.inlang/messages`. -- URLs are localized through the Paraglide Vite plugin and router `rewrite` hooks. -- Run the dev server or build to regenerate the `src/paraglide` outputs. - - - -## 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/package.json b/package.json index f004ea6..f6954a3 100644 --- a/package.json +++ b/package.json @@ -14,45 +14,45 @@ "machine-translate": "inlang machine translate --project project.inlang" }, "dependencies": { - "@heroui/react": "^3.0.0-rc.1", - "@heroui/styles": "^3.0.0-rc.1", + "@heroui/react": "^3.0.5", + "@heroui/styles": "^3.0.5", "@sentry/tanstackstart-react": "^10.45.0", - "@supabase/ssr": "^0.9.0", - "@supabase/supabase-js": "^2.99.3", - "@tailwindcss/vite": "^4.2.2", - "@tanstack/react-query": "^5.91.2", - "@tanstack/react-router": "^1.167.5", - "@tanstack/react-router-ssr-query": "^1.166.9", - "@tanstack/react-start": "^1.166.17", - "@tanstack/router-plugin": "^1.166.14", + "@supabase/ssr": "^0.10.3", + "@supabase/supabase-js": "^2.106.1", + "@tailwindcss/vite": "^4.3.0", + "@tanstack/react-query": "^5.100.11", + "@tanstack/react-router": "^1.170.7", + "@tanstack/react-router-ssr-query": "^1.167.0", + "@tanstack/react-start": "^1.168.10", + "@tanstack/router-plugin": "^1.168.10", "clsx": "^2.1.1", - "lucide-react": "^0.577.0", - "maplibre-gl": "^5.20.2", - "nitro": "^3.0.1-alpha.2", - "react": "^19.2.4", - "react-dom": "^19.2.4", - "tailwind-merge": "^3.5.0", - "tailwindcss": "^4.2.2", + "lucide-react": "^1.16.0", + "maplibre-gl": "^5.24.0", + "nitro": "^3.0.260522-beta", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "tailwind-merge": "^3.6.0", + "tailwindcss": "^4.3.0", "tw-animate-css": "^1.4.0", - "zod": "^4.3.6" + "zod": "^4.4.3" }, "devDependencies": { - "@biomejs/biome": "^2.4.8", - "@inlang/paraglide-js": "^2.15.0", - "@tanstack/devtools-vite": "^0.6.0", - "@tanstack/react-devtools": "^0.10.0", - "@tanstack/react-query-devtools": "^5.91.3", - "@tanstack/react-router-devtools": "^1.166.9", + "@biomejs/biome": "^2.4.15", + "@inlang/paraglide-js": "^2.18.1", + "@tanstack/devtools-vite": "^0.7.0", + "@tanstack/react-devtools": "^0.10.5", + "@tanstack/react-query-devtools": "^5.100.11", + "@tanstack/react-router-devtools": "^1.167.0", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.2", - "@types/node": "^22.19.15", - "@types/react": "^19.2.14", + "@types/node": "^22.19.19", + "@types/react": "^19.2.15", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.1", - "jsdom": "^29.0.0", - "typescript": "^5.9.3", - "vite": "^8.0.1", + "@vitejs/plugin-react": "^6.0.2", + "jsdom": "^29.1.1", + "typescript": "^6.0.3", + "vite": "^8.0.14", "vite-tsconfig-paths": "^6.1.1", - "vitest": "^4.1.0" + "vitest": "^4.1.7" } } diff --git a/src/integrations/supabase/supabase.ts b/src/integrations/supabase/supabase.ts index 71fb195..dc1d494 100644 --- a/src/integrations/supabase/supabase.ts +++ b/src/integrations/supabase/supabase.ts @@ -19,8 +19,8 @@ export function getSupabaseServerClient() { })) }, setAll(cookies) { - cookies.forEach((cookie) => { - setCookie(cookie.name, cookie.value) + cookies.forEach(({ name, value, options }) => { + setCookie(name, value, options) }) } } diff --git a/src/lib/server/middleware.ts b/src/lib/server/middleware.ts new file mode 100644 index 0000000..dd6ea8c --- /dev/null +++ b/src/lib/server/middleware.ts @@ -0,0 +1,45 @@ +import { createMiddleware } from "@tanstack/react-start" +import { getSupabaseServerClient } from "@/integrations/supabase/supabase" + +export const authMiddleware = createMiddleware().server(async ({ next }) => { + const supabase = getSupabaseServerClient() + const { data, error } = await supabase.auth.getUser() + + if (error || !data.user) { + throw new Error("Unauthorized") + } + + return next({ + context: { + user: { + id: data.user.id, + email: data.user.email, + name: data.user.user_metadata?.name || "", + location: data.user.user_metadata?.location || "" + } + } + }) +}) + +export const optionalAuthMiddleware = createMiddleware().server( + async ({ next }) => { + const supabase = getSupabaseServerClient() + const { data, error } = await supabase.auth.getUser() + + const userData = + !error && data.user + ? { + id: data.user.id, + email: data.user.email, + name: data.user.user_metadata?.name || "", + location: data.user.user_metadata?.location || "" + } + : null + + return next({ + context: { + user: userData + } + }) + } +) diff --git a/src/lib/server/user.ts b/src/lib/server/user.ts index 5f4cbfb..faa69e8 100644 --- a/src/lib/server/user.ts +++ b/src/lib/server/user.ts @@ -4,8 +4,9 @@ import { getSupabaseServerClient } from "@/integrations/supabase/supabase" import { loginFormSchema, signupFormSchema, - userListParamsSchema, -} from "../validation/user" + userListParamsSchema +} from "@/lib/validation/user" +import { authMiddleware, optionalAuthMiddleware } from "./middleware" const login = createServerFn({ method: "POST" }) .inputValidator(loginFormSchema) @@ -14,38 +15,41 @@ const login = createServerFn({ method: "POST" }) const login = await supabase.auth.signInWithPassword({ email: data.email, - password: data.password, + password: data.password }) if (login.error) { return { error: true, - message: login.error.message, + message: login.error.message } } - return { - error: false, - } + throw redirect({ + to: "/dashboard", + replace: true + }) }) -const logout = createServerFn().handler(async () => { - const supabase = getSupabaseServerClient() +const logout = createServerFn() + .middleware([authMiddleware]) + .handler(async () => { + const supabase = getSupabaseServerClient() - const { error } = await supabase.auth.signOut() - if (error) { - return { - error: true, - message: error.message, + const { error } = await supabase.auth.signOut() + if (error) { + return { + error: true, + message: error.message + } } - } - throw redirect({ - to: "/", - viewTransition: true, - replace: true, + throw redirect({ + to: "/", + viewTransition: true, + replace: true + }) }) -}) const signup = createServerFn({ method: "POST" }) .inputValidator(signupFormSchema) @@ -57,41 +61,36 @@ const signup = createServerFn({ method: "POST" }) options: { data: { name: data.name, - location: data.location, - }, - }, + location: data.location + } + } }) if (error) { return { error: true, - message: error.message, + message: error.message } } throw redirect({ - href: data.redirectUrl || "/", + href: data.redirectUrl || "/" }) }) -const userData = createServerFn().handler(async () => { - const supabase = getSupabaseServerClient() - const { data, error } = await supabase.auth.getUser() - if (error || !data.user) { - return { - error: true, - message: error?.message ?? "Unknown error", +const userData = createServerFn() + .middleware([optionalAuthMiddleware]) + .handler(async ({ context }) => { + if (!context.user) { + return { + error: true, + message: "Not authenticated" + } } - } - return { - user: { - id: data.user.id, - email: data.user.email, - name: data.user.user_metadata.name || "", - location: data.user.user_metadata.location || "", - }, - error: false, - } -}) + return { + user: context.user, + error: false + } + }) const resendConfirmationEmail = createServerFn({ method: "POST" }) .inputValidator(signupFormSchema.pick({ email: true })) @@ -102,33 +101,34 @@ const resendConfirmationEmail = createServerFn({ method: "POST" }) if (error) { return { error: true, - message: error.message, + message: error.message } } return { - error: false, + error: false } }) const userList = createServerFn() + .middleware([authMiddleware]) .inputValidator(userListParamsSchema) .handler(async ({ data }) => { const supabase = getSupabaseServerClient() const users = await supabase.auth.admin.listUsers({ page: data.page, - perPage: data.limit, + perPage: data.limit }) if (users.error) { return { error: true, - message: users.error.message, + message: users.error.message } } return { users: users.data, - error: false, + error: false } }) @@ -138,5 +138,5 @@ export const user = { signup, userData, resendConfirmationEmail, - userList, + userList } diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 499577b..e6358b3 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -12,7 +12,7 @@ import appCss from "@/styles/globals.css?url" interface MyRouterContext { queryClient: QueryClient - user: null + user: Awaited> } export const Route = createRootRouteWithContext()({ diff --git a/src/routes/_auth.tsx b/src/routes/_auth.tsx index dc32c63..7aebd77 100644 --- a/src/routes/_auth.tsx +++ b/src/routes/_auth.tsx @@ -1,9 +1,13 @@ -import { createFileRoute, Link, Outlet } from "@tanstack/react-router" +import { createFileRoute, Link, Outlet, redirect } from "@tanstack/react-router" export const Route = createFileRoute("/_auth")({ beforeLoad: ({ context }) => { if (context.user.error) { - throw new Error("Not authenticated") + redirect({ + to: "/access/login", + replace: true, + throw: true + }) } }, errorComponent: ({ error }) => { diff --git a/tsconfig.json b/tsconfig.json index ae8427a..1b1e835 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,6 @@ "target": "ES2022", "jsx": "react-jsx", "module": "ESNext", - "baseUrl": ".", "paths": { "@/*": ["./src/*"] }, diff --git a/vite.config.ts b/vite.config.ts index 0691269..f43c40e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,20 +5,21 @@ import { tanstackStart } from "@tanstack/react-start/plugin/vite" import viteReact from "@vitejs/plugin-react" import { nitro } from "nitro/vite" import { defineConfig } from "vite" -import tsconfigPaths from "vite-tsconfig-paths" export default defineConfig({ plugins: [ devtools(), paraglideVitePlugin({ project: "./project.inlang", - outdir: "./src/paraglide", + outdir: "./src/integrations/paraglide", strategy: ["url", "baseLocale"] }), nitro({ rollupConfig: { external: [/^@sentry\//] } }), - tsconfigPaths({ projects: ["./tsconfig.json"] }), tailwindcss(), tanstackStart(), viteReact() - ] + ], + resolve: { + tsconfigPaths: true + } })