Skip to content

Commit 37645d2

Browse files
committed
Refactor example app to use TanStack Start and separate routes / collection type
1 parent cd2bd57 commit 37645d2

21 files changed

+5450
-106
lines changed

examples/react/todo/app.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { defineConfig } from "@tanstack/react-start/config"
2+
3+
export default defineConfig({})

examples/react/todo/package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@
66
"@tanstack/db-collections": "^0.0.23",
77
"@tanstack/query-core": "^5.81.5",
88
"@tanstack/react-db": "^0.0.19",
9+
"@tanstack/react-router": "^1.125.6",
10+
"@tanstack/react-start": "^1.126.1",
911
"cors": "^2.8.5",
1012
"drizzle-orm": "^0.40.1",
1113
"drizzle-zod": "^0.7.0",
1214
"express": "^4.19.2",
1315
"postgres": "^3.4.7",
1416
"react": "^19.1.0",
1517
"react-dom": "^19.1.0",
16-
"tailwindcss": "^4.1.11"
18+
"tailwindcss": "^4.1.11",
19+
"vite-tsconfig-paths": "^5.1.4"
1720
},
1821
"devDependencies": {
1922
"@eslint/js": "^9.22.0",
@@ -39,13 +42,12 @@
3942
"vite": "^6.2.2"
4043
},
4144
"scripts": {
42-
"api:dev": "tsx watch src/api/server.ts",
43-
"build": "tsc -b && vite build",
45+
"build": "vite build",
4446
"db:ensure-config": "tsx scripts/ensure-default-config.ts",
4547
"db:generate": "drizzle-kit generate",
4648
"db:push": "tsx scripts/migrate.ts",
4749
"db:studio": "drizzle-kit studio",
48-
"dev": "docker compose up -d && concurrently \"pnpm api:dev\" \"vite\"",
50+
"dev": "docker compose up -d && vite dev",
4951
"lint": "eslint . --fix",
5052
"preview": "vite preview"
5153
},

examples/react/todo/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type { SelectConfig, SelectTodo } from "./db/validation"
1212
import type { FormEvent } from "react"
1313

1414
// API helper for todos and config
15-
const API_BASE_URL = `http://localhost:3001/api`
15+
const API_BASE_URL = `/api`
1616

1717
const api = {
1818
// Todo API methods
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
import React, { useState } from "react"
2+
import { Link } from "@tanstack/react-router"
3+
import type { Collection } from "@tanstack/react-db"
4+
import type { SelectConfig, SelectTodo } from "../db/validation"
5+
import type { FormEvent } from "react"
6+
7+
interface TodoAppProps {
8+
todos: Array<SelectTodo>
9+
configData: Array<SelectConfig>
10+
todoCollection: Collection<SelectTodo>
11+
configCollection: Collection<SelectConfig>
12+
title: string
13+
}
14+
15+
export function TodoApp({
16+
todos,
17+
configData,
18+
todoCollection,
19+
configCollection,
20+
title,
21+
}: TodoAppProps) {
22+
const [newTodo, setNewTodo] = useState(``)
23+
24+
// Define a type-safe helper function to get config values
25+
const getConfigValue = (key: string): string => {
26+
for (const config of configData) {
27+
if (config.key === key) {
28+
return config.value
29+
}
30+
}
31+
return ``
32+
}
33+
34+
// Define a helper function to update config values
35+
const setConfigValue = (key: string, value: string): void => {
36+
for (const config of configData) {
37+
if (config.key === key) {
38+
configCollection.update(config.id, (draft) => {
39+
draft.value = value
40+
})
41+
return
42+
}
43+
}
44+
45+
// If the config doesn't exist yet, create it
46+
configCollection.insert({
47+
id: Math.round(Math.random() * 1000000),
48+
key,
49+
value,
50+
created_at: new Date(),
51+
updated_at: new Date(),
52+
})
53+
}
54+
55+
const backgroundColor = getConfigValue(`backgroundColor`)
56+
57+
// Function to generate a complementary color
58+
const getComplementaryColor = (hexColor: string): string => {
59+
// Default to a nice blue if no color is provided
60+
if (!hexColor) return `#3498db`
61+
62+
// Remove the hash if it exists
63+
const color = hexColor.replace(`#`, ``)
64+
65+
// Convert hex to RGB
66+
const r = parseInt(color.substr(0, 2), 16)
67+
const g = parseInt(color.substr(2, 2), 16)
68+
const b = parseInt(color.substr(4, 2), 16)
69+
70+
// Calculate complementary color (inverting the RGB values)
71+
const compR = 255 - r
72+
const compG = 255 - g
73+
const compB = 255 - b
74+
75+
// Convert back to hex
76+
const compHex =
77+
`#` +
78+
((1 << 24) + (compR << 16) + (compG << 8) + compB).toString(16).slice(1)
79+
80+
// Calculate brightness of the background
81+
const brightness = r * 0.299 + g * 0.587 + b * 0.114
82+
83+
// If the complementary color doesn't have enough contrast, adjust it
84+
const compBrightness = compR * 0.299 + compG * 0.587 + compB * 0.114
85+
const brightnessDiff = Math.abs(brightness - compBrightness)
86+
87+
if (brightnessDiff < 128) {
88+
// Not enough contrast, use a more vibrant alternative
89+
if (brightness > 128) {
90+
// Dark color for light background
91+
return `#8e44ad` // Purple
92+
} else {
93+
// Light color for dark background
94+
return `#f1c40f` // Yellow
95+
}
96+
}
97+
98+
return compHex
99+
}
100+
101+
const titleColor = getComplementaryColor(backgroundColor)
102+
103+
const handleColorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
104+
const newColor = e.target.value
105+
setConfigValue(`backgroundColor`, newColor)
106+
}
107+
108+
const handleSubmit = (e: FormEvent) => {
109+
e.preventDefault()
110+
if (!newTodo.trim()) return
111+
112+
todoCollection.insert({
113+
text: newTodo,
114+
completed: false,
115+
id: Math.round(Math.random() * 1000000),
116+
created_at: new Date(),
117+
updated_at: new Date(),
118+
})
119+
setNewTodo(``)
120+
}
121+
122+
const activeTodos = todos.filter((todo) => !todo.completed)
123+
const completedTodos = todos.filter((todo) => todo.completed)
124+
125+
return (
126+
<div
127+
className="min-h-screen flex items-start justify-center overflow-auto py-8"
128+
style={{ backgroundColor }}
129+
>
130+
<div style={{ width: 550 }} className="mx-auto relative">
131+
<div className="text-center mb-8">
132+
<h1
133+
className="text-[70px] font-bold mb-4"
134+
style={{ color: titleColor }}
135+
>
136+
{title}
137+
</h1>
138+
139+
{/* Navigation */}
140+
<div className="flex justify-center gap-4 mb-4">
141+
<Link
142+
to="/"
143+
className="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600 transition-colors"
144+
>
145+
← Home
146+
</Link>
147+
<Link
148+
to="/query"
149+
className="px-4 py-2 bg-green-700 text-white rounded hover:bg-green-800 transition-colors"
150+
>
151+
Query
152+
</Link>
153+
<Link
154+
to="/electric"
155+
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
156+
>
157+
Electric
158+
</Link>
159+
</div>
160+
</div>
161+
162+
<div className="mb-4 flex justify-end">
163+
<div className="flex items-center">
164+
<label
165+
htmlFor="colorPicker"
166+
className="mr-2 text-sm font-medium text-gray-700"
167+
style={{ color: titleColor }}
168+
>
169+
Background Color:
170+
</label>
171+
<input
172+
type="color"
173+
id="colorPicker"
174+
value={backgroundColor}
175+
onChange={handleColorChange}
176+
className="cursor-pointer border border-gray-300 rounded"
177+
/>
178+
</div>
179+
</div>
180+
181+
<div className="bg-white shadow-[0_2px_4px_0_rgba(0,0,0,0.2),0_25px_50px_0_rgba(0,0,0,0.1)] relative">
182+
<form onSubmit={handleSubmit} className="relative">
183+
{todos.length > 0 && (
184+
<button
185+
type="button"
186+
className="absolute left-0 w-12 h-full text-[30px] text-[#e6e6e6] hover:text-[#4d4d4d]"
187+
onClick={() => {
188+
const allCompleted = completedTodos.length === todos.length
189+
const todosToToggle = allCompleted
190+
? completedTodos
191+
: activeTodos
192+
todoCollection.update(
193+
todosToToggle.map((todo) => todo.id),
194+
(drafts) => {
195+
drafts.forEach(
196+
(draft) => (draft.completed = !allCompleted)
197+
)
198+
}
199+
)
200+
}}
201+
>
202+
203+
</button>
204+
)}
205+
<input
206+
type="text"
207+
value={newTodo}
208+
onChange={(e) => setNewTodo(e.target.value)}
209+
placeholder="What needs to be done?"
210+
className="w-full py-4 pl-[60px] pr-4 text-2xl font-light border-none shadow-[inset_0_-2px_1px_rgba(0,0,0,0.03)] box-border"
211+
style={{
212+
background: `rgba(0, 0, 0, 0.003)`,
213+
}}
214+
/>
215+
</form>
216+
217+
{todos.length > 0 && (
218+
<>
219+
<ul className="my-0 mx-0 p-0 list-none">
220+
{todos.map((todo) => (
221+
<li
222+
key={`todo-${todo.id}`}
223+
className="relative border-b border-[#ededed] last:border-none group"
224+
>
225+
<div className="flex items-center h-[58px] pl-[60px]">
226+
<input
227+
type="checkbox"
228+
checked={todo.completed}
229+
onChange={() =>
230+
todoCollection.update(todo.id, (draft) => {
231+
draft.completed = !draft.completed
232+
})
233+
}
234+
className="absolute left-[12px] top-0 bottom-0 my-auto h-[40px] w-[40px] cursor-pointer"
235+
/>
236+
<label
237+
className={`block leading-[1.2] py-[15px] px-[15px] text-2xl transition-colors ${
238+
todo.completed ? `text-[#d9d9d9] line-through` : ``
239+
}`}
240+
>
241+
{todo.text}
242+
</label>
243+
<button
244+
onClick={() => {
245+
todoCollection.delete(todo.id)
246+
}}
247+
className="hidden group-hover:block absolute right-[10px] w-[40px] h-[40px] my-auto top-0 bottom-0 text-[30px] text-[#cc9a9a] hover:text-[#af5b5e] transition-colors"
248+
>
249+
×
250+
</button>
251+
</div>
252+
</li>
253+
))}
254+
</ul>
255+
256+
<footer className="text-[14px] text-[#777] py-[10px] px-[15px] h-[40px] relative border-t border-[#e6e6e6] flex justify-between items-center">
257+
<span className="text-[inherit]">
258+
{activeTodos.length}
259+
{` `}
260+
{activeTodos.length === 1 ? `item` : `items`} left
261+
</span>
262+
{completedTodos.length > 0 && (
263+
<button
264+
onClick={() => {
265+
todoCollection.delete(
266+
completedTodos.map((todo) => todo.id)
267+
)
268+
}}
269+
className="text-inherit hover:underline"
270+
>
271+
Clear completed
272+
</button>
273+
)}
274+
</footer>
275+
</>
276+
)}
277+
</div>
278+
</div>
279+
</div>
280+
)
281+
}

0 commit comments

Comments
 (0)