@korajs/cli 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/dist/bin.cjs +1636 -601
  2. package/dist/bin.cjs.map +1 -1
  3. package/dist/bin.js +1086 -139
  4. package/dist/bin.js.map +1 -1
  5. package/dist/{chunk-MVP5PDT4.js → chunk-EEZNRI5W.js} +51 -6
  6. package/dist/chunk-EEZNRI5W.js.map +1 -0
  7. package/dist/{chunk-YGVO4POI.js → chunk-Q2FBCOQD.js} +27 -15
  8. package/dist/chunk-Q2FBCOQD.js.map +1 -0
  9. package/dist/{chunk-CMSX76KM.js → chunk-VLTPEATY.js} +172 -99
  10. package/dist/chunk-VLTPEATY.js.map +1 -0
  11. package/dist/create.cjs +130 -54
  12. package/dist/create.cjs.map +1 -1
  13. package/dist/create.js +2 -2
  14. package/dist/index.cjs +143 -62
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +8 -6
  17. package/dist/index.d.ts +8 -6
  18. package/dist/index.js +2 -2
  19. package/package.json +9 -3
  20. package/templates/react-basic/README.md.hbs +10 -1
  21. package/templates/react-basic/src/App.tsx +112 -126
  22. package/templates/react-basic/src/index.css +165 -159
  23. package/templates/react-basic/src/kora-worker.ts +0 -1
  24. package/templates/react-basic/src/main.tsx +12 -12
  25. package/templates/react-basic/src/modules/todos/todo.mutations.ts +25 -0
  26. package/templates/react-basic/src/modules/todos/todo.queries.ts +5 -0
  27. package/templates/react-basic/src/modules/todos/todo.schema.ts +10 -0
  28. package/templates/react-basic/src/modules/todos/useTodos.ts +31 -0
  29. package/templates/react-basic/src/schema.ts +6 -12
  30. package/templates/react-basic/src/vite-env.d.ts +17 -0
  31. package/templates/react-basic/{tsconfig.json → tsconfig.json.hbs} +1 -2
  32. package/templates/react-basic/vite.config.ts +57 -50
  33. package/templates/react-sync/.env.example +19 -0
  34. package/templates/react-sync/README.md.hbs +43 -1
  35. package/templates/react-sync/package.json.hbs +3 -0
  36. package/templates/react-sync/server.ts +90 -26
  37. package/templates/react-sync/src/App.tsx +132 -130
  38. package/templates/react-sync/src/auth.ts +25 -0
  39. package/templates/react-sync/src/index.css +212 -165
  40. package/templates/react-sync/src/kora-worker.ts +0 -1
  41. package/templates/react-sync/src/main.tsx +34 -18
  42. package/templates/react-sync/src/modules/todos/todo.mutations.ts +25 -0
  43. package/templates/react-sync/src/modules/todos/todo.queries.ts +5 -0
  44. package/templates/react-sync/src/modules/todos/todo.schema.ts +10 -0
  45. package/templates/react-sync/src/modules/todos/useTodos.ts +31 -0
  46. package/templates/react-sync/src/schema.ts +6 -12
  47. package/templates/react-sync/src/vite-env.d.ts +17 -0
  48. package/templates/react-sync/{tsconfig.json → tsconfig.json.hbs} +1 -2
  49. package/templates/react-sync/vite.config.ts +67 -60
  50. package/templates/react-tailwind/README.md.hbs +10 -1
  51. package/templates/react-tailwind/src/App.tsx +186 -197
  52. package/templates/react-tailwind/src/index.css +3 -3
  53. package/templates/react-tailwind/src/kora-worker.ts +0 -1
  54. package/templates/react-tailwind/src/main.tsx +19 -12
  55. package/templates/react-tailwind/src/modules/todos/todo.mutations.ts +25 -0
  56. package/templates/react-tailwind/src/modules/todos/todo.queries.ts +5 -0
  57. package/templates/react-tailwind/src/modules/todos/todo.schema.ts +10 -0
  58. package/templates/react-tailwind/src/modules/todos/useTodos.ts +31 -0
  59. package/templates/react-tailwind/src/schema.ts +6 -12
  60. package/templates/react-tailwind/src/vite-env.d.ts +17 -0
  61. package/templates/react-tailwind/{tsconfig.json → tsconfig.json.hbs} +1 -2
  62. package/templates/react-tailwind/vite.config.ts +57 -50
  63. package/templates/react-tailwind-sync/.env.example +19 -0
  64. package/templates/react-tailwind-sync/README.md.hbs +43 -1
  65. package/templates/react-tailwind-sync/package.json.hbs +3 -0
  66. package/templates/react-tailwind-sync/server.ts +90 -26
  67. package/templates/react-tailwind-sync/src/App.tsx +246 -222
  68. package/templates/react-tailwind-sync/src/auth.ts +25 -0
  69. package/templates/react-tailwind-sync/src/index.css +3 -3
  70. package/templates/react-tailwind-sync/src/kora-worker.ts +0 -1
  71. package/templates/react-tailwind-sync/src/main.tsx +48 -18
  72. package/templates/react-tailwind-sync/src/modules/todos/todo.mutations.ts +25 -0
  73. package/templates/react-tailwind-sync/src/modules/todos/todo.queries.ts +5 -0
  74. package/templates/react-tailwind-sync/src/modules/todos/todo.schema.ts +10 -0
  75. package/templates/react-tailwind-sync/src/modules/todos/useTodos.ts +31 -0
  76. package/templates/react-tailwind-sync/src/schema.ts +6 -12
  77. package/templates/react-tailwind-sync/src/vite-env.d.ts +17 -0
  78. package/templates/react-tailwind-sync/{tsconfig.json → tsconfig.json.hbs} +1 -2
  79. package/templates/react-tailwind-sync/vite.config.ts +67 -60
  80. package/templates/svelte-basic/.env.example +3 -0
  81. package/templates/svelte-basic/README.md.hbs +62 -0
  82. package/templates/svelte-basic/index.html.hbs +12 -0
  83. package/templates/svelte-basic/kora.config.ts +12 -0
  84. package/templates/svelte-basic/package.json.hbs +27 -0
  85. package/templates/svelte-basic/src/App.svelte +131 -0
  86. package/templates/svelte-basic/src/Root.svelte +14 -0
  87. package/templates/svelte-basic/src/index.css +287 -0
  88. package/templates/svelte-basic/src/kora-worker.ts +9 -0
  89. package/templates/svelte-basic/src/main.ts +16 -0
  90. package/templates/svelte-basic/src/modules/todos/todo.mutations.ts +25 -0
  91. package/templates/svelte-basic/src/modules/todos/todo.queries.ts +5 -0
  92. package/templates/svelte-basic/src/modules/todos/todo.schema.ts +10 -0
  93. package/templates/svelte-basic/src/modules/todos/useTodos.ts +25 -0
  94. package/templates/svelte-basic/src/schema.ts +9 -0
  95. package/templates/svelte-basic/src/vite-env.d.ts +12 -0
  96. package/templates/svelte-basic/svelte.config.js +5 -0
  97. package/templates/svelte-basic/tsconfig.json.hbs +14 -0
  98. package/templates/svelte-basic/vite.config.ts +52 -0
  99. package/templates/svelte-sync/.env.example +25 -0
  100. package/templates/svelte-sync/README.md.hbs +105 -0
  101. package/templates/svelte-sync/index.html.hbs +12 -0
  102. package/templates/svelte-sync/kora.config.ts +17 -0
  103. package/templates/svelte-sync/package.json.hbs +43 -0
  104. package/templates/svelte-sync/server.ts +90 -0
  105. package/templates/svelte-sync/src/App.svelte +154 -0
  106. package/templates/svelte-sync/src/Root.svelte +22 -0
  107. package/templates/svelte-sync/src/auth.ts +25 -0
  108. package/templates/svelte-sync/src/index.css +339 -0
  109. package/templates/svelte-sync/src/kora-worker.ts +9 -0
  110. package/templates/svelte-sync/src/main.ts +32 -0
  111. package/templates/svelte-sync/src/modules/todos/todo.mutations.ts +25 -0
  112. package/templates/svelte-sync/src/modules/todos/todo.queries.ts +5 -0
  113. package/templates/svelte-sync/src/modules/todos/todo.schema.ts +10 -0
  114. package/templates/svelte-sync/src/modules/todos/useTodos.ts +25 -0
  115. package/templates/svelte-sync/src/schema.ts +9 -0
  116. package/templates/svelte-sync/src/vite-env.d.ts +21 -0
  117. package/templates/svelte-sync/svelte.config.js +5 -0
  118. package/templates/svelte-sync/tsconfig.json.hbs +14 -0
  119. package/templates/svelte-sync/vite.config.ts +79 -0
  120. package/templates/svelte-tailwind/README.md.hbs +62 -0
  121. package/templates/svelte-tailwind/index.html.hbs +12 -0
  122. package/templates/svelte-tailwind/kora.config.ts +12 -0
  123. package/templates/svelte-tailwind/package.json.hbs +30 -0
  124. package/templates/svelte-tailwind/src/App.svelte +79 -0
  125. package/templates/svelte-tailwind/src/Root.svelte +14 -0
  126. package/templates/svelte-tailwind/src/index.css +7 -0
  127. package/templates/svelte-tailwind/src/kora-worker.ts +9 -0
  128. package/templates/svelte-tailwind/src/main.ts +16 -0
  129. package/templates/svelte-tailwind/src/modules/todos/todo.mutations.ts +25 -0
  130. package/templates/svelte-tailwind/src/modules/todos/todo.queries.ts +5 -0
  131. package/templates/svelte-tailwind/src/modules/todos/todo.schema.ts +10 -0
  132. package/templates/svelte-tailwind/src/modules/todos/useTodos.ts +25 -0
  133. package/templates/svelte-tailwind/src/schema.ts +9 -0
  134. package/templates/svelte-tailwind/src/vite-env.d.ts +12 -0
  135. package/templates/svelte-tailwind/svelte.config.js +5 -0
  136. package/templates/svelte-tailwind/tsconfig.json.hbs +14 -0
  137. package/templates/svelte-tailwind/vite.config.ts +53 -0
  138. package/templates/svelte-tailwind-sync/.env.example +25 -0
  139. package/templates/svelte-tailwind-sync/README.md.hbs +105 -0
  140. package/templates/svelte-tailwind-sync/index.html.hbs +12 -0
  141. package/templates/svelte-tailwind-sync/kora.config.ts +17 -0
  142. package/templates/svelte-tailwind-sync/package.json.hbs +46 -0
  143. package/templates/svelte-tailwind-sync/server.ts +90 -0
  144. package/templates/svelte-tailwind-sync/src/App.svelte +196 -0
  145. package/templates/svelte-tailwind-sync/src/Root.svelte +26 -0
  146. package/templates/svelte-tailwind-sync/src/auth.ts +25 -0
  147. package/templates/svelte-tailwind-sync/src/index.css +7 -0
  148. package/templates/svelte-tailwind-sync/src/kora-worker.ts +9 -0
  149. package/templates/svelte-tailwind-sync/src/main.ts +32 -0
  150. package/templates/svelte-tailwind-sync/src/modules/todos/todo.mutations.ts +25 -0
  151. package/templates/svelte-tailwind-sync/src/modules/todos/todo.queries.ts +5 -0
  152. package/templates/svelte-tailwind-sync/src/modules/todos/todo.schema.ts +10 -0
  153. package/templates/svelte-tailwind-sync/src/modules/todos/useTodos.ts +25 -0
  154. package/templates/svelte-tailwind-sync/src/schema.ts +9 -0
  155. package/templates/svelte-tailwind-sync/src/vite-env.d.ts +21 -0
  156. package/templates/svelte-tailwind-sync/svelte.config.js +5 -0
  157. package/templates/svelte-tailwind-sync/tsconfig.json.hbs +14 -0
  158. package/templates/svelte-tailwind-sync/vite.config.ts +80 -0
  159. package/templates/tauri-react/.env.example +20 -0
  160. package/templates/tauri-react/README.md.hbs +36 -1
  161. package/templates/tauri-react/dev.ts +11 -11
  162. package/templates/tauri-react/package.json.hbs +2 -0
  163. package/templates/tauri-react/server.ts +86 -12
  164. package/templates/tauri-react/src/App.tsx +769 -544
  165. package/templates/tauri-react/src/AppShell.tsx +125 -83
  166. package/templates/tauri-react/src/SetupScreen.tsx +257 -208
  167. package/templates/tauri-react/src/auth.ts +45 -0
  168. package/templates/tauri-react/src/main.tsx +3 -3
  169. package/templates/tauri-react/src/modules/todos/todo.mutations.ts +25 -0
  170. package/templates/tauri-react/src/modules/todos/todo.queries.ts +5 -0
  171. package/templates/tauri-react/src/modules/todos/todo.schema.ts +10 -0
  172. package/templates/tauri-react/src/modules/todos/useTodos.ts +31 -0
  173. package/templates/tauri-react/src/schema.ts +6 -12
  174. package/templates/tauri-react/src/sync-config.ts +47 -47
  175. package/templates/tauri-react/src/updater.ts +15 -15
  176. package/templates/tauri-react/src/vite-env.d.ts +17 -0
  177. package/templates/tauri-react/src-tauri/tauri.conf.json +1 -1
  178. package/templates/tauri-react/tsconfig.json.hbs +14 -0
  179. package/templates/tauri-react/vite.config.ts +9 -9
  180. package/templates/vue-basic/README.md.hbs +62 -0
  181. package/templates/vue-basic/index.html.hbs +12 -0
  182. package/templates/vue-basic/kora.config.ts +12 -0
  183. package/templates/vue-basic/package.json.hbs +26 -0
  184. package/templates/vue-basic/src/App.vue +117 -0
  185. package/templates/vue-basic/src/index.css +287 -0
  186. package/templates/vue-basic/src/kora-worker.ts +9 -0
  187. package/templates/vue-basic/src/main.ts +20 -0
  188. package/templates/vue-basic/src/modules/todos/todo.mutations.ts +25 -0
  189. package/templates/vue-basic/src/modules/todos/todo.queries.ts +5 -0
  190. package/templates/vue-basic/src/modules/todos/todo.schema.ts +10 -0
  191. package/templates/vue-basic/src/modules/todos/useTodos.ts +32 -0
  192. package/templates/vue-basic/src/schema.ts +9 -0
  193. package/templates/vue-basic/src/vite-env.d.ts +17 -0
  194. package/templates/vue-basic/tsconfig.json.hbs +15 -0
  195. package/templates/vue-basic/vite.config.ts +69 -0
  196. package/templates/vue-sync/.env.example +25 -0
  197. package/templates/vue-sync/README.md.hbs +105 -0
  198. package/templates/vue-sync/index.html.hbs +12 -0
  199. package/templates/vue-sync/kora.config.ts +17 -0
  200. package/templates/vue-sync/package.json.hbs +42 -0
  201. package/templates/vue-sync/server.ts +90 -0
  202. package/templates/vue-sync/src/App.vue +135 -0
  203. package/templates/vue-sync/src/auth.ts +25 -0
  204. package/templates/vue-sync/src/index.css +339 -0
  205. package/templates/vue-sync/src/kora-worker.ts +9 -0
  206. package/templates/vue-sync/src/main.ts +43 -0
  207. package/templates/vue-sync/src/modules/todos/todo.mutations.ts +25 -0
  208. package/templates/vue-sync/src/modules/todos/todo.queries.ts +5 -0
  209. package/templates/vue-sync/src/modules/todos/todo.schema.ts +10 -0
  210. package/templates/vue-sync/src/modules/todos/useTodos.ts +32 -0
  211. package/templates/vue-sync/src/schema.ts +9 -0
  212. package/templates/vue-sync/src/vite-env.d.ts +26 -0
  213. package/templates/vue-sync/tsconfig.json.hbs +15 -0
  214. package/templates/vue-sync/vite.config.ts +79 -0
  215. package/templates/vue-tailwind/README.md.hbs +62 -0
  216. package/templates/vue-tailwind/index.html.hbs +12 -0
  217. package/templates/vue-tailwind/kora.config.ts +12 -0
  218. package/templates/vue-tailwind/package.json.hbs +29 -0
  219. package/templates/vue-tailwind/src/App.vue +142 -0
  220. package/templates/vue-tailwind/src/index.css +7 -0
  221. package/templates/vue-tailwind/src/kora-worker.ts +9 -0
  222. package/templates/vue-tailwind/src/main.ts +31 -0
  223. package/templates/vue-tailwind/src/modules/todos/todo.mutations.ts +25 -0
  224. package/templates/vue-tailwind/src/modules/todos/todo.queries.ts +5 -0
  225. package/templates/vue-tailwind/src/modules/todos/todo.schema.ts +10 -0
  226. package/templates/vue-tailwind/src/modules/todos/useTodos.ts +32 -0
  227. package/templates/vue-tailwind/src/schema.ts +9 -0
  228. package/templates/vue-tailwind/src/vite-env.d.ts +17 -0
  229. package/templates/vue-tailwind/tsconfig.json.hbs +15 -0
  230. package/templates/vue-tailwind/vite.config.ts +70 -0
  231. package/templates/vue-tailwind-sync/.env.example +25 -0
  232. package/templates/vue-tailwind-sync/README.md.hbs +105 -0
  233. package/templates/vue-tailwind-sync/index.html.hbs +12 -0
  234. package/templates/vue-tailwind-sync/kora.config.ts +17 -0
  235. package/templates/vue-tailwind-sync/package.json.hbs +45 -0
  236. package/templates/vue-tailwind-sync/server.ts +90 -0
  237. package/templates/vue-tailwind-sync/src/App.vue +245 -0
  238. package/templates/vue-tailwind-sync/src/auth.ts +25 -0
  239. package/templates/vue-tailwind-sync/src/index.css +7 -0
  240. package/templates/vue-tailwind-sync/src/kora-worker.ts +9 -0
  241. package/templates/vue-tailwind-sync/src/main.ts +56 -0
  242. package/templates/vue-tailwind-sync/src/modules/todos/todo.mutations.ts +25 -0
  243. package/templates/vue-tailwind-sync/src/modules/todos/todo.queries.ts +5 -0
  244. package/templates/vue-tailwind-sync/src/modules/todos/todo.schema.ts +10 -0
  245. package/templates/vue-tailwind-sync/src/modules/todos/useTodos.ts +32 -0
  246. package/templates/vue-tailwind-sync/src/schema.ts +9 -0
  247. package/templates/vue-tailwind-sync/src/vite-env.d.ts +26 -0
  248. package/templates/vue-tailwind-sync/tsconfig.json.hbs +15 -0
  249. package/templates/vue-tailwind-sync/vite.config.ts +80 -0
  250. package/dist/chunk-CMSX76KM.js.map +0 -1
  251. package/dist/chunk-MVP5PDT4.js.map +0 -1
  252. package/dist/chunk-YGVO4POI.js.map +0 -1
  253. package/templates/tauri-react/tsconfig.json +0 -15
@@ -1,241 +1,265 @@
1
- import { useState } from 'react'
2
- import { useQuery, useMutation, useSyncStatus, useCollection } from '@korajs/react'
1
+ import { useAuth } from '@korajs/auth/react'
2
+ import { useSyncStatus } from '@korajs/react'
3
3
  import {
4
- CheckCircle2,
5
- Circle,
6
- ClipboardList,
7
- Loader2,
8
- Plus,
9
- Trash2,
10
- Wifi,
11
- WifiOff,
12
- AlertCircle,
4
+ AlertCircle,
5
+ CheckCircle2,
6
+ Circle,
7
+ ClipboardList,
8
+ Loader2,
9
+ Plus,
10
+ Trash2,
11
+ Wifi,
12
+ WifiOff,
13
13
  } from 'lucide-react'
14
+ import { useState } from 'react'
15
+ import { useTodos } from './modules/todos/useTodos'
14
16
 
15
17
  type Filter = 'all' | 'active' | 'completed'
16
18
 
17
19
  export function App() {
18
- const todos = useCollection('todos')
19
- const allTodos = useQuery(todos.where({}).orderBy('createdAt', 'desc'))
20
- const { mutate: addTodo, isLoading: isAdding } = useMutation(
21
- (data: { title: string }) => todos.insert(data)
22
- )
23
- const { mutate: toggleTodo } = useMutation(
24
- (id: string, data: { completed: boolean }) => todos.update(id, data)
25
- )
26
- const { mutate: deleteTodo } = useMutation(
27
- (id: string) => todos.delete(id)
28
- )
29
- const status = useSyncStatus()
30
-
31
- const [filter, setFilter] = useState<Filter>('all')
32
- const [input, setInput] = useState('')
33
-
34
- const activeTodos = allTodos.filter((t) => !t.completed)
35
- const completedTodos = allTodos.filter((t) => !!t.completed)
36
- const filteredTodos =
37
- filter === 'active' ? activeTodos : filter === 'completed' ? completedTodos : allTodos
38
-
39
- const handleSubmit = (e: React.FormEvent) => {
40
- e.preventDefault()
41
- const title = input.trim()
42
- if (title) {
43
- addTodo({ title })
44
- setInput('')
45
- }
46
- }
47
-
48
- const clearCompleted = () => {
49
- for (const todo of completedTodos) {
50
- deleteTodo(todo.id)
51
- }
52
- }
53
-
54
- return (
55
- <div className="min-h-screen bg-gray-950 text-gray-100">
56
- <div className="mx-auto max-w-2xl px-4 py-12">
57
- {/* Header */}
58
- <div className="flex items-center justify-between mb-8">
59
- <div className="flex items-center gap-3">
60
- <ClipboardList className="h-8 w-8 text-indigo-400" />
61
- <h1 className="text-2xl font-bold">My Tasks</h1>
62
- </div>
63
- <SyncBadge status={status} />
64
- </div>
65
-
66
- {/* Stats */}
67
- <div className="grid grid-cols-3 gap-4 mb-8">
68
- <StatCard label="Total" value={allTodos.length} color="text-gray-300" />
69
- <StatCard label="Remaining" value={activeTodos.length} color="text-amber-400" />
70
- <StatCard label="Done" value={completedTodos.length} color="text-emerald-400" />
71
- </div>
72
-
73
- {/* Add form */}
74
- <form onSubmit={handleSubmit} className="flex gap-3 mb-8">
75
- <input
76
- type="text"
77
- value={input}
78
- onChange={(e) => setInput(e.target.value)}
79
- placeholder="What needs to be done?"
80
- className="flex-1 rounded-lg border border-gray-700 bg-gray-900 px-4 py-3 text-gray-100 placeholder-gray-500 outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500"
81
- />
82
- <button
83
- type="submit"
84
- disabled={isAdding || !input.trim()}
85
- className="flex items-center gap-2 rounded-lg bg-indigo-600 px-5 py-3 font-medium text-white transition hover:bg-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
86
- >
87
- {isAdding ? <Loader2 className="h-4 w-4 animate-spin" /> : <Plus className="h-4 w-4" />}
88
- Add
89
- </button>
90
- </form>
91
-
92
- {/* Filter tabs */}
93
- <div className="flex gap-2 mb-6">
94
- {(['all', 'active', 'completed'] as const).map((f) => {
95
- const count = f === 'all' ? allTodos.length : f === 'active' ? activeTodos.length : completedTodos.length
96
- return (
97
- <button
98
- key={f}
99
- onClick={() => setFilter(f)}
100
- className={`flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition ${
101
- filter === f
102
- ? 'bg-indigo-600 text-white'
103
- : 'bg-gray-800 text-gray-400 hover:bg-gray-700 hover:text-gray-200'
104
- }`}
105
- >
106
- {f.charAt(0).toUpperCase() + f.slice(1)}
107
- <span
108
- className={`rounded-full px-2 py-0.5 text-xs ${
109
- filter === f ? 'bg-indigo-500 text-white' : 'bg-gray-700 text-gray-400'
110
- }`}
111
- >
112
- {count}
113
- </span>
114
- </button>
115
- )
116
- })}
117
- </div>
118
-
119
- {/* Todo list */}
120
- <div className="space-y-2">
121
- {filteredTodos.length === 0 ? (
122
- <EmptyState filter={filter} />
123
- ) : (
124
- filteredTodos.map((todo) => (
125
- <div
126
- key={todo.id}
127
- className="group flex items-center gap-3 rounded-lg border border-gray-800 bg-gray-900 px-4 py-3 transition hover:border-gray-700"
128
- >
129
- <button
130
- onClick={() => toggleTodo(todo.id, { completed: !todo.completed })}
131
- className="shrink-0 text-gray-500 hover:text-indigo-400 transition"
132
- >
133
- {todo.completed ? (
134
- <CheckCircle2 className="h-5 w-5 text-emerald-400" />
135
- ) : (
136
- <Circle className="h-5 w-5" />
137
- )}
138
- </button>
139
- <span
140
- className={`flex-1 ${
141
- todo.completed ? 'text-gray-500 line-through' : 'text-gray-100'
142
- }`}
143
- >
144
- {String(todo.title)}
145
- </span>
146
- {todo.createdAt && (
147
- <span className="text-xs text-gray-600">
148
- {formatTime(Number(todo.createdAt))}
149
- </span>
150
- )}
151
- <button
152
- onClick={() => deleteTodo(todo.id)}
153
- className="shrink-0 text-gray-600 opacity-0 transition hover:text-red-400 group-hover:opacity-100"
154
- >
155
- <Trash2 className="h-4 w-4" />
156
- </button>
157
- </div>
158
- ))
159
- )}
160
- </div>
161
-
162
- {/* Footer */}
163
- {allTodos.length > 0 && (
164
- <div className="mt-6 flex items-center justify-between text-sm text-gray-500">
165
- <span>{activeTodos.length} item{activeTodos.length !== 1 ? 's' : ''} left</span>
166
- {completedTodos.length > 0 && (
167
- <button
168
- onClick={clearCompleted}
169
- className="text-gray-500 transition hover:text-gray-300"
170
- >
171
- Clear completed
172
- </button>
173
- )}
174
- </div>
175
- )}
176
-
177
- <p className="mt-12 text-center text-xs text-gray-700">
178
- Powered by Kora &mdash; offline-first, real-time sync
179
- </p>
180
- </div>
181
- </div>
182
- )
20
+ const { allTodos, activeTodos, completedTodos, addTodo, toggleTodo, deleteTodo } = useTodos()
21
+ const isAdding = addTodo.isLoading
22
+ const status = useSyncStatus()
23
+ const { user, isAuthenticated, signInWithOAuth, signOut, error } = useAuth()
24
+
25
+ const [filter, setFilter] = useState<Filter>('all')
26
+ const [input, setInput] = useState('')
27
+
28
+ const filteredTodos =
29
+ filter === 'active' ? activeTodos : filter === 'completed' ? completedTodos : allTodos
30
+
31
+ const handleSubmit = (e: React.FormEvent) => {
32
+ e.preventDefault()
33
+ const title = input.trim()
34
+ if (title) {
35
+ addTodo.mutate({ title })
36
+ setInput('')
37
+ }
38
+ }
39
+
40
+ const clearCompleted = () => {
41
+ for (const todo of completedTodos) {
42
+ deleteTodo.mutate(todo.id)
43
+ }
44
+ }
45
+
46
+ return (
47
+ <div className="min-h-screen bg-gray-950 text-gray-100">
48
+ <div className="mx-auto max-w-2xl px-4 py-12">
49
+ {/* Header */}
50
+ <div className="flex items-center justify-between mb-8">
51
+ <div className="flex items-center gap-3">
52
+ <ClipboardList className="h-8 w-8 text-indigo-400" />
53
+ <h1 className="text-2xl font-bold">My Tasks</h1>
54
+ </div>
55
+ <div className="flex items-center gap-2">
56
+ <SyncBadge status={status} />
57
+ {isAuthenticated ? (
58
+ <button
59
+ type="button"
60
+ onClick={() => signOut()}
61
+ className="max-w-44 truncate rounded-full border border-gray-700 bg-gray-900 px-3 py-1.5 text-sm text-gray-300 transition hover:border-gray-600"
62
+ >
63
+ {user?.email || 'Sign out'}
64
+ </button>
65
+ ) : (
66
+ <button
67
+ type="button"
68
+ onClick={() => signInWithOAuth('google')}
69
+ className="rounded-full border border-gray-700 bg-gray-900 px-3 py-1.5 text-sm text-gray-300 transition hover:border-gray-600"
70
+ >
71
+ Sign in
72
+ </button>
73
+ )}
74
+ </div>
75
+ </div>
76
+
77
+ {error && (
78
+ <div className="mb-4 rounded-lg border border-red-400/30 px-3 py-2 text-sm text-red-300">
79
+ {error}
80
+ </div>
81
+ )}
82
+
83
+ {/* Stats */}
84
+ <div className="grid grid-cols-3 gap-4 mb-8">
85
+ <StatCard label="Total" value={allTodos.length} color="text-gray-300" />
86
+ <StatCard label="Remaining" value={activeTodos.length} color="text-amber-400" />
87
+ <StatCard label="Done" value={completedTodos.length} color="text-emerald-400" />
88
+ </div>
89
+
90
+ {/* Add form */}
91
+ <form onSubmit={handleSubmit} className="flex gap-3 mb-8">
92
+ <input
93
+ type="text"
94
+ value={input}
95
+ onChange={(e) => setInput(e.target.value)}
96
+ placeholder="What needs to be done?"
97
+ className="flex-1 rounded-lg border border-gray-700 bg-gray-900 px-4 py-3 text-gray-100 placeholder-gray-500 outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500"
98
+ />
99
+ <button
100
+ type="submit"
101
+ disabled={isAdding || !input.trim()}
102
+ className="flex items-center gap-2 rounded-lg bg-indigo-600 px-5 py-3 font-medium text-white transition hover:bg-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
103
+ >
104
+ {isAdding ? <Loader2 className="h-4 w-4 animate-spin" /> : <Plus className="h-4 w-4" />}
105
+ Add
106
+ </button>
107
+ </form>
108
+
109
+ {/* Filter tabs */}
110
+ <div className="flex gap-2 mb-6">
111
+ {(['all', 'active', 'completed'] as const).map((f) => {
112
+ const count =
113
+ f === 'all'
114
+ ? allTodos.length
115
+ : f === 'active'
116
+ ? activeTodos.length
117
+ : completedTodos.length
118
+ return (
119
+ <button
120
+ key={f}
121
+ onClick={() => setFilter(f)}
122
+ className={`flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition ${
123
+ filter === f
124
+ ? 'bg-indigo-600 text-white'
125
+ : 'bg-gray-800 text-gray-400 hover:bg-gray-700 hover:text-gray-200'
126
+ }`}
127
+ >
128
+ {f.charAt(0).toUpperCase() + f.slice(1)}
129
+ <span
130
+ className={`rounded-full px-2 py-0.5 text-xs ${
131
+ filter === f ? 'bg-indigo-500 text-white' : 'bg-gray-700 text-gray-400'
132
+ }`}
133
+ >
134
+ {count}
135
+ </span>
136
+ </button>
137
+ )
138
+ })}
139
+ </div>
140
+
141
+ {/* Todo list */}
142
+ <div className="space-y-2">
143
+ {filteredTodos.length === 0 ? (
144
+ <EmptyState filter={filter} />
145
+ ) : (
146
+ filteredTodos.map((todo) => (
147
+ <div
148
+ key={todo.id}
149
+ className="group flex items-center gap-3 rounded-lg border border-gray-800 bg-gray-900 px-4 py-3 transition hover:border-gray-700"
150
+ >
151
+ <button
152
+ onClick={() => toggleTodo.mutate(todo.id, { completed: !todo.completed })}
153
+ className="shrink-0 text-gray-500 hover:text-indigo-400 transition"
154
+ >
155
+ {todo.completed ? (
156
+ <CheckCircle2 className="h-5 w-5 text-emerald-400" />
157
+ ) : (
158
+ <Circle className="h-5 w-5" />
159
+ )}
160
+ </button>
161
+ <span
162
+ className={`flex-1 ${
163
+ todo.completed ? 'text-gray-500 line-through' : 'text-gray-100'
164
+ }`}
165
+ >
166
+ {String(todo.title)}
167
+ </span>
168
+ {todo.createdAt && (
169
+ <span className="text-xs text-gray-600">
170
+ {formatTime(Number(todo.createdAt))}
171
+ </span>
172
+ )}
173
+ <button
174
+ onClick={() => deleteTodo.mutate(todo.id)}
175
+ className="shrink-0 text-gray-600 opacity-0 transition hover:text-red-400 group-hover:opacity-100"
176
+ >
177
+ <Trash2 className="h-4 w-4" />
178
+ </button>
179
+ </div>
180
+ ))
181
+ )}
182
+ </div>
183
+
184
+ {/* Footer */}
185
+ {allTodos.length > 0 && (
186
+ <div className="mt-6 flex items-center justify-between text-sm text-gray-500">
187
+ <span>
188
+ {activeTodos.length} item{activeTodos.length !== 1 ? 's' : ''} left
189
+ </span>
190
+ {completedTodos.length > 0 && (
191
+ <button
192
+ onClick={clearCompleted}
193
+ className="text-gray-500 transition hover:text-gray-300"
194
+ >
195
+ Clear completed
196
+ </button>
197
+ )}
198
+ </div>
199
+ )}
200
+
201
+ <p className="mt-12 text-center text-xs text-gray-700">
202
+ Powered by Kora &mdash; offline-first, real-time sync
203
+ </p>
204
+ </div>
205
+ </div>
206
+ )
183
207
  }
184
208
 
185
209
  function SyncBadge({ status }: { status: { status: string; pendingOperations?: number } }) {
186
- const s = status.status
187
- const pending = status.pendingOperations ?? 0
188
-
189
- const config: Record<string, { icon: typeof Wifi; color: string; label: string }> = {
190
- connected: { icon: Wifi, color: 'text-emerald-400', label: 'Connected' },
191
- syncing: { icon: Wifi, color: 'text-amber-400', label: 'Syncing' },
192
- synced: { icon: Wifi, color: 'text-emerald-400', label: 'Synced' },
193
- offline: { icon: WifiOff, color: 'text-gray-500', label: 'Offline' },
194
- error: { icon: AlertCircle, color: 'text-red-400', label: 'Error' },
195
- }
196
-
197
- const { icon: Icon, color, label } = config[s] ?? config.offline!
198
-
199
- return (
200
- <div className="flex items-center gap-2 rounded-full bg-gray-800 px-3 py-1.5 text-sm">
201
- <Icon className={`h-4 w-4 ${color}`} />
202
- <span className={color}>{label}</span>
203
- {pending > 0 && (
204
- <span className="rounded-full bg-gray-700 px-2 py-0.5 text-xs text-gray-400">
205
- {pending} pending
206
- </span>
207
- )}
208
- </div>
209
- )
210
+ const s = status.status
211
+ const pending = status.pendingOperations ?? 0
212
+
213
+ const config: Record<string, { icon: typeof Wifi; color: string; label: string }> = {
214
+ connected: { icon: Wifi, color: 'text-emerald-400', label: 'Connected' },
215
+ syncing: { icon: Wifi, color: 'text-amber-400', label: 'Syncing' },
216
+ synced: { icon: Wifi, color: 'text-emerald-400', label: 'Synced' },
217
+ offline: { icon: WifiOff, color: 'text-gray-500', label: 'Offline' },
218
+ error: { icon: AlertCircle, color: 'text-red-400', label: 'Error' },
219
+ }
220
+
221
+ const { icon: Icon, color, label } = config[s] ?? config.offline!
222
+
223
+ return (
224
+ <div className="flex items-center gap-2 rounded-full bg-gray-800 px-3 py-1.5 text-sm">
225
+ <Icon className={`h-4 w-4 ${color}`} />
226
+ <span className={color}>{label}</span>
227
+ {pending > 0 && (
228
+ <span className="rounded-full bg-gray-700 px-2 py-0.5 text-xs text-gray-400">
229
+ {pending} pending
230
+ </span>
231
+ )}
232
+ </div>
233
+ )
210
234
  }
211
235
 
212
236
  function StatCard({ label, value, color }: { label: string; value: number; color: string }) {
213
- return (
214
- <div className="rounded-lg border border-gray-800 bg-gray-900 p-4">
215
- <p className="text-sm text-gray-500">{label}</p>
216
- <p className={`text-2xl font-bold ${color}`}>{value}</p>
217
- </div>
218
- )
237
+ return (
238
+ <div className="rounded-lg border border-gray-800 bg-gray-900 p-4">
239
+ <p className="text-sm text-gray-500">{label}</p>
240
+ <p className={`text-2xl font-bold ${color}`}>{value}</p>
241
+ </div>
242
+ )
219
243
  }
220
244
 
221
245
  function EmptyState({ filter }: { filter: Filter }) {
222
- const messages: Record<Filter, string> = {
223
- all: 'No tasks yet. Add one above!',
224
- active: 'All caught up! No active tasks.',
225
- completed: 'No completed tasks yet.',
226
- }
227
- return (
228
- <div className="rounded-lg border border-dashed border-gray-800 py-12 text-center text-gray-600">
229
- {messages[filter]}
230
- </div>
231
- )
246
+ const messages: Record<Filter, string> = {
247
+ all: 'No tasks yet. Add one above!',
248
+ active: 'All caught up! No active tasks.',
249
+ completed: 'No completed tasks yet.',
250
+ }
251
+ return (
252
+ <div className="rounded-lg border border-dashed border-gray-800 py-12 text-center text-gray-600">
253
+ {messages[filter]}
254
+ </div>
255
+ )
232
256
  }
233
257
 
234
258
  function formatTime(timestamp: number): string {
235
- const date = new Date(timestamp)
236
- const now = new Date()
237
- if (date.toDateString() === now.toDateString()) {
238
- return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
239
- }
240
- return date.toLocaleDateString([], { month: 'short', day: 'numeric' })
259
+ const date = new Date(timestamp)
260
+ const now = new Date()
261
+ if (date.toDateString() === now.toDateString()) {
262
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
263
+ }
264
+ return date.toLocaleDateString([], { month: 'short', day: 'numeric' })
241
265
  }
@@ -0,0 +1,25 @@
1
+ import { createKoraAuth } from '@korajs/auth'
2
+
3
+ export const authServerUrl =
4
+ import.meta.env.VITE_AUTH_URL || `${window.location.protocol}//${window.location.host}`
5
+
6
+ export const authClient = createKoraAuth({
7
+ serverUrl: authServerUrl,
8
+ })
9
+
10
+ export async function completeOAuthCallbackFromLocation(): Promise<void> {
11
+ const url = new URL(window.location.href)
12
+ const code = url.searchParams.get('code')
13
+ const state = url.searchParams.get('state')
14
+ const provider = url.searchParams.get('provider') || 'google'
15
+
16
+ if (!code || !state) {
17
+ return
18
+ }
19
+
20
+ await authClient.completeOAuthSignIn(provider, { code, state })
21
+ url.searchParams.delete('code')
22
+ url.searchParams.delete('state')
23
+ url.searchParams.delete('provider')
24
+ window.history.replaceState({}, document.title, `${url.pathname}${url.search}${url.hash}`)
25
+ }
@@ -1,7 +1,7 @@
1
1
  @import "tailwindcss";
2
2
 
3
3
  @layer base {
4
- body {
5
- @apply bg-gray-950 text-gray-100 antialiased;
6
- }
4
+ body {
5
+ @apply bg-gray-950 text-gray-100 antialiased;
6
+ }
7
7
  }
@@ -4,7 +4,6 @@
4
4
  // Import the WASM binary URL so Vite resolves it with the correct content hash.
5
5
  // Without this, production builds fail because sqlite3 looks for the unhashed filename.
6
6
  import sqliteWasmUrl from '@sqlite.org/sqlite-wasm/sqlite3.wasm?url'
7
-
8
7
  ;(globalThis as Record<string, unknown>).__KORA_SQLITE_WASM_URL = sqliteWasmUrl
9
8
 
10
9
  import '@korajs/store/sqlite-wasm/worker'
@@ -1,36 +1,66 @@
1
- import { createApp } from 'korajs'
1
+ import { createKoraAuthSync } from '@korajs/auth'
2
+ import { AuthProvider } from '@korajs/auth/react'
2
3
  import { KoraProvider } from '@korajs/react'
4
+ import { createApp } from 'korajs'
3
5
  import { StrictMode } from 'react'
4
6
  import { createRoot } from 'react-dom/client'
5
- import schema from './schema'
6
7
  import { App } from './App'
8
+ import { authClient, completeOAuthCallbackFromLocation } from './auth'
9
+ import schema from './schema'
7
10
  import './index.css'
8
11
  import koraWorkerUrl from './kora-worker.ts?worker&url'
9
12
 
10
13
  // Build sync URL: use env var if set, otherwise derive from current page host.
11
14
  // This allows the Vite proxy (/kora-sync → ws://localhost:3001) to work in dev,
12
15
  // and also works through any tunnel (ngrok, cloudflared) without extra configuration.
13
- const syncUrl = import.meta.env.VITE_SYNC_URL
14
- || `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/kora-sync`
16
+ const syncUrl =
17
+ import.meta.env.VITE_SYNC_URL ||
18
+ `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/kora-sync`
15
19
 
16
20
  const app = createApp({
17
- schema,
18
- sync: {
19
- url: syncUrl,
20
- },
21
- store: {
22
- workerUrl: koraWorkerUrl,
23
- },
24
- devtools: true,
21
+ schema,
22
+ sync: {
23
+ url: syncUrl,
24
+ authClient: createKoraAuthSync({ authClient, schema }),
25
+ },
26
+ store: {
27
+ workerUrl: koraWorkerUrl,
28
+ },
29
+ devtools: true,
25
30
  })
26
31
 
27
32
  // Connect to sync server once the app is ready
28
33
  app.ready.then(() => app.sync?.connect())
34
+ void completeOAuthCallbackFromLocation().catch((error) => {
35
+ console.error('[Kora Auth] OAuth callback failed:', error)
36
+ })
37
+
38
+ const rootElement = document.getElementById('root')
39
+
40
+ if (!rootElement) {
41
+ throw new Error('Root element not found')
42
+ }
29
43
 
30
- createRoot(document.getElementById('root')!).render(
31
- <StrictMode>
32
- <KoraProvider app={app} fallback={<div className="flex items-center justify-center h-screen bg-gray-950 text-gray-400">Loading...</div>}>
33
- <App />
34
- </KoraProvider>
35
- </StrictMode>,
44
+ createRoot(rootElement).render(
45
+ <StrictMode>
46
+ <AuthProvider
47
+ client={authClient}
48
+ fallback={
49
+ <div className="flex h-screen items-center justify-center bg-gray-950 text-gray-400">
50
+ Restoring session...
51
+ </div>
52
+ }
53
+ >
54
+ <KoraProvider
55
+ app={app}
56
+ fallback={
57
+ <div className="flex h-screen items-center justify-center bg-gray-950 text-gray-400">
58
+ Loading...
59
+ </div>
60
+ }
61
+ >
62
+ <App />
63
+ </KoraProvider>
64
+ </AuthProvider>
65
+ </StrictMode>,
36
66
  )
@@ -0,0 +1,25 @@
1
+ import type { CollectionAccessor } from 'korajs'
2
+
3
+ export interface CreateTodoInput {
4
+ title: string
5
+ }
6
+
7
+ export interface UpdateTodoStatusInput {
8
+ completed: boolean
9
+ }
10
+
11
+ export function createTodo(todos: CollectionAccessor, data: CreateTodoInput) {
12
+ return todos.insert({ title: data.title })
13
+ }
14
+
15
+ export function updateTodoStatus(
16
+ todos: CollectionAccessor,
17
+ id: string,
18
+ data: UpdateTodoStatusInput,
19
+ ) {
20
+ return todos.update(id, { completed: data.completed })
21
+ }
22
+
23
+ export function deleteTodo(todos: CollectionAccessor, id: string) {
24
+ return todos.delete(id)
25
+ }