Prettier prettied
This commit is contained in:
+10
-13
@@ -1,14 +1,11 @@
|
|||||||
{
|
{
|
||||||
"projectName": ".",
|
"projectName": ".",
|
||||||
"mode": "file-router",
|
"mode": "file-router",
|
||||||
"typescript": true,
|
"typescript": true,
|
||||||
"tailwind": true,
|
"tailwind": true,
|
||||||
"packageManager": "bun",
|
"packageManager": "bun",
|
||||||
"git": true,
|
"git": true,
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"framework": "react-cra",
|
"framework": "react-cra",
|
||||||
"chosenAddOns": [
|
"chosenAddOns": ["form", "tanstack-query"]
|
||||||
"form",
|
}
|
||||||
"tanstack-query"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
const config = {
|
||||||
|
trailingComma: "es5",
|
||||||
|
printWidth: 120,
|
||||||
|
tabWidth: 4,
|
||||||
|
semi: true,
|
||||||
|
singleQuote: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
+84
-91
@@ -1,4 +1,4 @@
|
|||||||
Welcome to your new TanStack app!
|
Welcome to your new TanStack app!
|
||||||
|
|
||||||
# Getting Started
|
# Getting Started
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ To run this application:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun install
|
bun install
|
||||||
bunx --bun run start
|
bunx --bun run start
|
||||||
```
|
```
|
||||||
|
|
||||||
# Building For Production
|
# Building For Production
|
||||||
@@ -29,10 +29,8 @@ bunx --bun run test
|
|||||||
|
|
||||||
This project uses [Tailwind CSS](https://tailwindcss.com/) for styling.
|
This project uses [Tailwind CSS](https://tailwindcss.com/) for styling.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Routing
|
## Routing
|
||||||
|
|
||||||
This project uses [TanStack Router](https://tanstack.com/router). The initial setup is a file based router. Which means that the routes are managed as files in `src/routes`.
|
This project uses [TanStack Router](https://tanstack.com/router). The initial setup is a file based router. Which means that the routes are managed as files in `src/routes`.
|
||||||
|
|
||||||
### Adding A Route
|
### Adding A Route
|
||||||
@@ -68,32 +66,31 @@ In the File Based Routing setup the layout is located in `src/routes/__root.tsx`
|
|||||||
Here is an example layout that includes a header:
|
Here is an example layout that includes a header:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { Outlet, createRootRoute } from '@tanstack/react-router'
|
import { Outlet, createRootRoute } from "@tanstack/react-router";
|
||||||
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
|
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
||||||
|
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
export const Route = createRootRoute({
|
export const Route = createRootRoute({
|
||||||
component: () => (
|
component: () => (
|
||||||
<>
|
<>
|
||||||
<header>
|
<header>
|
||||||
<nav>
|
<nav>
|
||||||
<Link to="/">Home</Link>
|
<Link to="/">Home</Link>
|
||||||
<Link to="/about">About</Link>
|
<Link to="/about">About</Link>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<TanStackRouterDevtools />
|
<TanStackRouterDevtools />
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
})
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
The `<TanStackRouterDevtools />` component is not required so you can remove it if you don't want it in your layout.
|
The `<TanStackRouterDevtools />` component is not required so you can remove it if you don't want it in your layout.
|
||||||
|
|
||||||
More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts).
|
More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts).
|
||||||
|
|
||||||
|
|
||||||
## Data Fetching
|
## 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.
|
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.
|
||||||
@@ -102,26 +99,26 @@ For example:
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
const peopleRoute = createRoute({
|
const peopleRoute = createRoute({
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
path: "/people",
|
path: "/people",
|
||||||
loader: async () => {
|
loader: async () => {
|
||||||
const response = await fetch("https://swapi.dev/api/people");
|
const response = await fetch("https://swapi.dev/api/people");
|
||||||
return response.json() as Promise<{
|
return response.json() as Promise<{
|
||||||
results: {
|
results: {
|
||||||
name: string;
|
name: string;
|
||||||
}[];
|
}[];
|
||||||
}>;
|
}>;
|
||||||
},
|
},
|
||||||
component: () => {
|
component: () => {
|
||||||
const data = peopleRoute.useLoaderData();
|
const data = peopleRoute.useLoaderData();
|
||||||
return (
|
return (
|
||||||
<ul>
|
<ul>
|
||||||
{data.results.map((person) => (
|
{data.results.map((person) => (
|
||||||
<li key={person.name}>{person.name}</li>
|
<li key={person.name}>{person.name}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -149,13 +146,13 @@ const queryClient = new QueryClient();
|
|||||||
// ...
|
// ...
|
||||||
|
|
||||||
if (!rootElement.innerHTML) {
|
if (!rootElement.innerHTML) {
|
||||||
const root = ReactDOM.createRoot(rootElement);
|
const root = ReactDOM.createRoot(rootElement);
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -165,13 +162,13 @@ You can also add TanStack Query Devtools to the root route (optional).
|
|||||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||||
|
|
||||||
const rootRoute = createRootRoute({
|
const rootRoute = createRootRoute({
|
||||||
component: () => (
|
component: () => (
|
||||||
<>
|
<>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<ReactQueryDevtools buttonPosition="top-right" />
|
<ReactQueryDevtools buttonPosition="top-right" />
|
||||||
<TanStackRouterDevtools />
|
<TanStackRouterDevtools />
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -183,24 +180,24 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { data } = useQuery({
|
const { data } = useQuery({
|
||||||
queryKey: ["people"],
|
queryKey: ["people"],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
fetch("https://swapi.dev/api/people")
|
fetch("https://swapi.dev/api/people")
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => data.results as { name: string }[]),
|
.then((data) => data.results as { name: string }[]),
|
||||||
initialData: [],
|
initialData: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ul>
|
<ul>
|
||||||
{data.map((person) => (
|
{data.map((person) => (
|
||||||
<li key={person.name}>{person.name}</li>
|
<li key={person.name}>{person.name}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
@@ -228,14 +225,12 @@ import "./App.css";
|
|||||||
const countStore = new Store(0);
|
const countStore = new Store(0);
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const count = useStore(countStore);
|
const count = useStore(countStore);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button onClick={() => countStore.setState((n) => n + 1)}>
|
<button onClick={() => countStore.setState((n) => n + 1)}>Increment - {count}</button>
|
||||||
Increment - {count}
|
</div>
|
||||||
</button>
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
@@ -253,23 +248,21 @@ import "./App.css";
|
|||||||
const countStore = new Store(0);
|
const countStore = new Store(0);
|
||||||
|
|
||||||
const doubledStore = new Derived({
|
const doubledStore = new Derived({
|
||||||
fn: () => countStore.state * 2,
|
fn: () => countStore.state * 2,
|
||||||
deps: [countStore],
|
deps: [countStore],
|
||||||
});
|
});
|
||||||
doubledStore.mount();
|
doubledStore.mount();
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const count = useStore(countStore);
|
const count = useStore(countStore);
|
||||||
const doubledCount = useStore(doubledStore);
|
const doubledCount = useStore(doubledStore);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button onClick={() => countStore.setState((n) => n + 1)}>
|
<button onClick={() => countStore.setState((n) => n + 1)}>Increment - {count}</button>
|
||||||
Increment - {count}
|
<div>Doubled - {doubledCount}</div>
|
||||||
</button>
|
</div>
|
||||||
<div>Doubled - {doubledCount}</div>
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"@types/react-dom": "^19.0.3",
|
"@types/react-dom": "^19.0.3",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"vite": "^6.1.0",
|
"vite": "^6.1.0",
|
||||||
"vitest": "^3.0.5",
|
"vitest": "^3.0.5",
|
||||||
|
|||||||
+15
-18
@@ -1,20 +1,17 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta
|
<meta name="description" content="Web site created using create-tsrouter-app" />
|
||||||
name="description"
|
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||||
content="Web site created using create-tsrouter-app"
|
<link rel="manifest" href="/manifest.json" />
|
||||||
/>
|
<title>Create TanStack App - .</title>
|
||||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
</head>
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<body>
|
||||||
<title>Create TanStack App - .</title>
|
<div id="app"></div>
|
||||||
</head>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
<body>
|
</body>
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+35
-35
@@ -1,37 +1,37 @@
|
|||||||
{
|
{
|
||||||
"name": "Cover letter frontend",
|
"name": "Cover letter frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --port 3000 --host",
|
"dev": "vite --port 3000 --host",
|
||||||
"build": "vite build && tsc",
|
"build": "vite build && tsc",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
"test": "vitest run"
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.6",
|
"@tailwindcss/vite": "^4.0.6",
|
||||||
"@tanstack/react-form": "^1.0.0",
|
"@tanstack/react-form": "^1.0.0",
|
||||||
"@tanstack/react-query": "^5.66.5",
|
"@tanstack/react-query": "^5.66.5",
|
||||||
"@tanstack/react-query-devtools": "^5.66.5",
|
"@tanstack/react-query-devtools": "^5.66.5",
|
||||||
"@tanstack/react-router": "^1.121.2",
|
"@tanstack/react-router": "^1.121.2",
|
||||||
"@tanstack/react-router-devtools": "^1.121.2",
|
"@tanstack/react-router-devtools": "^1.121.2",
|
||||||
"@tanstack/router-plugin": "^1.121.2",
|
"@tanstack/router-plugin": "^1.121.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"tailwindcss": "^4.0.6",
|
"tailwindcss": "^4.0.6",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/react": "^16.2.0",
|
"@testing-library/react": "^16.2.0",
|
||||||
"@types/react": "^19.0.8",
|
"@types/react": "^19.0.8",
|
||||||
"@types/react-dom": "^19.0.3",
|
"@types/react-dom": "^19.0.3",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
"typescript": "^5.7.2",
|
"prettier": "^3.6.2",
|
||||||
"vite": "^6.1.0",
|
"typescript": "^5.7.2",
|
||||||
"vitest": "^3.0.5",
|
"vite": "^6.1.0",
|
||||||
"web-vitals": "^4.2.4"
|
"vitest": "^3.0.5",
|
||||||
}
|
"web-vitals": "^4.2.4"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
{
|
{
|
||||||
"short_name": "TanStack App",
|
"short_name": "TanStack App",
|
||||||
"name": "Create TanStack App Sample",
|
"name": "Create TanStack App Sample",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "favicon.ico",
|
||||||
"sizes": "64x64 32x32 24x24 16x16",
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
"type": "image/x-icon"
|
"type": "image/x-icon"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "logo192.png",
|
"src": "logo192.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "192x192"
|
"sizes": "192x192"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "logo512.png",
|
"src": "logo512.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "512x512"
|
"sizes": "512x512"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"start_url": ".",
|
"start_url": ".",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"theme_color": "#000000",
|
"theme_color": "#000000",
|
||||||
"background_color": "#ffffff"
|
"background_color": "#ffffff"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
import { Link } from '@tanstack/react-router'
|
import { Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
return (
|
return (
|
||||||
<header className="p-2 flex gap-2 bg-white text-black justify-between">
|
<header className="p-2 flex gap-2 bg-white text-black justify-between">
|
||||||
<nav className="flex flex-row">
|
<nav className="flex flex-row">
|
||||||
<div className="px-2 font-bold">
|
<div className="px-2 font-bold">
|
||||||
<Link to="/">Home</Link>
|
<Link to="/">Home</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-2 font-bold">
|
<div className="px-2 font-bold">
|
||||||
<Link to="/demo/form/simple">Simple Form</Link>
|
<Link to="/demo/form/simple">Simple Form</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-2 font-bold">
|
<div className="px-2 font-bold">
|
||||||
<Link to="/demo/form/address">Address Form</Link>
|
<Link to="/demo/form/address">Address Form</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-2 font-bold">
|
<div className="px-2 font-bold">
|
||||||
<Link to="/demo/tanstack-query">TanStack Query</Link>
|
<Link to="/demo/tanstack-query">TanStack Query</Link>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,127 +1,108 @@
|
|||||||
import { useStore } from '@tanstack/react-form'
|
import { useStore } from "@tanstack/react-form";
|
||||||
|
|
||||||
import { useFieldContext, useFormContext } from '../hooks/demo.form-context'
|
import { useFieldContext, useFormContext } from "../hooks/demo.form-context";
|
||||||
|
|
||||||
export function SubscribeButton({ label }: { label: string }) {
|
export function SubscribeButton({ label }: { label: string }) {
|
||||||
const form = useFormContext()
|
const form = useFormContext();
|
||||||
return (
|
return (
|
||||||
<form.Subscribe selector={(state) => state.isSubmitting}>
|
<form.Subscribe selector={(state) => state.isSubmitting}>
|
||||||
{(isSubmitting) => (
|
{(isSubmitting) => (
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className="px-6 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition-colors disabled:opacity-50"
|
className="px-6 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</form.Subscribe>
|
</form.Subscribe>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ErrorMessages({
|
function ErrorMessages({ errors }: { errors: Array<string | { message: string }> }) {
|
||||||
errors,
|
return (
|
||||||
}: {
|
<>
|
||||||
errors: Array<string | { message: string }>
|
{errors.map((error) => (
|
||||||
}) {
|
<div key={typeof error === "string" ? error : error.message} className="text-red-500 mt-1 font-bold">
|
||||||
return (
|
{typeof error === "string" ? error : error.message}
|
||||||
<>
|
</div>
|
||||||
{errors.map((error) => (
|
))}
|
||||||
<div
|
</>
|
||||||
key={typeof error === 'string' ? error : error.message}
|
);
|
||||||
className="text-red-500 mt-1 font-bold"
|
}
|
||||||
>
|
|
||||||
{typeof error === 'string' ? error : error.message}
|
export function TextField({ label, placeholder }: { label: string; placeholder?: string }) {
|
||||||
|
const field = useFieldContext<string>();
|
||||||
|
const errors = useStore(field.store, (state) => state.meta.errors);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label htmlFor={label} className="block font-bold mb-1 text-xl">
|
||||||
|
{label}
|
||||||
|
<input
|
||||||
|
value={field.state.value}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) => field.handleChange(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{field.state.meta.isTouched && <ErrorMessages errors={errors} />}
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TextField({
|
export function TextArea({ label, rows = 3 }: { label: string; rows?: number }) {
|
||||||
label,
|
const field = useFieldContext<string>();
|
||||||
placeholder,
|
const errors = useStore(field.store, (state) => state.meta.errors);
|
||||||
}: {
|
|
||||||
label: string
|
|
||||||
placeholder?: string
|
|
||||||
}) {
|
|
||||||
const field = useFieldContext<string>()
|
|
||||||
const errors = useStore(field.store, (state) => state.meta.errors)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={label} className="block font-bold mb-1 text-xl">
|
<label htmlFor={label} className="block font-bold mb-1 text-xl">
|
||||||
{label}
|
{label}
|
||||||
<input
|
<textarea
|
||||||
value={field.state.value}
|
value={field.state.value}
|
||||||
placeholder={placeholder}
|
onBlur={field.handleBlur}
|
||||||
onBlur={field.handleBlur}
|
rows={rows}
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
onChange={(e) => field.handleChange(e.target.value)}
|
||||||
className="w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
className="w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
{field.state.meta.isTouched && <ErrorMessages errors={errors} />}
|
{field.state.meta.isTouched && <ErrorMessages errors={errors} />}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
|
||||||
|
|
||||||
export function TextArea({
|
|
||||||
label,
|
|
||||||
rows = 3,
|
|
||||||
}: {
|
|
||||||
label: string
|
|
||||||
rows?: number
|
|
||||||
}) {
|
|
||||||
const field = useFieldContext<string>()
|
|
||||||
const errors = useStore(field.store, (state) => state.meta.errors)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<label htmlFor={label} className="block font-bold mb-1 text-xl">
|
|
||||||
{label}
|
|
||||||
<textarea
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
rows={rows}
|
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
|
||||||
className="w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
{field.state.meta.isTouched && <ErrorMessages errors={errors} />}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Select({
|
export function Select({
|
||||||
label,
|
label,
|
||||||
values,
|
values,
|
||||||
}: {
|
}: {
|
||||||
label: string
|
label: string;
|
||||||
values: Array<{ label: string; value: string }>
|
values: Array<{ label: string; value: string }>;
|
||||||
placeholder?: string
|
placeholder?: string;
|
||||||
}) {
|
}) {
|
||||||
const field = useFieldContext<string>()
|
const field = useFieldContext<string>();
|
||||||
const errors = useStore(field.store, (state) => state.meta.errors)
|
const errors = useStore(field.store, (state) => state.meta.errors);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={label} className="block font-bold mb-1 text-xl">
|
<label htmlFor={label} className="block font-bold mb-1 text-xl">
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
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)}
|
||||||
className="w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
className="w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||||
>
|
>
|
||||||
{values.map((value) => (
|
{values.map((value) => (
|
||||||
<option key={value.value} value={value.value}>
|
<option key={value.value} value={value.value}>
|
||||||
{value.label}
|
{value.label}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
{field.state.meta.isTouched && <ErrorMessages errors={errors} />}
|
{field.state.meta.isTouched && <ErrorMessages errors={errors} />}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { createFormHookContexts } from '@tanstack/react-form'
|
import { createFormHookContexts } from "@tanstack/react-form";
|
||||||
|
|
||||||
export const { fieldContext, useFieldContext, formContext, useFormContext } =
|
export const { fieldContext, useFieldContext, formContext, useFormContext } = createFormHookContexts();
|
||||||
createFormHookContexts()
|
|
||||||
|
|||||||
@@ -1,22 +1,17 @@
|
|||||||
import { createFormHook } from '@tanstack/react-form'
|
import { createFormHook } from "@tanstack/react-form";
|
||||||
|
|
||||||
import {
|
import { Select, SubscribeButton, TextArea, TextField } from "../components/demo.FormComponents";
|
||||||
Select,
|
import { fieldContext, formContext } from "./demo.form-context";
|
||||||
SubscribeButton,
|
|
||||||
TextArea,
|
|
||||||
TextField,
|
|
||||||
} from '../components/demo.FormComponents'
|
|
||||||
import { fieldContext, formContext } from './demo.form-context'
|
|
||||||
|
|
||||||
export const { useAppForm } = createFormHook({
|
export const { useAppForm } = createFormHook({
|
||||||
fieldComponents: {
|
fieldComponents: {
|
||||||
TextField,
|
TextField,
|
||||||
Select,
|
Select,
|
||||||
TextArea,
|
TextArea,
|
||||||
},
|
},
|
||||||
formComponents: {
|
formComponents: {
|
||||||
SubscribeButton,
|
SubscribeButton,
|
||||||
},
|
},
|
||||||
fieldContext,
|
fieldContext,
|
||||||
formContext,
|
formContext,
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||||
|
|
||||||
export default function LayoutAddition() {
|
export default function LayoutAddition() {
|
||||||
return <ReactQueryDevtools buttonPosition="bottom-right" />
|
return <ReactQueryDevtools buttonPosition="bottom-right" />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
|
||||||
const queryClient = new QueryClient()
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
export function getContext() {
|
export function getContext() {
|
||||||
return {
|
return {
|
||||||
queryClient,
|
queryClient,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Provider({ children }: { children: React.ReactNode }) {
|
export function Provider({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
||||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-30
@@ -1,48 +1,48 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from "react";
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from "react-dom/client";
|
||||||
import { RouterProvider, createRouter } from '@tanstack/react-router'
|
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
||||||
|
|
||||||
import * as TanStackQueryProvider from './integrations/tanstack-query/root-provider.tsx'
|
import * as TanStackQueryProvider from "./integrations/tanstack-query/root-provider.tsx";
|
||||||
|
|
||||||
// Import the generated route tree
|
// Import the generated route tree
|
||||||
import { routeTree } from './routeTree.gen'
|
import { routeTree } from "./routeTree.gen";
|
||||||
|
|
||||||
import './styles.css'
|
import "./styles.css";
|
||||||
import reportWebVitals from './reportWebVitals.ts'
|
import reportWebVitals from "./reportWebVitals.ts";
|
||||||
|
|
||||||
// Create a new router instance
|
// Create a new router instance
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
routeTree,
|
routeTree,
|
||||||
context: {
|
context: {
|
||||||
...TanStackQueryProvider.getContext(),
|
...TanStackQueryProvider.getContext(),
|
||||||
},
|
},
|
||||||
defaultPreload: 'intent',
|
defaultPreload: "intent",
|
||||||
scrollRestoration: true,
|
scrollRestoration: true,
|
||||||
defaultStructuralSharing: true,
|
defaultStructuralSharing: true,
|
||||||
defaultPreloadStaleTime: 0,
|
defaultPreloadStaleTime: 0,
|
||||||
})
|
});
|
||||||
|
|
||||||
// Register the router instance for type safety
|
// Register the router instance for type safety
|
||||||
declare module '@tanstack/react-router' {
|
declare module "@tanstack/react-router" {
|
||||||
interface Register {
|
interface Register {
|
||||||
router: typeof router
|
router: typeof router;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render the app
|
// Render the app
|
||||||
const rootElement = document.getElementById('app')
|
const rootElement = document.getElementById("app");
|
||||||
if (rootElement && !rootElement.innerHTML) {
|
if (rootElement && !rootElement.innerHTML) {
|
||||||
const root = ReactDOM.createRoot(rootElement)
|
const root = ReactDOM.createRoot(rootElement);
|
||||||
root.render(
|
root.render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<TanStackQueryProvider.Provider>
|
<TanStackQueryProvider.Provider>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</TanStackQueryProvider.Provider>
|
</TanStackQueryProvider.Provider>
|
||||||
</StrictMode>,
|
</StrictMode>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If you want to start measuring performance in your app, pass a function
|
// If you want to start measuring performance in your app, pass a function
|
||||||
// to log results (for example: reportWebVitals(console.log))
|
// to log results (for example: reportWebVitals(console.log))
|
||||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||||
reportWebVitals()
|
reportWebVitals();
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
const reportWebVitals = (onPerfEntry?: () => void) => {
|
const reportWebVitals = (onPerfEntry?: () => void) => {
|
||||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||||
import('web-vitals').then(({ onCLS, onINP, onFCP, onLCP, onTTFB }) => {
|
import("web-vitals").then(({ onCLS, onINP, onFCP, onLCP, onTTFB }) => {
|
||||||
onCLS(onPerfEntry)
|
onCLS(onPerfEntry);
|
||||||
onINP(onPerfEntry)
|
onINP(onPerfEntry);
|
||||||
onFCP(onPerfEntry)
|
onFCP(onPerfEntry);
|
||||||
onLCP(onPerfEntry)
|
onLCP(onPerfEntry);
|
||||||
onTTFB(onPerfEntry)
|
onTTFB(onPerfEntry);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export default reportWebVitals
|
export default reportWebVitals;
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
import { Outlet, createRootRouteWithContext } from '@tanstack/react-router'
|
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
|
||||||
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
|
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
||||||
|
|
||||||
import Header from '../components/Header'
|
import Header from "../components/Header";
|
||||||
|
|
||||||
import TanStackQueryLayout from '../integrations/tanstack-query/layout.tsx'
|
import TanStackQueryLayout from "../integrations/tanstack-query/layout.tsx";
|
||||||
|
|
||||||
import type { QueryClient } from '@tanstack/react-query'
|
import type { QueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
interface MyRouterContext {
|
interface MyRouterContext {
|
||||||
queryClient: QueryClient
|
queryClient: QueryClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Route = createRootRouteWithContext<MyRouterContext>()({
|
export const Route = createRootRouteWithContext<MyRouterContext>()({
|
||||||
component: () => (
|
component: () => (
|
||||||
<>
|
<>
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<TanStackRouterDevtools />
|
<TanStackRouterDevtools />
|
||||||
|
|
||||||
<TanStackQueryLayout />
|
<TanStackQueryLayout />
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,200 +1,191 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { useAppForm } from '../hooks/demo.form'
|
import { useAppForm } from "../hooks/demo.form";
|
||||||
|
|
||||||
export const Route = createFileRoute('/demo/form/address')({
|
export const Route = createFileRoute("/demo/form/address")({
|
||||||
component: AddressForm,
|
component: AddressForm,
|
||||||
})
|
});
|
||||||
|
|
||||||
function AddressForm() {
|
function AddressForm() {
|
||||||
const form = useAppForm({
|
const form = useAppForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
fullName: '',
|
fullName: "",
|
||||||
email: '',
|
email: "",
|
||||||
address: {
|
address: {
|
||||||
street: '',
|
street: "",
|
||||||
city: '',
|
city: "",
|
||||||
state: '',
|
state: "",
|
||||||
zipCode: '',
|
zipCode: "",
|
||||||
country: '',
|
country: "",
|
||||||
},
|
},
|
||||||
phone: '',
|
phone: "",
|
||||||
},
|
},
|
||||||
validators: {
|
validators: {
|
||||||
onBlur: ({ value }) => {
|
onBlur: ({ value }) => {
|
||||||
const errors = {
|
const errors = {
|
||||||
fields: {},
|
fields: {},
|
||||||
} as {
|
} as {
|
||||||
fields: Record<string, string>
|
fields: Record<string, string>;
|
||||||
}
|
};
|
||||||
if (value.fullName.trim().length === 0) {
|
if (value.fullName.trim().length === 0) {
|
||||||
errors.fields.fullName = 'Full name is required'
|
errors.fields.fullName = "Full name is required";
|
||||||
}
|
}
|
||||||
return errors
|
return errors;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
onSubmit: ({ value }) => {
|
onSubmit: ({ value }) => {
|
||||||
console.log(value)
|
console.log(value);
|
||||||
// Show success message
|
// Show success message
|
||||||
alert('Form submitted successfully!')
|
alert("Form submitted successfully!");
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-center min-h-screen bg-gradient-to-br from-purple-100 to-blue-100 p-4 text-white"
|
className="flex items-center justify-center min-h-screen bg-gradient-to-br from-purple-100 to-blue-100 p-4 text-white"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage:
|
backgroundImage: "radial-gradient(50% 50% at 5% 40%, #f4a460 0%, #8b4513 70%, #1a0f0a 100%)",
|
||||||
'radial-gradient(50% 50% at 5% 40%, #f4a460 0%, #8b4513 70%, #1a0f0a 100%)',
|
}}
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="w-full max-w-2xl p-8 rounded-xl backdrop-blur-md bg-black/50 shadow-xl border-8 border-black/10">
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
form.handleSubmit()
|
|
||||||
}}
|
|
||||||
className="space-y-6"
|
|
||||||
>
|
>
|
||||||
<form.AppField name="fullName">
|
<div className="w-full max-w-2xl p-8 rounded-xl backdrop-blur-md bg-black/50 shadow-xl border-8 border-black/10">
|
||||||
{(field) => <field.TextField label="Full Name" />}
|
<form
|
||||||
</form.AppField>
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
form.handleSubmit();
|
||||||
|
}}
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
|
<form.AppField name="fullName">{(field) => <field.TextField label="Full Name" />}</form.AppField>
|
||||||
|
|
||||||
<form.AppField
|
<form.AppField
|
||||||
name="email"
|
name="email"
|
||||||
validators={{
|
validators={{
|
||||||
onBlur: ({ value }) => {
|
onBlur: ({ value }) => {
|
||||||
if (!value || value.trim().length === 0) {
|
if (!value || value.trim().length === 0) {
|
||||||
return 'Email is required'
|
return "Email is required";
|
||||||
}
|
}
|
||||||
if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value)) {
|
if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value)) {
|
||||||
return 'Invalid email address'
|
return "Invalid email address";
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined;
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(field) => <field.TextField label="Email" />}
|
{(field) => <field.TextField label="Email" />}
|
||||||
</form.AppField>
|
</form.AppField>
|
||||||
|
|
||||||
<form.AppField
|
<form.AppField
|
||||||
name="address.street"
|
name="address.street"
|
||||||
validators={{
|
validators={{
|
||||||
onBlur: ({ value }) => {
|
onBlur: ({ value }) => {
|
||||||
if (!value || value.trim().length === 0) {
|
if (!value || value.trim().length === 0) {
|
||||||
return 'Street address is required'
|
return "Street address is required";
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined;
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(field) => <field.TextField label="Street Address" />}
|
{(field) => <field.TextField label="Street Address" />}
|
||||||
</form.AppField>
|
</form.AppField>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<form.AppField
|
<form.AppField
|
||||||
name="address.city"
|
name="address.city"
|
||||||
validators={{
|
validators={{
|
||||||
onBlur: ({ value }) => {
|
onBlur: ({ value }) => {
|
||||||
if (!value || value.trim().length === 0) {
|
if (!value || value.trim().length === 0) {
|
||||||
return 'City is required'
|
return "City is required";
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined;
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(field) => <field.TextField label="City" />}
|
{(field) => <field.TextField label="City" />}
|
||||||
</form.AppField>
|
</form.AppField>
|
||||||
<form.AppField
|
<form.AppField
|
||||||
name="address.state"
|
name="address.state"
|
||||||
validators={{
|
validators={{
|
||||||
onBlur: ({ value }) => {
|
onBlur: ({ value }) => {
|
||||||
if (!value || value.trim().length === 0) {
|
if (!value || value.trim().length === 0) {
|
||||||
return 'State is required'
|
return "State is required";
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined;
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(field) => <field.TextField label="State" />}
|
{(field) => <field.TextField label="State" />}
|
||||||
</form.AppField>
|
</form.AppField>
|
||||||
<form.AppField
|
<form.AppField
|
||||||
name="address.zipCode"
|
name="address.zipCode"
|
||||||
validators={{
|
validators={{
|
||||||
onBlur: ({ value }) => {
|
onBlur: ({ value }) => {
|
||||||
if (!value || value.trim().length === 0) {
|
if (!value || value.trim().length === 0) {
|
||||||
return 'Zip code is required'
|
return "Zip code is required";
|
||||||
}
|
}
|
||||||
if (!/^\d{5}(-\d{4})?$/.test(value)) {
|
if (!/^\d{5}(-\d{4})?$/.test(value)) {
|
||||||
return 'Invalid zip code format'
|
return "Invalid zip code format";
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined;
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(field) => <field.TextField label="Zip Code" />}
|
{(field) => <field.TextField label="Zip Code" />}
|
||||||
</form.AppField>
|
</form.AppField>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form.AppField
|
<form.AppField
|
||||||
name="address.country"
|
name="address.country"
|
||||||
validators={{
|
validators={{
|
||||||
onBlur: ({ value }) => {
|
onBlur: ({ value }) => {
|
||||||
if (!value || value.trim().length === 0) {
|
if (!value || value.trim().length === 0) {
|
||||||
return 'Country is required'
|
return "Country is required";
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined;
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(field) => (
|
{(field) => (
|
||||||
<field.Select
|
<field.Select
|
||||||
label="Country"
|
label="Country"
|
||||||
values={[
|
values={[
|
||||||
{ label: 'United States', value: 'US' },
|
{ label: "United States", value: "US" },
|
||||||
{ label: 'Canada', value: 'CA' },
|
{ label: "Canada", value: "CA" },
|
||||||
{ label: 'United Kingdom', value: 'UK' },
|
{ label: "United Kingdom", value: "UK" },
|
||||||
{ label: 'Australia', value: 'AU' },
|
{ label: "Australia", value: "AU" },
|
||||||
{ label: 'Germany', value: 'DE' },
|
{ label: "Germany", value: "DE" },
|
||||||
{ label: 'France', value: 'FR' },
|
{ label: "France", value: "FR" },
|
||||||
{ label: 'Japan', value: 'JP' },
|
{ label: "Japan", value: "JP" },
|
||||||
]}
|
]}
|
||||||
placeholder="Select a country"
|
placeholder="Select a country"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</form.AppField>
|
</form.AppField>
|
||||||
|
|
||||||
<form.AppField
|
<form.AppField
|
||||||
name="phone"
|
name="phone"
|
||||||
validators={{
|
validators={{
|
||||||
onBlur: ({ value }) => {
|
onBlur: ({ value }) => {
|
||||||
if (!value || value.trim().length === 0) {
|
if (!value || value.trim().length === 0) {
|
||||||
return 'Phone number is required'
|
return "Phone number is required";
|
||||||
}
|
}
|
||||||
if (
|
if (!/^(\+\d{1,3})?\s?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$/.test(value)) {
|
||||||
!/^(\+\d{1,3})?\s?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$/.test(
|
return "Invalid phone number format";
|
||||||
value,
|
}
|
||||||
)
|
return undefined;
|
||||||
) {
|
},
|
||||||
return 'Invalid phone number format'
|
}}
|
||||||
}
|
>
|
||||||
return undefined
|
{(field) => <field.TextField label="Phone" placeholder="123-456-7890" />}
|
||||||
},
|
</form.AppField>
|
||||||
}}
|
|
||||||
>
|
|
||||||
{(field) => (
|
|
||||||
<field.TextField label="Phone" placeholder="123-456-7890" />
|
|
||||||
)}
|
|
||||||
</form.AppField>
|
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<form.AppForm>
|
<form.AppForm>
|
||||||
<form.SubscribeButton label="Submit" />
|
<form.SubscribeButton label="Submit" />
|
||||||
</form.AppForm>
|
</form.AppForm>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +1,63 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { z } from 'zod'
|
import { z } from "zod";
|
||||||
|
|
||||||
import { useAppForm } from '../hooks/demo.form'
|
import { useAppForm } from "../hooks/demo.form";
|
||||||
|
|
||||||
export const Route = createFileRoute('/demo/form/simple')({
|
// Routing
|
||||||
component: SimpleForm,
|
export const Route = createFileRoute("/demo/form/simple")({
|
||||||
})
|
component: SimpleForm,
|
||||||
|
});
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
title: z.string().min(1, 'Title is required'),
|
title: z.string().min(1, "Title is required"),
|
||||||
description: z.string().min(1, 'Description is required'),
|
description: z.string().min(1, "Description is required"),
|
||||||
})
|
});
|
||||||
|
|
||||||
function SimpleForm() {
|
function SimpleForm() {
|
||||||
const form = useAppForm({
|
const form = useAppForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
title: '',
|
title: "",
|
||||||
description: '',
|
description: "",
|
||||||
},
|
},
|
||||||
validators: {
|
validators: {
|
||||||
onBlur: schema,
|
onBlur: schema,
|
||||||
},
|
},
|
||||||
onSubmit: ({ value }) => {
|
onSubmit: ({ value }) => {
|
||||||
console.log(value)
|
console.log(value);
|
||||||
// Show success message
|
// Show success message
|
||||||
alert('Form submitted successfully!')
|
alert("Form submitted successfully!");
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-center min-h-screen bg-gradient-to-br from-purple-100 to-blue-100 p-4 text-white"
|
className="flex items-center justify-center min-h-screen bg-gradient-to-br from-purple-100 to-blue-100 p-4 text-white"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage:
|
backgroundImage: "radial-gradient(50% 50% at 5% 40%, #add8e6 0%, #0000ff 70%, #00008b 100%)",
|
||||||
'radial-gradient(50% 50% at 5% 40%, #add8e6 0%, #0000ff 70%, #00008b 100%)',
|
}}
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="w-full max-w-2xl p-8 rounded-xl backdrop-blur-md bg-black/50 shadow-xl border-8 border-black/10">
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
form.handleSubmit()
|
|
||||||
}}
|
|
||||||
className="space-y-6"
|
|
||||||
>
|
>
|
||||||
<form.AppField name="title">
|
<div className="w-full max-w-2xl p-8 rounded-xl backdrop-blur-md bg-black/50 shadow-xl border-8 border-black/10">
|
||||||
{(field) => <field.TextField label="Title" />}
|
<form
|
||||||
</form.AppField>
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
form.handleSubmit();
|
||||||
|
}}
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
|
<form.AppField name="title">{(field) => <field.TextField label="Title" />}</form.AppField>
|
||||||
|
|
||||||
<form.AppField name="description">
|
<form.AppField name="description">
|
||||||
{(field) => <field.TextArea label="Description" />}
|
{(field) => <field.TextArea label="Description" />}
|
||||||
</form.AppField>
|
</form.AppField>
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<form.AppForm>
|
<form.AppForm>
|
||||||
<form.SubscribeButton label="Submit" />
|
<form.SubscribeButton label="Submit" />
|
||||||
</form.AppForm>
|
</form.AppForm>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
export const Route = createFileRoute('/demo/tanstack-query')({
|
export const Route = createFileRoute("/demo/tanstack-query")({
|
||||||
component: TanStackQueryDemo,
|
component: TanStackQueryDemo,
|
||||||
})
|
});
|
||||||
|
|
||||||
function TanStackQueryDemo() {
|
function TanStackQueryDemo() {
|
||||||
const { data } = useQuery({
|
const { data } = useQuery({
|
||||||
queryKey: ['people'],
|
queryKey: ["people"],
|
||||||
queryFn: () =>
|
queryFn: () => Promise.resolve([{ name: "John Doe" }, { name: "Jane Doe" }]),
|
||||||
Promise.resolve([{ name: 'John Doe' }, { name: 'Jane Doe' }]),
|
initialData: [],
|
||||||
initialData: [],
|
});
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<h1 className="text-2xl mb-4">People list</h1>
|
<h1 className="text-2xl mb-4">People list</h1>
|
||||||
<ul>
|
<ul>
|
||||||
{data.map((person) => (
|
{data.map((person) => (
|
||||||
<li key={person.name}>{person.name}</li>
|
<li key={person.name}>{person.name}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,38 +2,38 @@ import { createFileRoute } from "@tanstack/react-router";
|
|||||||
import logo from "../logo.svg";
|
import logo from "../logo.svg";
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
component: App,
|
component: App,
|
||||||
});
|
});
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<header className="min-h-screen flex flex-col items-center justify-center bg-[#282c34] text-white text-[calc(10px+2vmin)]">
|
<header className="min-h-screen flex flex-col items-center justify-center bg-[#282c34] text-white text-[calc(10px+2vmin)]">
|
||||||
<img
|
<img
|
||||||
src={logo}
|
src={logo}
|
||||||
className="h-[40vmin] pointer-events-none animate-[spin_20s_linear_infinite]"
|
className="h-[40vmin] pointer-events-none animate-[spin_20s_linear_infinite]"
|
||||||
alt="logo"
|
alt="logo"
|
||||||
/>
|
/>
|
||||||
<p>
|
<p>
|
||||||
Edit <code>src/routes/index.tsx</code> and save to reload.
|
Edit <code>src/routes/index.tsx</code> and save to reload.
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
className="text-[#61dafb] hover:underline"
|
className="text-[#61dafb] hover:underline"
|
||||||
href="https://reactjs.org"
|
href="https://reactjs.org"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
Learn React
|
Learn React
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
className="text-[#61dafb] hover:underline"
|
className="text-[#61dafb] hover:underline"
|
||||||
href="https://tanstack.com"
|
href="https://tanstack.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
Learn TanStack
|
Learn TanStack
|
||||||
</a>
|
</a>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply m-0;
|
@apply m-0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
font-family:
|
||||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
|
||||||
sans-serif;
|
"Droid Sans", "Helvetica Neue", sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||||
monospace;
|
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-23
@@ -1,28 +1,28 @@
|
|||||||
{
|
{
|
||||||
"include": ["**/*.ts", "**/*.tsx"],
|
"include": ["**/*.ts", "**/*.tsx"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
"types": ["vite/client"],
|
"types": ["vite/client"],
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true,
|
"noUncheckedSideEffectImports": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"],
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-19
@@ -1,24 +1,20 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from "vite";
|
||||||
import viteReact from '@vitejs/plugin-react'
|
import viteReact from "@vitejs/plugin-react";
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
|
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
|
||||||
import { resolve } from 'node:path'
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [TanStackRouterVite({ autoCodeSplitting: true }), viteReact(), tailwindcss()],
|
||||||
TanStackRouterVite({ autoCodeSplitting: true }),
|
test: {
|
||||||
viteReact(),
|
globals: true,
|
||||||
tailwindcss(),
|
environment: "jsdom",
|
||||||
],
|
|
||||||
test: {
|
|
||||||
globals: true,
|
|
||||||
environment: 'jsdom',
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': resolve(__dirname, './src'),
|
|
||||||
},
|
},
|
||||||
},
|
resolve: {
|
||||||
})
|
alias: {
|
||||||
|
"@": resolve(__dirname, "./src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
file=development.yml
|
||||||
|
|
||||||
|
docker compose -f $file down && docker compose -f $file up --build -d
|
||||||
Reference in New Issue
Block a user