@korajs/cli 0.0.0-canary-20261008174129
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.
- package/LICENSE +21 -0
- package/README.md +44 -0
- package/dist/bin.cjs +9465 -0
- package/dist/bin.cjs.map +1 -0
- package/dist/bin.js +3150 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-5NI2FEQL.js +92 -0
- package/dist/chunk-5NI2FEQL.js.map +1 -0
- package/dist/chunk-6C4BHSRA.js +82 -0
- package/dist/chunk-6C4BHSRA.js.map +1 -0
- package/dist/chunk-7Y4J2GXP.js +796 -0
- package/dist/chunk-7Y4J2GXP.js.map +1 -0
- package/dist/chunk-BURFQVMV.js +719 -0
- package/dist/chunk-BURFQVMV.js.map +1 -0
- package/dist/chunk-TVIRMDER.js +2569 -0
- package/dist/chunk-TVIRMDER.js.map +1 -0
- package/dist/create.cjs +1368 -0
- package/dist/create.cjs.map +1 -0
- package/dist/create.js +11 -0
- package/dist/create.js.map +1 -0
- package/dist/index.cjs +3489 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +772 -0
- package/dist/index.d.ts +772 -0
- package/dist/index.js +129 -0
- package/dist/index.js.map +1 -0
- package/dist/lab-manager-KUDII6BT.js +280 -0
- package/dist/lab-manager-KUDII6BT.js.map +1 -0
- package/dist/schema-loader-GGHECMTM.js +9 -0
- package/dist/schema-loader-GGHECMTM.js.map +1 -0
- package/dist/spectator-manager-FTH2NFSE.js +145 -0
- package/dist/spectator-manager-FTH2NFSE.js.map +1 -0
- package/dist/studio-server-XLGZNGG5.js +1747 -0
- package/dist/studio-server-XLGZNGG5.js.map +1 -0
- package/dist/vite.cjs +351 -0
- package/dist/vite.cjs.map +1 -0
- package/dist/vite.d.cts +161 -0
- package/dist/vite.d.ts +161 -0
- package/dist/vite.js +321 -0
- package/dist/vite.js.map +1 -0
- package/package.json +79 -0
- package/templates/react-basic/.env.example +3 -0
- package/templates/react-basic/AGENTS.md +87 -0
- package/templates/react-basic/README.md.hbs +62 -0
- package/templates/react-basic/index.html.hbs +12 -0
- package/templates/react-basic/kora.config.ts +13 -0
- package/templates/react-basic/package.json.hbs +35 -0
- package/templates/react-basic/src/App.tsx +132 -0
- package/templates/react-basic/src/index.css +287 -0
- package/templates/react-basic/src/kora-worker.ts +9 -0
- package/templates/react-basic/src/main.tsx +24 -0
- package/templates/react-basic/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/react-basic/src/modules/todos/todo.queries.ts +5 -0
- package/templates/react-basic/src/modules/todos/todo.schema.ts +10 -0
- package/templates/react-basic/src/modules/todos/useTodos.ts +31 -0
- package/templates/react-basic/src/schema.ts +9 -0
- package/templates/react-basic/src/vite-env.d.ts +18 -0
- package/templates/react-basic/tsconfig.json.hbs +14 -0
- package/templates/react-basic/vite.config.ts +88 -0
- package/templates/react-sync/.env.example +25 -0
- package/templates/react-sync/AGENTS.md +87 -0
- package/templates/react-sync/README.md.hbs +106 -0
- package/templates/react-sync/index.html.hbs +12 -0
- package/templates/react-sync/kora.config.ts +17 -0
- package/templates/react-sync/package.json.hbs +44 -0
- package/templates/react-sync/server.ts +141 -0
- package/templates/react-sync/src/App.tsx +160 -0
- package/templates/react-sync/src/auth.ts +25 -0
- package/templates/react-sync/src/index.css +350 -0
- package/templates/react-sync/src/kora-worker.ts +9 -0
- package/templates/react-sync/src/main.tsx +52 -0
- package/templates/react-sync/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/react-sync/src/modules/todos/todo.queries.ts +5 -0
- package/templates/react-sync/src/modules/todos/todo.schema.ts +10 -0
- package/templates/react-sync/src/modules/todos/useTodos.ts +31 -0
- package/templates/react-sync/src/schema.ts +9 -0
- package/templates/react-sync/src/vite-env.d.ts +19 -0
- package/templates/react-sync/tsconfig.json.hbs +14 -0
- package/templates/react-sync/vite.config.ts +105 -0
- package/templates/react-tailwind/.env.example +3 -0
- package/templates/react-tailwind/AGENTS.md +87 -0
- package/templates/react-tailwind/README.md.hbs +62 -0
- package/templates/react-tailwind/index.html.hbs +12 -0
- package/templates/react-tailwind/kora.config.ts +13 -0
- package/templates/react-tailwind/package.json.hbs +38 -0
- package/templates/react-tailwind/src/App.tsx +201 -0
- package/templates/react-tailwind/src/index.css +7 -0
- package/templates/react-tailwind/src/kora-worker.ts +9 -0
- package/templates/react-tailwind/src/main.tsx +31 -0
- package/templates/react-tailwind/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/react-tailwind/src/modules/todos/todo.queries.ts +5 -0
- package/templates/react-tailwind/src/modules/todos/todo.schema.ts +10 -0
- package/templates/react-tailwind/src/modules/todos/useTodos.ts +31 -0
- package/templates/react-tailwind/src/schema.ts +9 -0
- package/templates/react-tailwind/src/vite-env.d.ts +18 -0
- package/templates/react-tailwind/tsconfig.json.hbs +14 -0
- package/templates/react-tailwind/vite.config.ts +95 -0
- package/templates/react-tailwind-sync/.env.example +25 -0
- package/templates/react-tailwind-sync/AGENTS.md +89 -0
- package/templates/react-tailwind-sync/README.md.hbs +106 -0
- package/templates/react-tailwind-sync/index.html.hbs +12 -0
- package/templates/react-tailwind-sync/kora.config.ts +17 -0
- package/templates/react-tailwind-sync/package.json.hbs +47 -0
- package/templates/react-tailwind-sync/server.ts +141 -0
- package/templates/react-tailwind-sync/src/App.tsx +293 -0
- package/templates/react-tailwind-sync/src/auth.ts +25 -0
- package/templates/react-tailwind-sync/src/index.css +7 -0
- package/templates/react-tailwind-sync/src/kora-worker.ts +9 -0
- package/templates/react-tailwind-sync/src/kora.ts +32 -0
- package/templates/react-tailwind-sync/src/main.tsx +44 -0
- package/templates/react-tailwind-sync/src/modules/todos/todo.mutations.ts +21 -0
- package/templates/react-tailwind-sync/src/modules/todos/todo.queries.ts +5 -0
- package/templates/react-tailwind-sync/src/modules/todos/todo.schema.ts +10 -0
- package/templates/react-tailwind-sync/src/modules/todos/useTodos.ts +31 -0
- package/templates/react-tailwind-sync/src/schema.ts +9 -0
- package/templates/react-tailwind-sync/src/vite-env.d.ts +19 -0
- package/templates/react-tailwind-sync/tsconfig.json.hbs +14 -0
- package/templates/react-tailwind-sync/vite.config.ts +112 -0
- package/templates/svelte-basic/.env.example +3 -0
- package/templates/svelte-basic/AGENTS.md +76 -0
- package/templates/svelte-basic/README.md.hbs +62 -0
- package/templates/svelte-basic/index.html.hbs +12 -0
- package/templates/svelte-basic/kora.config.ts +12 -0
- package/templates/svelte-basic/package.json.hbs +33 -0
- package/templates/svelte-basic/src/App.svelte +128 -0
- package/templates/svelte-basic/src/Root.svelte +14 -0
- package/templates/svelte-basic/src/index.css +287 -0
- package/templates/svelte-basic/src/kora-worker.ts +9 -0
- package/templates/svelte-basic/src/main.ts +16 -0
- package/templates/svelte-basic/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/svelte-basic/src/modules/todos/todo.queries.ts +5 -0
- package/templates/svelte-basic/src/modules/todos/todo.schema.ts +10 -0
- package/templates/svelte-basic/src/modules/todos/useTodos.ts +25 -0
- package/templates/svelte-basic/src/schema.ts +9 -0
- package/templates/svelte-basic/src/vite-env.d.ts +12 -0
- package/templates/svelte-basic/svelte.config.js +5 -0
- package/templates/svelte-basic/tsconfig.json.hbs +14 -0
- package/templates/svelte-basic/vite.config.ts +57 -0
- package/templates/svelte-sync/.env.example +25 -0
- package/templates/svelte-sync/AGENTS.md +76 -0
- package/templates/svelte-sync/README.md.hbs +106 -0
- package/templates/svelte-sync/index.html.hbs +12 -0
- package/templates/svelte-sync/kora.config.ts +17 -0
- package/templates/svelte-sync/package.json.hbs +43 -0
- package/templates/svelte-sync/server.ts +141 -0
- package/templates/svelte-sync/src/App.svelte +151 -0
- package/templates/svelte-sync/src/Root.svelte +22 -0
- package/templates/svelte-sync/src/auth.ts +25 -0
- package/templates/svelte-sync/src/index.css +339 -0
- package/templates/svelte-sync/src/kora-worker.ts +9 -0
- package/templates/svelte-sync/src/main.ts +32 -0
- package/templates/svelte-sync/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/svelte-sync/src/modules/todos/todo.queries.ts +5 -0
- package/templates/svelte-sync/src/modules/todos/todo.schema.ts +10 -0
- package/templates/svelte-sync/src/modules/todos/useTodos.ts +25 -0
- package/templates/svelte-sync/src/schema.ts +9 -0
- package/templates/svelte-sync/src/vite-env.d.ts +22 -0
- package/templates/svelte-sync/svelte.config.js +5 -0
- package/templates/svelte-sync/tsconfig.json.hbs +14 -0
- package/templates/svelte-sync/vite.config.ts +91 -0
- package/templates/svelte-tailwind/AGENTS.md +76 -0
- package/templates/svelte-tailwind/README.md.hbs +62 -0
- package/templates/svelte-tailwind/index.html.hbs +12 -0
- package/templates/svelte-tailwind/kora.config.ts +12 -0
- package/templates/svelte-tailwind/package.json.hbs +36 -0
- package/templates/svelte-tailwind/src/App.svelte +76 -0
- package/templates/svelte-tailwind/src/Root.svelte +14 -0
- package/templates/svelte-tailwind/src/index.css +7 -0
- package/templates/svelte-tailwind/src/kora-worker.ts +9 -0
- package/templates/svelte-tailwind/src/main.ts +16 -0
- package/templates/svelte-tailwind/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/svelte-tailwind/src/modules/todos/todo.queries.ts +5 -0
- package/templates/svelte-tailwind/src/modules/todos/todo.schema.ts +10 -0
- package/templates/svelte-tailwind/src/modules/todos/useTodos.ts +25 -0
- package/templates/svelte-tailwind/src/schema.ts +9 -0
- package/templates/svelte-tailwind/src/vite-env.d.ts +12 -0
- package/templates/svelte-tailwind/svelte.config.js +5 -0
- package/templates/svelte-tailwind/tsconfig.json.hbs +14 -0
- package/templates/svelte-tailwind/vite.config.ts +64 -0
- package/templates/svelte-tailwind-sync/.env.example +25 -0
- package/templates/svelte-tailwind-sync/AGENTS.md +76 -0
- package/templates/svelte-tailwind-sync/README.md.hbs +106 -0
- package/templates/svelte-tailwind-sync/index.html.hbs +12 -0
- package/templates/svelte-tailwind-sync/kora.config.ts +17 -0
- package/templates/svelte-tailwind-sync/package.json.hbs +46 -0
- package/templates/svelte-tailwind-sync/server.ts +141 -0
- package/templates/svelte-tailwind-sync/src/App.svelte +193 -0
- package/templates/svelte-tailwind-sync/src/Root.svelte +26 -0
- package/templates/svelte-tailwind-sync/src/auth.ts +25 -0
- package/templates/svelte-tailwind-sync/src/index.css +7 -0
- package/templates/svelte-tailwind-sync/src/kora-worker.ts +9 -0
- package/templates/svelte-tailwind-sync/src/main.ts +32 -0
- package/templates/svelte-tailwind-sync/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/svelte-tailwind-sync/src/modules/todos/todo.queries.ts +5 -0
- package/templates/svelte-tailwind-sync/src/modules/todos/todo.schema.ts +10 -0
- package/templates/svelte-tailwind-sync/src/modules/todos/useTodos.ts +25 -0
- package/templates/svelte-tailwind-sync/src/schema.ts +9 -0
- package/templates/svelte-tailwind-sync/src/vite-env.d.ts +22 -0
- package/templates/svelte-tailwind-sync/svelte.config.js +5 -0
- package/templates/svelte-tailwind-sync/tsconfig.json.hbs +14 -0
- package/templates/svelte-tailwind-sync/vite.config.ts +98 -0
- package/templates/tauri-react/.env.example +27 -0
- package/templates/tauri-react/.github/workflows/release-desktop.yml +107 -0
- package/templates/tauri-react/AGENTS.md +91 -0
- package/templates/tauri-react/README.md.hbs +183 -0
- package/templates/tauri-react/dev.ts +37 -0
- package/templates/tauri-react/index.html.hbs +16 -0
- package/templates/tauri-react/kora.config.ts +17 -0
- package/templates/tauri-react/package.json.hbs +46 -0
- package/templates/tauri-react/server.ts +148 -0
- package/templates/tauri-react/src/App.tsx +803 -0
- package/templates/tauri-react/src/AppShell.tsx +158 -0
- package/templates/tauri-react/src/SetupScreen.tsx +288 -0
- package/templates/tauri-react/src/auth.ts +45 -0
- package/templates/tauri-react/src/main.tsx +9 -0
- package/templates/tauri-react/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/tauri-react/src/modules/todos/todo.queries.ts +5 -0
- package/templates/tauri-react/src/modules/todos/todo.schema.ts +10 -0
- package/templates/tauri-react/src/modules/todos/useTodos.ts +31 -0
- package/templates/tauri-react/src/schema.ts +9 -0
- package/templates/tauri-react/src/sync-config.ts +103 -0
- package/templates/tauri-react/src/updater.ts +38 -0
- package/templates/tauri-react/src/vite-env.d.ts +19 -0
- package/templates/tauri-react/src-tauri/Cargo.toml +19 -0
- package/templates/tauri-react/src-tauri/build.rs +3 -0
- package/templates/tauri-react/src-tauri/capabilities/default.json +12 -0
- package/templates/tauri-react/src-tauri/icons/128x128.png +0 -0
- package/templates/tauri-react/src-tauri/icons/128x128@2x.png +0 -0
- package/templates/tauri-react/src-tauri/icons/32x32.png +0 -0
- package/templates/tauri-react/src-tauri/icons/icon.icns +0 -0
- package/templates/tauri-react/src-tauri/icons/icon.ico +0 -0
- package/templates/tauri-react/src-tauri/src/lib.rs +8 -0
- package/templates/tauri-react/src-tauri/src/main.rs +6 -0
- package/templates/tauri-react/src-tauri/tauri.conf.json +44 -0
- package/templates/tauri-react/tsconfig.json.hbs +14 -0
- package/templates/tauri-react/vite.config.ts +15 -0
- package/templates/vue-basic/AGENTS.md +84 -0
- package/templates/vue-basic/README.md.hbs +62 -0
- package/templates/vue-basic/index.html.hbs +12 -0
- package/templates/vue-basic/kora.config.ts +12 -0
- package/templates/vue-basic/package.json.hbs +32 -0
- package/templates/vue-basic/src/App.vue +117 -0
- package/templates/vue-basic/src/index.css +287 -0
- package/templates/vue-basic/src/kora-worker.ts +9 -0
- package/templates/vue-basic/src/main.ts +20 -0
- package/templates/vue-basic/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/vue-basic/src/modules/todos/todo.queries.ts +5 -0
- package/templates/vue-basic/src/modules/todos/todo.schema.ts +10 -0
- package/templates/vue-basic/src/modules/todos/useTodos.ts +32 -0
- package/templates/vue-basic/src/schema.ts +9 -0
- package/templates/vue-basic/src/vite-env.d.ts +17 -0
- package/templates/vue-basic/tsconfig.json.hbs +15 -0
- package/templates/vue-basic/vite.config.ts +74 -0
- package/templates/vue-sync/.env.example +25 -0
- package/templates/vue-sync/AGENTS.md +84 -0
- package/templates/vue-sync/README.md.hbs +106 -0
- package/templates/vue-sync/index.html.hbs +12 -0
- package/templates/vue-sync/kora.config.ts +17 -0
- package/templates/vue-sync/package.json.hbs +42 -0
- package/templates/vue-sync/server.ts +141 -0
- package/templates/vue-sync/src/App.vue +135 -0
- package/templates/vue-sync/src/auth.ts +25 -0
- package/templates/vue-sync/src/index.css +339 -0
- package/templates/vue-sync/src/kora-worker.ts +9 -0
- package/templates/vue-sync/src/main.ts +47 -0
- package/templates/vue-sync/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/vue-sync/src/modules/todos/todo.queries.ts +5 -0
- package/templates/vue-sync/src/modules/todos/todo.schema.ts +10 -0
- package/templates/vue-sync/src/modules/todos/useTodos.ts +32 -0
- package/templates/vue-sync/src/schema.ts +9 -0
- package/templates/vue-sync/src/vite-env.d.ts +27 -0
- package/templates/vue-sync/tsconfig.json.hbs +15 -0
- package/templates/vue-sync/vite.config.ts +91 -0
- package/templates/vue-tailwind/AGENTS.md +84 -0
- package/templates/vue-tailwind/README.md.hbs +62 -0
- package/templates/vue-tailwind/index.html.hbs +12 -0
- package/templates/vue-tailwind/kora.config.ts +12 -0
- package/templates/vue-tailwind/package.json.hbs +35 -0
- package/templates/vue-tailwind/src/App.vue +135 -0
- package/templates/vue-tailwind/src/index.css +7 -0
- package/templates/vue-tailwind/src/kora-worker.ts +9 -0
- package/templates/vue-tailwind/src/main.ts +31 -0
- package/templates/vue-tailwind/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/vue-tailwind/src/modules/todos/todo.queries.ts +5 -0
- package/templates/vue-tailwind/src/modules/todos/todo.schema.ts +10 -0
- package/templates/vue-tailwind/src/modules/todos/useTodos.ts +32 -0
- package/templates/vue-tailwind/src/schema.ts +9 -0
- package/templates/vue-tailwind/src/vite-env.d.ts +17 -0
- package/templates/vue-tailwind/tsconfig.json.hbs +15 -0
- package/templates/vue-tailwind/vite.config.ts +75 -0
- package/templates/vue-tailwind-sync/.env.example +25 -0
- package/templates/vue-tailwind-sync/AGENTS.md +84 -0
- package/templates/vue-tailwind-sync/README.md.hbs +106 -0
- package/templates/vue-tailwind-sync/index.html.hbs +12 -0
- package/templates/vue-tailwind-sync/kora.config.ts +17 -0
- package/templates/vue-tailwind-sync/package.json.hbs +45 -0
- package/templates/vue-tailwind-sync/server.ts +141 -0
- package/templates/vue-tailwind-sync/src/App.vue +245 -0
- package/templates/vue-tailwind-sync/src/auth.ts +25 -0
- package/templates/vue-tailwind-sync/src/index.css +7 -0
- package/templates/vue-tailwind-sync/src/kora-worker.ts +9 -0
- package/templates/vue-tailwind-sync/src/main.ts +56 -0
- package/templates/vue-tailwind-sync/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/vue-tailwind-sync/src/modules/todos/todo.queries.ts +5 -0
- package/templates/vue-tailwind-sync/src/modules/todos/todo.schema.ts +10 -0
- package/templates/vue-tailwind-sync/src/modules/todos/useTodos.ts +32 -0
- package/templates/vue-tailwind-sync/src/schema.ts +9 -0
- package/templates/vue-tailwind-sync/src/vite-env.d.ts +27 -0
- package/templates/vue-tailwind-sync/tsconfig.json.hbs +15 -0
- package/templates/vue-tailwind-sync/vite.config.ts +92 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { createKoraAuthSync } from '@korajs/auth'
|
|
2
|
+
import { AuthProvider } from '@korajs/auth/react'
|
|
3
|
+
import { KoraProvider } from '@korajs/react'
|
|
4
|
+
import { createApp } from 'korajs'
|
|
5
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
6
|
+
import { App } from './App'
|
|
7
|
+
import { SetupScreen } from './SetupScreen'
|
|
8
|
+
import { completeOAuthCallbackFromLocation, createDesktopAuthClient } from './auth'
|
|
9
|
+
import schema from './schema'
|
|
10
|
+
import {
|
|
11
|
+
clearSyncUrl,
|
|
12
|
+
factoryReset,
|
|
13
|
+
getSyncUrl,
|
|
14
|
+
hasCompletedSetup,
|
|
15
|
+
markSetupComplete,
|
|
16
|
+
setSyncUrl,
|
|
17
|
+
} from './sync-config'
|
|
18
|
+
import { checkForUpdates } from './updater'
|
|
19
|
+
|
|
20
|
+
type AppState = { phase: 'setup' } | { phase: 'running'; syncUrl: string | null }
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Root component that handles the first-launch setup flow.
|
|
24
|
+
*
|
|
25
|
+
* - If setup not completed, shows the setup screen.
|
|
26
|
+
* - If completed, initializes Kora and renders the app.
|
|
27
|
+
* - Handles server changes with proper data isolation warnings.
|
|
28
|
+
*
|
|
29
|
+
* Changing the sync server requires an app restart (to re-create the
|
|
30
|
+
* Kora app instance with the new URL). This is intentional — it prevents
|
|
31
|
+
* partial state from one server leaking to another.
|
|
32
|
+
*/
|
|
33
|
+
export function AppShell() {
|
|
34
|
+
const [state, setState] = useState<AppState>(() => {
|
|
35
|
+
if (hasCompletedSetup()) {
|
|
36
|
+
return { phase: 'running', syncUrl: getSyncUrl() }
|
|
37
|
+
}
|
|
38
|
+
return { phase: 'setup' }
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const handleConnect = useCallback((url: string) => {
|
|
42
|
+
setSyncUrl(url)
|
|
43
|
+
setState({ phase: 'running', syncUrl: url })
|
|
44
|
+
}, [])
|
|
45
|
+
|
|
46
|
+
const handleSkip = useCallback(() => {
|
|
47
|
+
markSetupComplete()
|
|
48
|
+
setState({ phase: 'running', syncUrl: null })
|
|
49
|
+
}, [])
|
|
50
|
+
|
|
51
|
+
// Change to a different server URL.
|
|
52
|
+
// This requires restarting the app to create a fresh Kora instance.
|
|
53
|
+
const handleChangeServer = useCallback((newUrl: string | null) => {
|
|
54
|
+
if (newUrl) {
|
|
55
|
+
setSyncUrl(newUrl)
|
|
56
|
+
} else {
|
|
57
|
+
clearSyncUrl()
|
|
58
|
+
markSetupComplete() // Still completed — just disconnected
|
|
59
|
+
}
|
|
60
|
+
// Reload to re-initialize Kora with the new URL.
|
|
61
|
+
// This is cleaner than trying to hot-swap the sync connection,
|
|
62
|
+
// and prevents data from one server context leaking to another.
|
|
63
|
+
window.location.reload()
|
|
64
|
+
}, [])
|
|
65
|
+
|
|
66
|
+
const handleFactoryReset = useCallback(() => {
|
|
67
|
+
factoryReset() // Clears all data and reloads
|
|
68
|
+
}, [])
|
|
69
|
+
|
|
70
|
+
if (state.phase === 'setup') {
|
|
71
|
+
return <SetupScreen onConnect={handleConnect} onSkip={handleSkip} />
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<ConnectedApp
|
|
76
|
+
syncUrl={state.syncUrl}
|
|
77
|
+
onChangeServer={handleChangeServer}
|
|
78
|
+
onFactoryReset={handleFactoryReset}
|
|
79
|
+
/>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface ConnectedAppProps {
|
|
84
|
+
syncUrl: string | null
|
|
85
|
+
onChangeServer: (newUrl: string | null) => void
|
|
86
|
+
onFactoryReset: () => void
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function ConnectedApp({ syncUrl, onChangeServer, onFactoryReset }: ConnectedAppProps) {
|
|
90
|
+
const [authClient] = useState(() => createDesktopAuthClient(syncUrl))
|
|
91
|
+
// Create the Kora app instance with optional sync.
|
|
92
|
+
// This only runs once — the app instance is stable for the lifetime of this component.
|
|
93
|
+
const [app] = useState(() =>
|
|
94
|
+
createApp({
|
|
95
|
+
schema,
|
|
96
|
+
...(syncUrl
|
|
97
|
+
? {
|
|
98
|
+
sync: {
|
|
99
|
+
url: syncUrl,
|
|
100
|
+
authClient: createKoraAuthSync({ authClient, schema }),
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
: {}),
|
|
104
|
+
devtools: import.meta.env.DEV,
|
|
105
|
+
}),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
// Connect to sync server once ready, and check for updates
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
void completeOAuthCallbackFromLocation(authClient).catch((error) => {
|
|
111
|
+
console.error('[Kora Auth] OAuth callback failed:', error)
|
|
112
|
+
})
|
|
113
|
+
if (syncUrl) {
|
|
114
|
+
app.ready.then(() => app.sync?.connect())
|
|
115
|
+
}
|
|
116
|
+
checkForUpdates()
|
|
117
|
+
}, [app, authClient, syncUrl])
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<AuthProvider
|
|
121
|
+
client={authClient}
|
|
122
|
+
fallback={
|
|
123
|
+
<div
|
|
124
|
+
style={{
|
|
125
|
+
display: 'flex',
|
|
126
|
+
alignItems: 'center',
|
|
127
|
+
justifyContent: 'center',
|
|
128
|
+
height: '100vh',
|
|
129
|
+
background: '#0a0a0a',
|
|
130
|
+
color: '#6b7280',
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
Restoring session...
|
|
134
|
+
</div>
|
|
135
|
+
}
|
|
136
|
+
>
|
|
137
|
+
<KoraProvider
|
|
138
|
+
app={app}
|
|
139
|
+
fallback={
|
|
140
|
+
<div
|
|
141
|
+
style={{
|
|
142
|
+
display: 'flex',
|
|
143
|
+
alignItems: 'center',
|
|
144
|
+
justifyContent: 'center',
|
|
145
|
+
height: '100vh',
|
|
146
|
+
background: '#0a0a0a',
|
|
147
|
+
color: '#6b7280',
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
Loading...
|
|
151
|
+
</div>
|
|
152
|
+
}
|
|
153
|
+
>
|
|
154
|
+
<App syncUrl={syncUrl} onChangeServer={onChangeServer} onFactoryReset={onFactoryReset} />
|
|
155
|
+
</KoraProvider>
|
|
156
|
+
</AuthProvider>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { AlertCircle, ArrowRight, Loader2, Monitor, Wifi } from 'lucide-react'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import { testConnection } from './sync-config'
|
|
4
|
+
|
|
5
|
+
interface SetupScreenProps {
|
|
6
|
+
onConnect: (syncUrl: string) => void
|
|
7
|
+
onSkip: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* First-launch setup screen.
|
|
12
|
+
*
|
|
13
|
+
* Shown when no sync server URL is configured. The user enters their
|
|
14
|
+
* organization's sync server URL, or skips to use the app in local-only mode.
|
|
15
|
+
*
|
|
16
|
+
* The connection test is advisory — if the server is unreachable (e.g.,
|
|
17
|
+
* during maintenance), the user can still save the URL and the app will
|
|
18
|
+
* connect when the server comes back.
|
|
19
|
+
*/
|
|
20
|
+
export function SetupScreen({ onConnect, onSkip }: SetupScreenProps) {
|
|
21
|
+
const [url, setUrl] = useState('')
|
|
22
|
+
const [testing, setTesting] = useState(false)
|
|
23
|
+
const [error, setError] = useState<string | null>(null)
|
|
24
|
+
const [unreachable, setUnreachable] = useState(false)
|
|
25
|
+
|
|
26
|
+
const validateUrl = (value: string): string | null => {
|
|
27
|
+
if (!value.startsWith('ws://') && !value.startsWith('wss://')) {
|
|
28
|
+
return 'URL must start with ws:// or wss://'
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
new URL(value.replace(/^ws/, 'http'))
|
|
32
|
+
} catch {
|
|
33
|
+
return 'Invalid URL format'
|
|
34
|
+
}
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
39
|
+
e.preventDefault()
|
|
40
|
+
const syncUrl = url.trim()
|
|
41
|
+
if (!syncUrl) return
|
|
42
|
+
|
|
43
|
+
const validationError = validateUrl(syncUrl)
|
|
44
|
+
if (validationError) {
|
|
45
|
+
setError(validationError)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
setTesting(true)
|
|
50
|
+
setError(null)
|
|
51
|
+
setUnreachable(false)
|
|
52
|
+
|
|
53
|
+
const reachable = await testConnection(syncUrl)
|
|
54
|
+
setTesting(false)
|
|
55
|
+
|
|
56
|
+
if (reachable) {
|
|
57
|
+
onConnect(syncUrl)
|
|
58
|
+
} else {
|
|
59
|
+
// Server unreachable — let the user save anyway
|
|
60
|
+
setUnreachable(true)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const handleSaveAnyway = () => {
|
|
65
|
+
onConnect(url.trim())
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div
|
|
70
|
+
style={{
|
|
71
|
+
minHeight: '100vh',
|
|
72
|
+
background: '#0a0a0a',
|
|
73
|
+
color: '#f3f4f6',
|
|
74
|
+
display: 'flex',
|
|
75
|
+
alignItems: 'center',
|
|
76
|
+
justifyContent: 'center',
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
<div style={{ maxWidth: '480px', width: '100%', padding: '0 24px' }}>
|
|
80
|
+
{/* Logo */}
|
|
81
|
+
<div style={{ textAlign: 'center', marginBottom: '48px' }}>
|
|
82
|
+
<Monitor
|
|
83
|
+
style={{ width: '48px', height: '48px', color: '#818cf8', margin: '0 auto 16px' }}
|
|
84
|
+
/>
|
|
85
|
+
<h1 style={{ fontSize: '28px', fontWeight: 'bold', marginBottom: '8px' }}>Welcome</h1>
|
|
86
|
+
<p style={{ color: '#6b7280', fontSize: '15px' }}>
|
|
87
|
+
Connect to your organization's sync server to sync data across devices.
|
|
88
|
+
</p>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* Connection form */}
|
|
92
|
+
<form onSubmit={handleSubmit}>
|
|
93
|
+
<div style={{ marginBottom: '16px' }}>
|
|
94
|
+
<label
|
|
95
|
+
htmlFor="sync-server-url"
|
|
96
|
+
style={{
|
|
97
|
+
display: 'block',
|
|
98
|
+
fontSize: '14px',
|
|
99
|
+
fontWeight: '500',
|
|
100
|
+
marginBottom: '8px',
|
|
101
|
+
color: '#d1d5db',
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
Sync server URL
|
|
105
|
+
</label>
|
|
106
|
+
<div style={{ position: 'relative' }}>
|
|
107
|
+
<Wifi
|
|
108
|
+
style={{
|
|
109
|
+
position: 'absolute',
|
|
110
|
+
left: '12px',
|
|
111
|
+
top: '50%',
|
|
112
|
+
transform: 'translateY(-50%)',
|
|
113
|
+
width: '18px',
|
|
114
|
+
height: '18px',
|
|
115
|
+
color: '#6b7280',
|
|
116
|
+
}}
|
|
117
|
+
/>
|
|
118
|
+
<input
|
|
119
|
+
id="sync-server-url"
|
|
120
|
+
type="text"
|
|
121
|
+
value={url}
|
|
122
|
+
onChange={(e) => {
|
|
123
|
+
setUrl(e.target.value)
|
|
124
|
+
setError(null)
|
|
125
|
+
setUnreachable(false)
|
|
126
|
+
}}
|
|
127
|
+
placeholder="wss://your-server.example.com/kora-sync"
|
|
128
|
+
style={{
|
|
129
|
+
width: '100%',
|
|
130
|
+
borderRadius: '8px',
|
|
131
|
+
border: `1px solid ${error ? '#ef4444' : '#374151'}`,
|
|
132
|
+
background: '#111827',
|
|
133
|
+
padding: '12px 16px 12px 40px',
|
|
134
|
+
color: '#f3f4f6',
|
|
135
|
+
outline: 'none',
|
|
136
|
+
fontSize: '14px',
|
|
137
|
+
boxSizing: 'border-box',
|
|
138
|
+
}}
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
{error && (
|
|
142
|
+
<p style={{ color: '#ef4444', fontSize: '13px', marginTop: '8px' }}>{error}</p>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{/* Server unreachable — offer to save anyway */}
|
|
147
|
+
{unreachable && (
|
|
148
|
+
<div
|
|
149
|
+
style={{
|
|
150
|
+
marginBottom: '16px',
|
|
151
|
+
padding: '12px 16px',
|
|
152
|
+
borderRadius: '8px',
|
|
153
|
+
border: '1px solid #92400e',
|
|
154
|
+
background: '#451a03',
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
<div style={{ display: 'flex', gap: '8px', alignItems: 'flex-start' }}>
|
|
158
|
+
<AlertCircle
|
|
159
|
+
style={{
|
|
160
|
+
width: '16px',
|
|
161
|
+
height: '16px',
|
|
162
|
+
color: '#fbbf24',
|
|
163
|
+
flexShrink: 0,
|
|
164
|
+
marginTop: '2px',
|
|
165
|
+
}}
|
|
166
|
+
/>
|
|
167
|
+
<div>
|
|
168
|
+
<p style={{ fontSize: '13px', color: '#fde68a', marginBottom: '8px' }}>
|
|
169
|
+
Server is not reachable right now. This could be temporary (maintenance, network
|
|
170
|
+
issues).
|
|
171
|
+
</p>
|
|
172
|
+
<div style={{ display: 'flex', gap: '8px' }}>
|
|
173
|
+
<button
|
|
174
|
+
type="button"
|
|
175
|
+
onClick={handleSaveAnyway}
|
|
176
|
+
style={{
|
|
177
|
+
fontSize: '13px',
|
|
178
|
+
color: '#fbbf24',
|
|
179
|
+
background: 'none',
|
|
180
|
+
border: '1px solid #92400e',
|
|
181
|
+
borderRadius: '6px',
|
|
182
|
+
padding: '4px 12px',
|
|
183
|
+
cursor: 'pointer',
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
Save anyway — connect later
|
|
187
|
+
</button>
|
|
188
|
+
<button
|
|
189
|
+
type="submit"
|
|
190
|
+
style={{
|
|
191
|
+
fontSize: '13px',
|
|
192
|
+
color: '#9ca3af',
|
|
193
|
+
background: 'none',
|
|
194
|
+
border: '1px solid #374151',
|
|
195
|
+
borderRadius: '6px',
|
|
196
|
+
padding: '4px 12px',
|
|
197
|
+
cursor: 'pointer',
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
200
|
+
Retry
|
|
201
|
+
</button>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
|
|
208
|
+
{!unreachable && (
|
|
209
|
+
<button
|
|
210
|
+
type="submit"
|
|
211
|
+
disabled={testing || !url.trim()}
|
|
212
|
+
style={{
|
|
213
|
+
width: '100%',
|
|
214
|
+
display: 'flex',
|
|
215
|
+
alignItems: 'center',
|
|
216
|
+
justifyContent: 'center',
|
|
217
|
+
gap: '8px',
|
|
218
|
+
borderRadius: '8px',
|
|
219
|
+
background: '#4f46e5',
|
|
220
|
+
padding: '12px 20px',
|
|
221
|
+
fontWeight: '500',
|
|
222
|
+
color: 'white',
|
|
223
|
+
border: 'none',
|
|
224
|
+
cursor: testing || !url.trim() ? 'default' : 'pointer',
|
|
225
|
+
opacity: testing || !url.trim() ? 0.5 : 1,
|
|
226
|
+
fontSize: '14px',
|
|
227
|
+
marginBottom: '12px',
|
|
228
|
+
}}
|
|
229
|
+
>
|
|
230
|
+
{testing ? (
|
|
231
|
+
<>
|
|
232
|
+
<Loader2
|
|
233
|
+
style={{ width: '16px', height: '16px', animation: 'spin 1s linear infinite' }}
|
|
234
|
+
/>
|
|
235
|
+
Testing connection...
|
|
236
|
+
</>
|
|
237
|
+
) : (
|
|
238
|
+
<>
|
|
239
|
+
Connect
|
|
240
|
+
<ArrowRight style={{ width: '16px', height: '16px' }} />
|
|
241
|
+
</>
|
|
242
|
+
)}
|
|
243
|
+
</button>
|
|
244
|
+
)}
|
|
245
|
+
</form>
|
|
246
|
+
|
|
247
|
+
{/* Skip option */}
|
|
248
|
+
<div style={{ textAlign: 'center', marginTop: '24px' }}>
|
|
249
|
+
<button
|
|
250
|
+
onClick={onSkip}
|
|
251
|
+
style={{
|
|
252
|
+
background: 'none',
|
|
253
|
+
border: 'none',
|
|
254
|
+
color: '#6b7280',
|
|
255
|
+
cursor: 'pointer',
|
|
256
|
+
fontSize: '14px',
|
|
257
|
+
textDecoration: 'underline',
|
|
258
|
+
textUnderlineOffset: '3px',
|
|
259
|
+
}}
|
|
260
|
+
>
|
|
261
|
+
Skip — use offline only
|
|
262
|
+
</button>
|
|
263
|
+
<p style={{ color: '#374151', fontSize: '12px', marginTop: '8px' }}>
|
|
264
|
+
You can connect to a sync server later from settings.
|
|
265
|
+
</p>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
{/* Help text */}
|
|
269
|
+
<div
|
|
270
|
+
style={{
|
|
271
|
+
marginTop: '48px',
|
|
272
|
+
padding: '16px',
|
|
273
|
+
borderRadius: '8px',
|
|
274
|
+
border: '1px solid #1f2937',
|
|
275
|
+
background: '#111827',
|
|
276
|
+
}}
|
|
277
|
+
>
|
|
278
|
+
<p style={{ fontSize: '13px', color: '#9ca3af', lineHeight: '1.6' }}>
|
|
279
|
+
<strong style={{ color: '#d1d5db' }}>Don't have a server URL?</strong>
|
|
280
|
+
<br />
|
|
281
|
+
Ask your administrator for the sync server address. For local development, use{' '}
|
|
282
|
+
<code style={{ color: '#818cf8' }}>ws://localhost:3001/kora-sync</code>.
|
|
283
|
+
</p>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
)
|
|
288
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type AuthClient, createKoraAuth } from '@korajs/auth'
|
|
2
|
+
|
|
3
|
+
export function createDesktopAuthClient(syncUrl: string | null): AuthClient {
|
|
4
|
+
return createKoraAuth({
|
|
5
|
+
serverUrl: getAuthServerUrl(syncUrl),
|
|
6
|
+
})
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getAuthServerUrl(syncUrl: string | null): string {
|
|
10
|
+
if (import.meta.env.VITE_AUTH_URL) {
|
|
11
|
+
return import.meta.env.VITE_AUTH_URL
|
|
12
|
+
}
|
|
13
|
+
if (!syncUrl) {
|
|
14
|
+
return 'http://localhost:3001'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const url = new URL(syncUrl)
|
|
18
|
+
url.protocol = url.protocol === 'wss:' ? 'https:' : 'http:'
|
|
19
|
+
url.pathname = url.pathname.replace(/\/kora-sync\/?$/, '') || '/'
|
|
20
|
+
url.search = ''
|
|
21
|
+
url.hash = ''
|
|
22
|
+
return url.toString().replace(/\/+$/, '')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function openOAuthSignIn(authClient: AuthClient, provider = 'google'): Promise<void> {
|
|
26
|
+
const { url } = await authClient.getOAuthAuthorizationUrl(provider)
|
|
27
|
+
window.open(url, '_blank', 'noopener,noreferrer')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function completeOAuthCallbackFromLocation(authClient: AuthClient): Promise<void> {
|
|
31
|
+
const url = new URL(window.location.href)
|
|
32
|
+
const code = url.searchParams.get('code')
|
|
33
|
+
const state = url.searchParams.get('state')
|
|
34
|
+
const provider = url.searchParams.get('provider') || 'google'
|
|
35
|
+
|
|
36
|
+
if (!code || !state) {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await authClient.completeOAuthSignIn(provider, { code, state })
|
|
41
|
+
url.searchParams.delete('code')
|
|
42
|
+
url.searchParams.delete('state')
|
|
43
|
+
url.searchParams.delete('provider')
|
|
44
|
+
window.history.replaceState({}, document.title, `${url.pathname}${url.search}${url.hash}`)
|
|
45
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useCollection, useMutation, useQuery } from '@korajs/react'
|
|
2
|
+
import {
|
|
3
|
+
type CreateTodoInput,
|
|
4
|
+
type UpdateTodoStatusInput,
|
|
5
|
+
createTodo,
|
|
6
|
+
deleteTodo,
|
|
7
|
+
updateTodoStatus,
|
|
8
|
+
} from './todo.mutations'
|
|
9
|
+
import { orderedTodos } from './todo.queries'
|
|
10
|
+
|
|
11
|
+
export function useTodos() {
|
|
12
|
+
const todos = useCollection('todos')
|
|
13
|
+
const allTodos = useQuery(orderedTodos(todos))
|
|
14
|
+
const addTodo = useMutation((data: CreateTodoInput) => createTodo(todos, data))
|
|
15
|
+
const toggleTodo = useMutation((id: string, data: UpdateTodoStatusInput) =>
|
|
16
|
+
updateTodoStatus(todos, id, data),
|
|
17
|
+
)
|
|
18
|
+
const removeTodo = useMutation((id: string) => deleteTodo(todos, id))
|
|
19
|
+
|
|
20
|
+
const activeTodos = allTodos.filter((todo) => !todo.completed)
|
|
21
|
+
const completedTodos = allTodos.filter((todo) => !!todo.completed)
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
allTodos,
|
|
25
|
+
activeTodos,
|
|
26
|
+
completedTodos,
|
|
27
|
+
addTodo,
|
|
28
|
+
toggleTodo,
|
|
29
|
+
deleteTodo: removeTodo,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime sync server configuration.
|
|
3
|
+
*
|
|
4
|
+
* On first launch, the user enters their sync server URL (or organization URL).
|
|
5
|
+
* This is stored in localStorage and used for all subsequent launches.
|
|
6
|
+
*
|
|
7
|
+
* This allows a single binary to be distributed to multiple organizations,
|
|
8
|
+
* each pointing to their own sync server.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const SYNC_URL_KEY = 'kora-sync-url'
|
|
12
|
+
const SYNC_CONFIGURED_KEY = 'kora-sync-configured'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get the configured sync server URL.
|
|
16
|
+
* Priority: localStorage → VITE_SYNC_URL env var → null (needs setup).
|
|
17
|
+
*/
|
|
18
|
+
export function getSyncUrl(): string | null {
|
|
19
|
+
const stored = localStorage.getItem(SYNC_URL_KEY)
|
|
20
|
+
if (stored) return stored
|
|
21
|
+
|
|
22
|
+
// Compile-time default (set via VITE_SYNC_URL when building)
|
|
23
|
+
const envUrl = import.meta.env.VITE_SYNC_URL
|
|
24
|
+
if (envUrl) return envUrl
|
|
25
|
+
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Save a sync server URL to persistent local config. */
|
|
30
|
+
export function setSyncUrl(url: string): void {
|
|
31
|
+
localStorage.setItem(SYNC_URL_KEY, url)
|
|
32
|
+
localStorage.setItem(SYNC_CONFIGURED_KEY, 'true')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Clear the stored sync server URL (returns to setup screen). */
|
|
36
|
+
export function clearSyncUrl(): void {
|
|
37
|
+
localStorage.removeItem(SYNC_URL_KEY)
|
|
38
|
+
localStorage.removeItem(SYNC_CONFIGURED_KEY)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether the user has completed initial setup (connected or explicitly skipped).
|
|
43
|
+
* This is separate from getSyncUrl() — a user who skipped still completed setup.
|
|
44
|
+
*/
|
|
45
|
+
export function hasCompletedSetup(): boolean {
|
|
46
|
+
// If there's a compile-time URL, setup is not needed
|
|
47
|
+
if (import.meta.env.VITE_SYNC_URL) return true
|
|
48
|
+
return localStorage.getItem(SYNC_CONFIGURED_KEY) === 'true'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Mark setup as completed (used when user skips sync). */
|
|
52
|
+
export function markSetupComplete(): void {
|
|
53
|
+
localStorage.setItem(SYNC_CONFIGURED_KEY, 'true')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Full factory reset: clears sync config and all local data.
|
|
58
|
+
* The app reloads with a fresh state, showing the setup screen.
|
|
59
|
+
*/
|
|
60
|
+
export function factoryReset(): void {
|
|
61
|
+
// Clear sync config
|
|
62
|
+
clearSyncUrl()
|
|
63
|
+
localStorage.removeItem(SYNC_CONFIGURED_KEY)
|
|
64
|
+
|
|
65
|
+
// Clear all Kora local data by removing IndexedDB databases
|
|
66
|
+
// and any other localStorage keys the app may have set.
|
|
67
|
+
const keysToRemove: string[] = []
|
|
68
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
69
|
+
const key = localStorage.key(i)
|
|
70
|
+
if (key?.startsWith('kora-')) {
|
|
71
|
+
keysToRemove.push(key)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const key of keysToRemove) {
|
|
75
|
+
localStorage.removeItem(key)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Reload to reinitialize everything from scratch
|
|
79
|
+
window.location.reload()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Test if a WebSocket URL is reachable by attempting a brief connection. */
|
|
83
|
+
export function testConnection(url: string, timeoutMs = 5000): Promise<boolean> {
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
const timeout = setTimeout(() => {
|
|
86
|
+
ws.close()
|
|
87
|
+
resolve(false)
|
|
88
|
+
}, timeoutMs)
|
|
89
|
+
|
|
90
|
+
const ws = new WebSocket(url)
|
|
91
|
+
|
|
92
|
+
ws.onopen = () => {
|
|
93
|
+
clearTimeout(timeout)
|
|
94
|
+
ws.close()
|
|
95
|
+
resolve(true)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
ws.onerror = () => {
|
|
99
|
+
clearTimeout(timeout)
|
|
100
|
+
resolve(false)
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
}
|