From e8a2a7e6f04331f82b2e69f510d4fb9fc9a8ca86 Mon Sep 17 00:00:00 2001 From: Ou Date: Tue, 15 Oct 2024 12:05:03 +0800 Subject: [PATCH] feat: sync user action --- package.json | 3 +- pnpm-lock.yaml | 3 + schema.sql | 6 -- server/database/user.ts | 17 +++--- server/middleware/auth.ts | 6 +- server/routes/me/index.ts | 5 ++ server/routes/me/sync.ts | 33 ++++++++++ server/routes/oauth/github.ts | 1 + server/routes/s/[id].ts | 28 ++++----- shared/types.ts | 18 +++--- shared/verify.ts | 8 +++ src/atoms/atomWithLocalStorage.ts | 67 --------------------- src/atoms/index.ts | 50 +++++++-------- src/atoms/primitiveMetadataAtom.ts | 62 +++++++++++++++++++ src/atoms/types.ts | 1 + src/components/column/card.tsx | 10 +-- src/components/column/dnd.tsx | 4 +- src/components/header.tsx | 10 +-- src/hooks/useSync.ts | 57 ++++++++++++++++++ src/routeTree.gen.ts | 38 ++++++------ src/routes/__root.tsx | 2 + src/routes/{s.$column.tsx => c.$column.tsx} | 5 +- src/routes/index.tsx | 6 +- 23 files changed, 260 insertions(+), 180 deletions(-) delete mode 100644 schema.sql create mode 100644 server/routes/me/index.ts create mode 100644 server/routes/me/sync.ts create mode 100644 shared/verify.ts delete mode 100644 src/atoms/atomWithLocalStorage.ts create mode 100644 src/atoms/primitiveMetadataAtom.ts create mode 100644 src/atoms/types.ts create mode 100644 src/hooks/useSync.ts rename src/routes/{s.$column.tsx => c.$column.tsx} (81%) diff --git a/package.json b/package.json index dd28be2..25fe525 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,8 @@ "overlayscrollbars-react": "^0.5.6", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-use": "^17.5.1" + "react-use": "^17.5.1", + "zod": "^3.23.8" }, "devDependencies": { "@eslint-react/eslint-plugin": "^1.14.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa5aca7..6ae0818 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,6 +108,9 @@ importers: react-use: specifier: ^17.5.1 version: 17.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + zod: + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@eslint-react/eslint-plugin': specifier: ^1.14.3 diff --git a/schema.sql b/schema.sql deleted file mode 100644 index 71e141e..0000000 --- a/schema.sql +++ /dev/null @@ -1,6 +0,0 @@ -DROP TABLE IF EXISTS cache; -CREATE TABLE IF NOT EXISTS cache ( - id TEXT PRIMARY KEY, - updated INTEGER, - data TEXT -); \ No newline at end of file diff --git a/server/database/user.ts b/server/database/user.ts index b5eacb9..e07f5ea 100644 --- a/server/database/user.ts +++ b/server/database/user.ts @@ -29,7 +29,7 @@ export class UserTable { .run(id, email, "", type, now, now) logger.success(`add user ${id}`) } else if (u.email !== email && u.type !== type) { - await this.db.prepare(`REPLACE INTO user (id, email, updated) VALUES (?, ?, ?)`).run(id, email, now) + await this.db.prepare(`UPDATE user SET email = ?, updated = ? WHERE id = ?`).run(id, email, now) logger.success(`update user ${id} email`) } else { logger.info(`user ${id} already exists`) @@ -40,20 +40,19 @@ export class UserTable { return (await this.db.prepare(`SELECT id, email, data, created, updated FROM user WHERE id = ?`).get(id)) as UserInfo } - async setData(key: string, value: string) { - const now = Date.now() + async setData(key: string, value: string, updatedTime = Date.now()) { const state = await this.db.prepare( - `REPLACE INTO user (id, data, updated) VALUES (?, ?, ?)`, - ).run(key, JSON.stringify(value), now) + `UPDATE user SET data = ?, updated = ? WHERE id = ?`, + ).run(value, updatedTime, key) if (!state.success) throw new Error(`set user ${key} data failed`) - logger.success(`set ${key} cache`) + logger.success(`set ${key} data`) } async getData(id: string) { - const row: any = await this.db.prepare(`SELECT data, update FROM user WHERE id = ?`).get(id) - if (!row || !row.data) throw new Error(`user ${id} not found`) + const row: any = await this.db.prepare(`SELECT data, updated FROM user WHERE id = ?`).get(id) + if (!row) throw new Error(`user ${id} not found`) logger.success(`get ${id} data`) - return row.data as { + return row as { data: string updated: number } diff --git a/server/middleware/auth.ts b/server/middleware/auth.ts index 292b144..c7f0a95 100644 --- a/server/middleware/auth.ts +++ b/server/middleware/auth.ts @@ -2,10 +2,11 @@ import process from "node:process" import { jwtVerify } from "jose" export default defineEventHandler(async (event) => { + const url = getRequestURL(event) if (["JWT_SECRET", "G_CLIENT_ID", "G_CLIENT_SECRET"].find(k => !process.env[k])) { event.context.disabledLogin = true + if (url.pathname.startsWith("/me")) throw createError({ statusCode: 506, message: "Server not configured" }) } else { - const url = getRequestURL(event) if (/^\/(?:me|s)\//.test(url.pathname)) { const token = getHeader(event, "Authorization") if (token && process.env.JWT_SECRET) { @@ -18,7 +19,8 @@ export default defineEventHandler(async (event) => { } } } catch { - logger.error("JWT verification failed") + if (url.pathname.startsWith("/me")) throw createError({ statusCode: 401, message: "JWT verification failed" }) + logger.warn("JWT verification failed") } } } diff --git a/server/routes/me/index.ts b/server/routes/me/index.ts new file mode 100644 index 0000000..36c8425 --- /dev/null +++ b/server/routes/me/index.ts @@ -0,0 +1,5 @@ +export default defineEventHandler(() => { + return { + hello: "world", + } +}) diff --git a/server/routes/me/sync.ts b/server/routes/me/sync.ts new file mode 100644 index 0000000..3c07252 --- /dev/null +++ b/server/routes/me/sync.ts @@ -0,0 +1,33 @@ +import { verifyPrimitiveMetadata } from "@shared/verify" +import { UserTable } from "#/database/user" + +export default defineEventHandler(async (event) => { + try { + const { id } = event.context.user + const db = useDatabase() + if (!db) throw new Error("Not found database") + const userTable = new UserTable(db) + if (event.method === "GET") { + const { data, updated } = await userTable.getData(id) + return { + data: data ? JSON.parse(data) : undefined, + updatedTime: updated, + } + } else if (event.method === "POST") { + const body = await readBody(event) + verifyPrimitiveMetadata(body) + const { updatedTime, data } = body + await userTable.setData(id, JSON.stringify(data), updatedTime) + return { + success: true, + updatedTime, + } + } + } catch (e) { + logger.error(e) + throw createError({ + statusCode: 500, + message: e instanceof Error ? e.message : "Internal Server Error", + }) + } +}) diff --git a/server/routes/oauth/github.ts b/server/routes/oauth/github.ts index 46fd9b0..ebb44c2 100644 --- a/server/routes/oauth/github.ts +++ b/server/routes/oauth/github.ts @@ -43,6 +43,7 @@ export default defineEventHandler(async (event) => { }, }) + console.log(userInfo) const userID = String(userInfo.id) await userTable.addUser(userID, userInfo.notification_email || userInfo.email, "github") diff --git a/server/routes/s/[id].ts b/server/routes/s/[id].ts index 8fe11f1..4344cb2 100644 --- a/server/routes/s/[id].ts +++ b/server/routes/s/[id].ts @@ -13,7 +13,7 @@ export default defineEventHandler(async (event): Promise => { const isValid = (id: SourceID) => !id || !sources[id] || !sourcesFn[id] if (isValid(id)) { - const redirectID = sources[id].redirect + const redirectID = sources?.[id].redirect if (redirectID) id = redirectID if (isValid(id)) throw new Error("Invalid source id") } @@ -31,10 +31,8 @@ export default defineEventHandler(async (event): Promise => { if (now - cache.updated < interval) { return { status: "success", - data: { - updatedTime: now, - items: cache.data, - }, + updatedTime: now, + items: cache.data, } } @@ -50,10 +48,8 @@ export default defineEventHandler(async (event): Promise => { if (event.context.disabledLogin) { return { status: "cache", - data: { - updatedTime: cache.updated, - items: cache.data, - }, + updatedTime: cache.updated, + items: cache.data, } } } @@ -66,16 +62,14 @@ export default defineEventHandler(async (event): Promise => { if (cacheTable) event.waitUntil(cacheTable.set(id, data)) return { status: "success", - data: { - updatedTime: now, - items: data, - }, + updatedTime: now, + items: data, } } catch (e: any) { logger.error(e) - return { - status: "error", - message: e.message ?? e, - } + throw createError({ + statusCode: 500, + message: e instanceof Error ? e.message : "Internal Server Error", + }) } }) diff --git a/shared/types.ts b/shared/types.ts index 8165261..334f8de 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -18,6 +18,12 @@ export type SourceID = { export type ColumnID = (typeof columnIds)[number] export type Metadata = Record +export interface PrimitiveMetadata { + updatedTime: number + data: Record + action: "init" | "manual" | "sync" +} + export interface OriginSource { name: string title?: string @@ -63,16 +69,8 @@ export interface NewsItem { extra?: Record } -// 路由数据 -export interface SourceInfo { +export interface SourceResponse { + status: "success" | "cache" updatedTime: number | string items: NewsItem[] } - -export type SourceResponse = { - status: "success" | "cache" - data: SourceInfo -} | { - status: "error" - message?: string -} diff --git a/shared/verify.ts b/shared/verify.ts new file mode 100644 index 0000000..1cd8254 --- /dev/null +++ b/shared/verify.ts @@ -0,0 +1,8 @@ +import z from "zod" + +export function verifyPrimitiveMetadata(target: any) { + return z.object({ + data: z.record(z.string(), z.array(z.string())), + updatedTime: z.number(), + }).parse(target) +} diff --git a/src/atoms/atomWithLocalStorage.ts b/src/atoms/atomWithLocalStorage.ts deleted file mode 100644 index 766bba7..0000000 --- a/src/atoms/atomWithLocalStorage.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { metadata } from "@shared/metadata" -import { typeSafeObjectEntries, typeSafeObjectFromEntries } from "@shared/type.util" -import type { ColumnID, SourceID } from "@shared/types" -import type { PrimitiveAtom } from "jotai" -import { atom } from "jotai" -import { ofetch } from "ofetch" - -function sync(nextValue: any) { - if (__ENABLE_LOGIN__) { - const jwt = localStorage.getItem("user_jwt") - if (jwt) { - ofetch("/me/sync", { - method: "POST", - body: { data: nextValue }, - headers: { - Authorization: `Bearer ${localStorage.getItem("user_jwt")}`, - }, - }).then(console.log).catch(e => console.error(e)) - } - } -} - -export function atomWithLocalStorage( - key: string, - initialValue: T | (() => T), - initFn?: ((stored: T) => T), - storage?: (next: T) => void, -): PrimitiveAtom { - const getInitialValue = () => { - const item = localStorage.getItem(key) - try { - if (item !== null) { - const stored = JSON.parse(item) - if (initFn) return initFn(stored) - return stored - } - } catch { - // - } - if (initialValue instanceof Function) return initialValue() - else return initialValue - } - const baseAtom = atom(getInitialValue()) - const derivedAtom = atom( - get => get(baseAtom), - (get, set, update: any) => { - const nextValue = typeof update === "function" ? update(get(baseAtom)) : update - set(baseAtom, nextValue) - localStorage.setItem(key, JSON.stringify(nextValue)) - storage?.(nextValue) - }, - ) - return derivedAtom -} - -const initialSources = typeSafeObjectFromEntries(typeSafeObjectEntries(metadata).map(([id, val]) => [id, val.sources])) -export const localSourcesAtom = atomWithLocalStorage>("localsources", initialSources, (stored) => { - return typeSafeObjectFromEntries(typeSafeObjectEntries({ - ...initialSources, - ...stored, - }).filter(([id]) => initialSources[id]).map(([id, val]) => { - if (id === "focus") return [id, val] - const oldS = val.filter(k => initialSources[id].includes(k)) - const newS = initialSources[id].filter(k => !oldS.includes(k)) - return [id, [...oldS, ...newS]] - })) -}, sync) diff --git a/src/atoms/index.ts b/src/atoms/index.ts index dd3b8c1..aad6ccd 100644 --- a/src/atoms/index.ts +++ b/src/atoms/index.ts @@ -1,32 +1,22 @@ import { atom } from "jotai" import type { ColumnID, SourceID } from "@shared/types" -import { metadata } from "@shared/metadata" import { sources } from "@shared/sources" -import { typeSafeObjectEntries, typeSafeObjectFromEntries } from "@shared/type.util" -import { atomWithLocalStorage } from "./atomWithLocalStorage" +import { primitiveMetadataAtom } from "./primitiveMetadataAtom" +import type { Update } from "./types" -const initialSources = typeSafeObjectFromEntries(typeSafeObjectEntries(metadata).map(([id, val]) => [id, val.sources])) -export const localSourcesAtom = atomWithLocalStorage>("localsources", () => { - return initialSources -}, (stored) => { - return typeSafeObjectFromEntries(typeSafeObjectEntries({ - ...initialSources, - ...stored, - }).filter(([id]) => initialSources[id]).map(([id, val]) => { - if (id === "focus") return [id, val] - const oldS = val.filter(k => initialSources[id].includes(k)) - const newS = initialSources[id].filter(k => !oldS.includes(k)) - return [id, [...oldS, ...newS]] - })) -}) +export { primitiveMetadataAtom, preprocessMetadata } from "./primitiveMetadataAtom" export const focusSourcesAtom = atom((get) => { - return get(localSourcesAtom).focus + return get(primitiveMetadataAtom).data.focus }, (get, set, update: Update) => { const _ = update instanceof Function ? update(get(focusSourcesAtom)) : update - set(localSourcesAtom, { - ...get(localSourcesAtom), - focus: _, + set(primitiveMetadataAtom, { + updatedTime: Date.now(), + action: "manual", + data: { + ...get(primitiveMetadataAtom).data, + focus: _, + }, }) }) @@ -47,19 +37,21 @@ export const refetchSourcesAtom = atom(initRefetchSources()) export const currentColumnIDAtom = atom("focus") -export const currentColumnAtom = atom((get) => { +export const currentSourcesAtom = atom((get) => { const id = get(currentColumnIDAtom) - return get(localSourcesAtom)[id] + return get(primitiveMetadataAtom).data[id] }, (get, set, update: Update) => { - const _ = update instanceof Function ? update(get(currentColumnAtom)) : update - set(localSourcesAtom, { - ...get(localSourcesAtom), - [get(currentColumnIDAtom)]: _, + const _ = update instanceof Function ? update(get(currentSourcesAtom)) : update + set(primitiveMetadataAtom, { + updatedTime: Date.now(), + action: "manual", + data: { + ...get(primitiveMetadataAtom).data, + [get(currentColumnIDAtom)]: _, + }, }) }) -export type Update = T | ((prev: T) => T) - export const goToTopAtom = atom({ ok: false, fn: undefined as (() => void) | undefined, diff --git a/src/atoms/primitiveMetadataAtom.ts b/src/atoms/primitiveMetadataAtom.ts new file mode 100644 index 0000000..827e9b1 --- /dev/null +++ b/src/atoms/primitiveMetadataAtom.ts @@ -0,0 +1,62 @@ +import { metadata } from "@shared/metadata" +import { typeSafeObjectEntries, typeSafeObjectFromEntries } from "@shared/type.util" +import type { PrimitiveAtom } from "jotai" +import { atom } from "jotai" +import type { PrimitiveMetadata } from "@shared/types" +import { verifyPrimitiveMetadata } from "@shared/verify" +import type { Update } from "./types" + +function createPrimitiveMetadataAtom( + key: string, + initialValue: PrimitiveMetadata, + preprocess: ((stored: PrimitiveMetadata) => PrimitiveMetadata), +): PrimitiveAtom { + const getInitialValue = (): PrimitiveMetadata => { + const item = localStorage.getItem(key) + try { + if (item) { + const stored = JSON.parse(item) as PrimitiveMetadata + verifyPrimitiveMetadata(stored) + return preprocess(stored) + } + } catch { } + return initialValue + } + const baseAtom = atom(getInitialValue()) + const derivedAtom = atom( + get => get(baseAtom), + (get, set, update: Update) => { + const nextValue = update instanceof Function ? update(get(baseAtom)) : update + set(baseAtom, nextValue) + localStorage.setItem(key, JSON.stringify(nextValue)) + }, + ) + return derivedAtom +} + +const initialMetadata = typeSafeObjectFromEntries(typeSafeObjectEntries(metadata).map(([id, val]) => [id, val.sources])) +export function preprocessMetadata(target: PrimitiveMetadata, action: PrimitiveMetadata["action"] = "init") { + return { + data: { + ...initialMetadata, + ...typeSafeObjectFromEntries( + typeSafeObjectEntries(target.data) + .filter(([id]) => initialMetadata[id]) + .map(([id, sources]) => { + if (id === "focus") return [id, sources] + const oldS = sources.filter(k => initialMetadata[id].includes(k)) + const newS = initialMetadata[id].filter(k => !oldS.includes(k)) + return [id, [...oldS, ...newS]] + }), + ), + }, + action, + updatedTime: target.updatedTime, + } as PrimitiveMetadata +} + +export const primitiveMetadataAtom = createPrimitiveMetadataAtom("metadata", { + updatedTime: 0, + data: initialMetadata, + action: "init", +}, preprocessMetadata) diff --git a/src/atoms/types.ts b/src/atoms/types.ts new file mode 100644 index 0000000..5dfd523 --- /dev/null +++ b/src/atoms/types.ts @@ -0,0 +1 @@ +export type Update = T | ((prev: T) => T) diff --git a/src/components/column/card.tsx b/src/components/column/card.tsx index 80ac043..551e5a1 100644 --- a/src/components/column/card.tsx +++ b/src/components/column/card.tsx @@ -1,4 +1,4 @@ -import type { NewsItem, SourceID, SourceInfo, SourceResponse } from "@shared/types" +import type { NewsItem, SourceID, SourceResponse } from "@shared/types" import type { UseQueryResult } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query" import clsx from "clsx" @@ -28,7 +28,7 @@ interface NewsCardProps { } interface Query { - query: UseQueryResult + query: UseQueryResult } export const CardWrapper = forwardRef(({ id, isDragged, handleListeners, style, ...props }, dndRef) => { @@ -111,11 +111,7 @@ function NewsCard({ id, inView, handleListeners }: NewsCardProps) { Authorization: `Bearer ${localStorage.getItem("user_jwt")}`, }, }) - if (response.status === "error") { - throw new Error(response.message) - } else { - return response.data - } + return response }, // refetch 时显示原有的数据 placeholderData: prev => prev, diff --git a/src/components/column/dnd.tsx b/src/components/column/dnd.tsx index b5aa178..56cd229 100644 --- a/src/components/column/dnd.tsx +++ b/src/components/column/dnd.tsx @@ -17,10 +17,10 @@ import { CSS } from "@dnd-kit/utilities" import { motion } from "framer-motion" import type { ItemsProps } from "./card" import { CardOverlay, CardWrapper } from "./card" -import { currentColumnAtom } from "~/atoms" +import { currentSourcesAtom } from "~/atoms" export function Dnd() { - const [items, setItems] = useAtom(currentColumnAtom) + const [items, setItems] = useAtom(currentSourcesAtom) return ( { - const obj = Object.fromEntries(currentColumn.map(id => [id, Date.now()])) + const obj = Object.fromEntries(currentSources.map(id => [id, Date.now()])) setRefetchSource(prev => ({ ...prev, ...obj, })) - }, [currentColumn, setRefetchSource]) + }, [currentSources, setRefetchSource]) const isFetching = useIsFetching({ predicate: (query) => { - return currentColumn.includes(query.queryKey[0] as SourceID) + return currentSources.includes(query.queryKey[0] as SourceID) }, }) diff --git a/src/hooks/useSync.ts b/src/hooks/useSync.ts new file mode 100644 index 0000000..8c3dc22 --- /dev/null +++ b/src/hooks/useSync.ts @@ -0,0 +1,57 @@ +import type { PrimitiveMetadata } from "@shared/types" +import { useAtom } from "jotai" +import { ofetch } from "ofetch" +import { useCallback, useEffect } from "react" +import { preprocessMetadata, primitiveMetadataAtom } from "~/atoms" + +export function useSync() { + const [primitiveMetadata, setPrimitiveMetadata] = useAtom(primitiveMetadataAtom) + const uploadMetadata = useCallback(async () => { + if (!__ENABLE_LOGIN__) return + const jwt = localStorage.getItem("user_jwt") + if (!jwt) return + if (primitiveMetadata.action !== "manual") return + try { + await ofetch("/api/me/sync", { + method: "POST", + headers: { + Authorization: `Bearer ${jwt}`, + }, + body: { + data: primitiveMetadata.data, + updatedTime: primitiveMetadata.updatedTime, + }, + }) + } catch (e) { + console.error(e) + } + }, [primitiveMetadata]) + + const downloadMetadata = useCallback(async () => { + if (!__ENABLE_LOGIN__) return + const jwt = localStorage.getItem("user_jwt") + if (!jwt) return + try { + const { data, updatedTime } = await ofetch("/api/me/sync", { + headers: { + Authorization: `Bearer ${jwt}`, + }, + }) as PrimitiveMetadata + if (data && updatedTime > primitiveMetadata.updatedTime) { + // 不用同步 action 字段 + setPrimitiveMetadata(preprocessMetadata({ action: "sync", data, updatedTime }, "sync")) + } + } catch (e) { + console.error(e) + } + // 只需要在初始化时执行一次 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [setPrimitiveMetadata]) + + useEffect(() => { + downloadMetadata() + }, [setPrimitiveMetadata, downloadMetadata]) + useEffect(() => { + uploadMetadata() + }, [primitiveMetadata, uploadMetadata]) +} diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index a255b68..06a02b8 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -12,7 +12,7 @@ import { Route as rootRoute } from './routes/__root' import { Route as IndexImport } from './routes/index' -import { Route as SColumnImport } from './routes/s.$column' +import { Route as CColumnImport } from './routes/c.$column' // Create/Update Routes @@ -21,8 +21,8 @@ const IndexRoute = IndexImport.update({ getParentRoute: () => rootRoute, } as any) -const SColumnRoute = SColumnImport.update({ - path: '/s/$column', +const CColumnRoute = CColumnImport.update({ + path: '/c/$column', getParentRoute: () => rootRoute, } as any) @@ -37,11 +37,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexImport parentRoute: typeof rootRoute } - '/s/$column': { - id: '/s/$column' - path: '/s/$column' - fullPath: '/s/$column' - preLoaderRoute: typeof SColumnImport + '/c/$column': { + id: '/c/$column' + path: '/c/$column' + fullPath: '/c/$column' + preLoaderRoute: typeof CColumnImport parentRoute: typeof rootRoute } } @@ -51,37 +51,37 @@ declare module '@tanstack/react-router' { export interface FileRoutesByFullPath { '/': typeof IndexRoute - '/s/$column': typeof SColumnRoute + '/c/$column': typeof CColumnRoute } export interface FileRoutesByTo { '/': typeof IndexRoute - '/s/$column': typeof SColumnRoute + '/c/$column': typeof CColumnRoute } export interface FileRoutesById { __root__: typeof rootRoute '/': typeof IndexRoute - '/s/$column': typeof SColumnRoute + '/c/$column': typeof CColumnRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/s/$column' + fullPaths: '/' | '/c/$column' fileRoutesByTo: FileRoutesByTo - to: '/' | '/s/$column' - id: '__root__' | '/' | '/s/$column' + to: '/' | '/c/$column' + id: '__root__' | '/' | '/c/$column' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute - SColumnRoute: typeof SColumnRoute + CColumnRoute: typeof CColumnRoute } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, - SColumnRoute: SColumnRoute, + CColumnRoute: CColumnRoute, } export const routeTree = rootRoute @@ -97,14 +97,14 @@ export const routeTree = rootRoute "filePath": "__root.tsx", "children": [ "/", - "/s/$column" + "/c/$column" ] }, "/": { "filePath": "index.tsx" }, - "/s/$column": { - "filePath": "s.$column.tsx" + "/c/$column": { + "filePath": "c.$column.tsx" } } } diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index a998fe3..c9d78e3 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -9,6 +9,7 @@ import clsx from "clsx" import { Header } from "~/components/header" import { useOnReload } from "~/hooks/useOnReload" import { GlobalOverlayScrollbar } from "~/components/common/overlay-scrollbar" +import { useSync } from "~/hooks/useSync" export const Route = createRootRouteWithContext<{ queryClient: QueryClient @@ -39,6 +40,7 @@ function NotFoundComponent() { function RootComponent() { useOnReload() + useSync() return ( <> { const column = columnIds.find(x => x === params.column.toLowerCase()) - if (!column) - throw new Error(`"${params.column}" is not a valid column.`) + if (!column) throw new Error(`"${params.column}" is not a valid column.`) return { column, } diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 27c818b..999a648 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,7 +1,7 @@ import { createFileRoute } from "@tanstack/react-router" import { useAtomValue } from "jotai" import { useMemo } from "react" -import { localSourcesAtom } from "~/atoms" +import { focusSourcesAtom } from "~/atoms" import { Column } from "~/components/column" import { } from "cookie-es" @@ -10,9 +10,9 @@ export const Route = createFileRoute("/")({ }) function IndexComponent() { - const focusSources = useAtomValue(localSourcesAtom) + const focusSources = useAtomValue(focusSourcesAtom) const id = useMemo(() => { - return focusSources.focus.length ? "focus" : "realtime" + return focusSources.length ? "focus" : "realtime" // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return