-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Example: Add example with Appwrite Auth #7225
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "image": "mcr.microsoft.com/devcontainers/typescript-node:24" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1 | ||
| APPWRITE_PROJECT_ID=PleaseChangeMe | ||
| APPWRITE_API_KEY=PleaseChangeMe |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| node_modules | ||
| # Keep environment variables out of version control | ||
| !.env | ||
| package-lock.json | ||
| yarn.lock | ||
|
|
||
| .DS_Store | ||
| .cache | ||
| .env | ||
| .vercel | ||
| .output | ||
| /build/ | ||
| /api/ | ||
| /server/build | ||
| /public/build# Sentry Config File | ||
| .env.sentry-build-plugin | ||
| /test-results/ | ||
| /playwright-report/ | ||
| /blob-report/ | ||
| /playwright/.cache/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| **/build | ||
| **/public | ||
| pnpm-lock.yaml | ||
| routeTree.gen.ts |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "files.watcherExclude": { | ||
| "**/routeTree.gen.ts": true | ||
| }, | ||
| "search.exclude": { | ||
| "**/routeTree.gen.ts": true | ||
| }, | ||
| "files.readonlyInclude": { | ||
| "**/routeTree.gen.ts": true | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| # TanStack Start - Appwrite Example | ||
|
|
||
| A TanStack Start example demonstrating integration with [Appwrite](https://appwrite.io) for authentication. | ||
|
|
||
| - [TanStack Router Docs](https://tanstack.com/router) | ||
| - [Appwrite Documentation](https://appwrite.io/docs) | ||
|
|
||
| ## Start a new project based on this example | ||
|
|
||
| To start a new project based on this example, run: | ||
|
|
||
| ```sh | ||
| npx gitpick TanStack/router/tree/main/examples/react/start-appwrite-basic start-appwrite-basic | ||
| ``` | ||
|
|
||
| ## Setup | ||
|
|
||
| This example uses Appwrite's [Server-Side Rendering flow](https://appwrite.io/docs/products/auth/server-side-rendering) | ||
| with the [`node-appwrite`](https://www.npmjs.com/package/node-appwrite) SDK. The `.env` file | ||
| contains the environment variables the server needs: | ||
|
|
||
| ```env | ||
| APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1 | ||
| APPWRITE_PROJECT_ID=your-project-id | ||
| APPWRITE_API_KEY=your-server-api-key | ||
| ``` | ||
|
|
||
| You'll need to: | ||
|
|
||
| 1. Create an Appwrite project at [cloud.appwrite.io](https://cloud.appwrite.io) (or self-host) | ||
| 2. Enable the **Email/Password** authentication method in **Auth → Settings** | ||
| 3. Create a server **API Key** with at least the `sessions.write` and `users.write` scopes | ||
| 4. Copy the project ID and API key into the `.env` file | ||
|
|
||
| ## Getting Started | ||
|
|
||
| From your terminal: | ||
|
|
||
| ```sh | ||
| pnpm install | ||
| pnpm dev | ||
| ``` | ||
|
|
||
| This starts your app in development mode, rebuilding assets on file changes. | ||
|
|
||
| ## Build | ||
|
|
||
| To build the app for production: | ||
|
|
||
| ```sh | ||
| pnpm build | ||
| ``` | ||
|
|
||
| ## Appwrite Integration | ||
|
|
||
| This example demonstrates: | ||
|
|
||
| - Email/password sign up and sign in with Appwrite's Account service | ||
| - SSR-safe auth state using a session-secret cookie read on the server | ||
| - Server functions (`createServerFn`) that use the SSR `node-appwrite` SDK |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| { | ||
| "name": "tanstack-start-example-appwrite-basic", | ||
| "private": true, | ||
| "main": "index.js", | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "vite dev", | ||
| "build": "vite build && tsc --noEmit", | ||
| "preview": "vite preview", | ||
| "start": "pnpx srvx --prod -s ../client dist/server/server.js" | ||
| }, | ||
| "keywords": [], | ||
| "author": "", | ||
| "license": "ISC", | ||
| "dependencies": { | ||
| "@tanstack/react-router": "^1.168.23", | ||
| "@tanstack/react-router-devtools": "^1.166.13", | ||
| "@tanstack/react-start": "^1.167.42", | ||
| "node-appwrite": "^24.0.0", | ||
| "react": "^19.0.0", | ||
| "react-dom": "^19.0.0", | ||
| "redaxios": "^0.5.1" | ||
| }, | ||
| "devDependencies": { | ||
| "@tailwindcss/vite": "^4.2.2", | ||
| "@types/react": "^19.0.8", | ||
| "@types/react-dom": "^19.0.3", | ||
| "@vitejs/plugin-react": "^6.0.1", | ||
| "tailwindcss": "^4.2.2", | ||
| "typescript": "^6.0.2", | ||
| "vite": "^8.0.0" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,57 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function Auth({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| actionText, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onSubmit, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| afterSubmit, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| actionText: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onSubmit: (e: React.FormEvent<HTMLFormElement>) => void | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: 'pending' | 'idle' | 'success' | 'error' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| afterSubmit?: React.ReactNode | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="fixed inset-0 bg-white dark:bg-black flex items-start justify-center p-8"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="bg-white dark:bg-gray-900 p-8 rounded-lg shadow-lg"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h1 className="text-2xl font-bold mb-4">{actionText}</h1> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <form | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onSubmit={(e) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| e.preventDefault() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onSubmit(e) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="space-y-4" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <label htmlFor="email" className="block text-xs"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Username | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </label> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="email" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name="email" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id="email" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="px-2 py-1 w-full rounded-sm border border-gray-500/20 bg-white dark:bg-gray-800" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <label htmlFor="password" className="block text-xs"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Password | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </label> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="password" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name="password" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id="password" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="px-2 py-1 w-full rounded-sm border border-gray-500/20 bg-white dark:bg-gray-800" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+24
to
+42
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make the auth fields match email/password semantics. Line 25 labels an email field as “Username”, and both fields allow empty submission. This makes the example easier to misuse and pushes avoidable validation errors to the server. Suggested form field cleanup <label htmlFor="email" className="block text-xs">
- Username
+ Email
</label>
<input
type="email"
name="email"
id="email"
+ required
+ autoComplete="email"
className="px-2 py-1 w-full rounded-sm border border-gray-500/20 bg-white dark:bg-gray-800"
/>
@@
<input
type="password"
name="password"
id="password"
+ required
className="px-2 py-1 w-full rounded-sm border border-gray-500/20 bg-white dark:bg-gray-800"
/>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inherited verbatim from
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Would you like me to open a GitHub issue to track the form-field improvements ( ʕ •ᴥ•ʔ ✏️ Learnings added
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="submit" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="w-full bg-cyan-600 text-white rounded-sm py-2 font-black uppercase" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| disabled={status === 'pending'} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {status === 'pending' ? '...' : actionText} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {afterSubmit ? afterSubmit : null} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </form> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import * as React from 'react' | ||
| import { | ||
| ErrorComponent, | ||
| Link, | ||
| rootRouteId, | ||
| useMatch, | ||
| useRouter, | ||
| } from '@tanstack/react-router' | ||
| import type { ErrorComponentProps } from '@tanstack/react-router' | ||
|
|
||
| export function DefaultCatchBoundary({ error }: ErrorComponentProps) { | ||
| const router = useRouter() | ||
| const isRoot = useMatch({ | ||
| strict: false, | ||
| select: (state) => state.id === rootRouteId, | ||
| }) | ||
|
|
||
| console.error(error) | ||
|
|
||
| return ( | ||
| <div className="min-w-0 flex-1 p-4 flex flex-col items-center justify-center gap-6"> | ||
| <ErrorComponent error={error} /> | ||
| <div className="flex gap-2 items-center flex-wrap"> | ||
| <button | ||
| onClick={() => { | ||
| router.invalidate() | ||
| }} | ||
| className={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded-sm text-white uppercase font-extrabold`} | ||
| > | ||
| Try Again | ||
| </button> | ||
| {isRoot ? ( | ||
| <Link | ||
| to="/" | ||
| className={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded-sm text-white uppercase font-extrabold`} | ||
| > | ||
| Home | ||
| </Link> | ||
| ) : ( | ||
| <Link | ||
| to="/" | ||
| className={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded-sm text-white uppercase font-extrabold`} | ||
| onClick={(e) => { | ||
| e.preventDefault() | ||
| window.history.back() | ||
| }} | ||
| > | ||
| Go Back | ||
| </Link> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,72 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useRouter } from '@tanstack/react-router' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useServerFn } from '@tanstack/react-start' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useMutation } from '../hooks/useMutation' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { loginFn } from '../routes/_authed' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { signupFn } from '../routes/signup' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Auth } from './Auth' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function Login() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const router = useRouter() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const loginMutation = useMutation({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn: loginFn, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onSuccess: async (ctx) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!ctx.data?.error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await router.invalidate() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| router.navigate({ to: '/' }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const signupMutation = useMutation({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn: useServerFn(signupFn), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+11
to
+24
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: In TanStack Start, use useServerFn when calling server functions (created with createServerFn) from client components. This hook wraps the server function in a useCallback that handles server responses properly on the client, including automatic handling of errors, redirects (via throw redirect), and other server-specific responses that would not work correctly if calling the raw server function directly from client code. Example usage in a client component with TanStack Query: function PostList { const getPosts = useServerFn(getServerPosts) const { data } = useQuery({ queryKey: ['posts'], queryFn: => getPosts, }) // ... } Or with mutations: const addPlaylistFn = useMutation({ mutationFn: useServerFn(addPlaylist), }) Without useServerFn, redirects and certain error responses from server functions fail silently or incorrectly on the client. Server functions can be called directly from server contexts like route loaders without this hook. Citations:
🏁 Script executed: cat -n examples/react/start-appwrite-basic/src/components/Login.tsx | head -40Repository: TanStack/router Length of output: 1354 🏁 Script executed: find examples/react/start-appwrite-basic/src/routes -name '*authed*' -o -name 'signup*' | head -10Repository: TanStack/router Length of output: 230 🏁 Script executed: cat -n examples/react/start-appwrite-basic/src/routes/_authed.tsxRepository: TanStack/router Length of output: 1395 🏁 Script executed: cat -n examples/react/start-appwrite-basic/src/routes/signup.tsxRepository: TanStack/router Length of output: 2480 Wrap Both Fix const loginMutation = useMutation({
- fn: loginFn,
+ fn: useServerFn(loginFn),
onSuccess: async (ctx) => {
if (!ctx.data?.error) {
await router.invalidate()
router.navigate({ to: '/' })
return
}
},
})📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
✏️ Learnings added
🧠 Learnings used |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Auth | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| actionText="Login" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status={loginMutation.status} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onSubmit={(e) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const formData = new FormData(e.target as HTMLFormElement) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| loginMutation.mutate({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| email: formData.get('email') as string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| password: formData.get('password') as string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| afterSubmit={ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| loginMutation.data ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="text-red-400">{loginMutation.data.message}</div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {loginMutation.data.error && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /invalid credentials/i.test(loginMutation.data.message) ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="text-blue-500" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={(e) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const formData = new FormData( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (e.target as HTMLButtonElement).form!, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| signupMutation.mutate({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| email: formData.get('email') as string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| password: formData.get('password') as string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Sign up instead? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : null} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : null | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { Link } from '@tanstack/react-router' | ||
|
|
||
| export function NotFound({ children }: { children?: React.ReactNode }) { | ||
| return ( | ||
| <div className="space-y-2 p-2"> | ||
| <div className="text-gray-600 dark:text-gray-400"> | ||
| {children || <p>The page you are looking for does not exist.</p>} | ||
| </div> | ||
| <p className="flex items-center gap-2 flex-wrap"> | ||
| <button | ||
| onClick={() => window.history.back()} | ||
| className="bg-emerald-500 text-white px-2 py-1 rounded-sm uppercase font-black text-sm" | ||
| > | ||
| Go back | ||
| </button> | ||
| <Link | ||
| to="/" | ||
| className="bg-cyan-600 text-white px-2 py-1 rounded-sm uppercase font-black text-sm" | ||
| > | ||
| Start Over | ||
| </Link> | ||
| </p> | ||
| </div> | ||
| ) | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Split the
/public/buildignore rule from the Sentry comment.Line 15 currently ignores a literal
/public/build# Sentry Config Filepath, so/public/buildis not covered.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inherited verbatim from
examples/react/start-supabase-basic/.gitignore. Keeping parity with the existing example here; happy to do a follow-up PR that fixes the malformed line in both.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ChiragAgg5k, understood — keeping the two examples in sync makes sense. Would you like me to open a follow-up GitHub issue to track fixing the malformed/public/build# Sentry Config Fileline in bothexamples/react/start-supabase-basic/.gitignoreandexamples/react/start-appwrite-basic/.gitignore? That way it won't get lost.