feat: sync user action

This commit is contained in:
Ou 2024-10-15 12:05:03 +08:00
parent 051c827ac1
commit e8a2a7e6f0
23 changed files with 260 additions and 180 deletions

View File

@ -53,7 +53,8 @@
"overlayscrollbars-react": "^0.5.6", "overlayscrollbars-react": "^0.5.6",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-use": "^17.5.1" "react-use": "^17.5.1",
"zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@eslint-react/eslint-plugin": "^1.14.3", "@eslint-react/eslint-plugin": "^1.14.3",

3
pnpm-lock.yaml generated
View File

@ -108,6 +108,9 @@ importers:
react-use: react-use:
specifier: ^17.5.1 specifier: ^17.5.1
version: 17.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.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: devDependencies:
'@eslint-react/eslint-plugin': '@eslint-react/eslint-plugin':
specifier: ^1.14.3 specifier: ^1.14.3

View File

@ -1,6 +0,0 @@
DROP TABLE IF EXISTS cache;
CREATE TABLE IF NOT EXISTS cache (
id TEXT PRIMARY KEY,
updated INTEGER,
data TEXT
);

View File

@ -29,7 +29,7 @@ export class UserTable {
.run(id, email, "", type, now, now) .run(id, email, "", type, now, now)
logger.success(`add user ${id}`) logger.success(`add user ${id}`)
} else if (u.email !== email && u.type !== type) { } 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`) logger.success(`update user ${id} email`)
} else { } else {
logger.info(`user ${id} already exists`) 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 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) { async setData(key: string, value: string, updatedTime = Date.now()) {
const now = Date.now()
const state = await this.db.prepare( const state = await this.db.prepare(
`REPLACE INTO user (id, data, updated) VALUES (?, ?, ?)`, `UPDATE user SET data = ?, updated = ? WHERE id = ?`,
).run(key, JSON.stringify(value), now) ).run(value, updatedTime, key)
if (!state.success) throw new Error(`set user ${key} data failed`) 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) { async getData(id: string) {
const row: any = await this.db.prepare(`SELECT data, update FROM user WHERE id = ?`).get(id) const row: any = await this.db.prepare(`SELECT data, updated FROM user WHERE id = ?`).get(id)
if (!row || !row.data) throw new Error(`user ${id} not found`) if (!row) throw new Error(`user ${id} not found`)
logger.success(`get ${id} data`) logger.success(`get ${id} data`)
return row.data as { return row as {
data: string data: string
updated: number updated: number
} }

View File

@ -2,10 +2,11 @@ import process from "node:process"
import { jwtVerify } from "jose" import { jwtVerify } from "jose"
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const url = getRequestURL(event)
if (["JWT_SECRET", "G_CLIENT_ID", "G_CLIENT_SECRET"].find(k => !process.env[k])) { if (["JWT_SECRET", "G_CLIENT_ID", "G_CLIENT_SECRET"].find(k => !process.env[k])) {
event.context.disabledLogin = true event.context.disabledLogin = true
if (url.pathname.startsWith("/me")) throw createError({ statusCode: 506, message: "Server not configured" })
} else { } else {
const url = getRequestURL(event)
if (/^\/(?:me|s)\//.test(url.pathname)) { if (/^\/(?:me|s)\//.test(url.pathname)) {
const token = getHeader(event, "Authorization") const token = getHeader(event, "Authorization")
if (token && process.env.JWT_SECRET) { if (token && process.env.JWT_SECRET) {
@ -18,7 +19,8 @@ export default defineEventHandler(async (event) => {
} }
} }
} catch { } catch {
logger.error("JWT verification failed") if (url.pathname.startsWith("/me")) throw createError({ statusCode: 401, message: "JWT verification failed" })
logger.warn("JWT verification failed")
} }
} }
} }

View File

@ -0,0 +1,5 @@
export default defineEventHandler(() => {
return {
hello: "world",
}
})

33
server/routes/me/sync.ts Normal file
View File

@ -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",
})
}
})

View File

@ -43,6 +43,7 @@ export default defineEventHandler(async (event) => {
}, },
}) })
console.log(userInfo)
const userID = String(userInfo.id) const userID = String(userInfo.id)
await userTable.addUser(userID, userInfo.notification_email || userInfo.email, "github") await userTable.addUser(userID, userInfo.notification_email || userInfo.email, "github")

View File

@ -13,7 +13,7 @@ export default defineEventHandler(async (event): Promise<SourceResponse> => {
const isValid = (id: SourceID) => !id || !sources[id] || !sourcesFn[id] const isValid = (id: SourceID) => !id || !sources[id] || !sourcesFn[id]
if (isValid(id)) { if (isValid(id)) {
const redirectID = sources[id].redirect const redirectID = sources?.[id].redirect
if (redirectID) id = redirectID if (redirectID) id = redirectID
if (isValid(id)) throw new Error("Invalid source id") if (isValid(id)) throw new Error("Invalid source id")
} }
@ -31,10 +31,8 @@ export default defineEventHandler(async (event): Promise<SourceResponse> => {
if (now - cache.updated < interval) { if (now - cache.updated < interval) {
return { return {
status: "success", status: "success",
data: { updatedTime: now,
updatedTime: now, items: cache.data,
items: cache.data,
},
} }
} }
@ -50,10 +48,8 @@ export default defineEventHandler(async (event): Promise<SourceResponse> => {
if (event.context.disabledLogin) { if (event.context.disabledLogin) {
return { return {
status: "cache", status: "cache",
data: { updatedTime: cache.updated,
updatedTime: cache.updated, items: cache.data,
items: cache.data,
},
} }
} }
} }
@ -66,16 +62,14 @@ export default defineEventHandler(async (event): Promise<SourceResponse> => {
if (cacheTable) event.waitUntil(cacheTable.set(id, data)) if (cacheTable) event.waitUntil(cacheTable.set(id, data))
return { return {
status: "success", status: "success",
data: { updatedTime: now,
updatedTime: now, items: data,
items: data,
},
} }
} catch (e: any) { } catch (e: any) {
logger.error(e) logger.error(e)
return { throw createError({
status: "error", statusCode: 500,
message: e.message ?? e, message: e instanceof Error ? e.message : "Internal Server Error",
} })
} }
}) })

View File

@ -18,6 +18,12 @@ export type SourceID = {
export type ColumnID = (typeof columnIds)[number] export type ColumnID = (typeof columnIds)[number]
export type Metadata = Record<ColumnID, Column> export type Metadata = Record<ColumnID, Column>
export interface PrimitiveMetadata {
updatedTime: number
data: Record<ColumnID, SourceID[]>
action: "init" | "manual" | "sync"
}
export interface OriginSource { export interface OriginSource {
name: string name: string
title?: string title?: string
@ -63,16 +69,8 @@ export interface NewsItem {
extra?: Record<string, any> extra?: Record<string, any>
} }
// 路由数据 export interface SourceResponse {
export interface SourceInfo { status: "success" | "cache"
updatedTime: number | string updatedTime: number | string
items: NewsItem[] items: NewsItem[]
} }
export type SourceResponse = {
status: "success" | "cache"
data: SourceInfo
} | {
status: "error"
message?: string
}

8
shared/verify.ts Normal file
View File

@ -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)
}

View File

@ -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<T>(
key: string,
initialValue: T | (() => T),
initFn?: ((stored: T) => T),
storage?: (next: T) => void,
): PrimitiveAtom<T> {
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<Record<ColumnID, SourceID[]>>("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)

View File

@ -1,32 +1,22 @@
import { atom } from "jotai" import { atom } from "jotai"
import type { ColumnID, SourceID } from "@shared/types" import type { ColumnID, SourceID } from "@shared/types"
import { metadata } from "@shared/metadata"
import { sources } from "@shared/sources" import { sources } from "@shared/sources"
import { typeSafeObjectEntries, typeSafeObjectFromEntries } from "@shared/type.util" import { primitiveMetadataAtom } from "./primitiveMetadataAtom"
import { atomWithLocalStorage } from "./atomWithLocalStorage" import type { Update } from "./types"
const initialSources = typeSafeObjectFromEntries(typeSafeObjectEntries(metadata).map(([id, val]) => [id, val.sources])) export { primitiveMetadataAtom, preprocessMetadata } from "./primitiveMetadataAtom"
export const localSourcesAtom = atomWithLocalStorage<Record<ColumnID, SourceID[]>>("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 const focusSourcesAtom = atom((get) => { export const focusSourcesAtom = atom((get) => {
return get(localSourcesAtom).focus return get(primitiveMetadataAtom).data.focus
}, (get, set, update: Update<SourceID[]>) => { }, (get, set, update: Update<SourceID[]>) => {
const _ = update instanceof Function ? update(get(focusSourcesAtom)) : update const _ = update instanceof Function ? update(get(focusSourcesAtom)) : update
set(localSourcesAtom, { set(primitiveMetadataAtom, {
...get(localSourcesAtom), updatedTime: Date.now(),
focus: _, action: "manual",
data: {
...get(primitiveMetadataAtom).data,
focus: _,
},
}) })
}) })
@ -47,19 +37,21 @@ export const refetchSourcesAtom = atom(initRefetchSources())
export const currentColumnIDAtom = atom<ColumnID>("focus") export const currentColumnIDAtom = atom<ColumnID>("focus")
export const currentColumnAtom = atom((get) => { export const currentSourcesAtom = atom((get) => {
const id = get(currentColumnIDAtom) const id = get(currentColumnIDAtom)
return get(localSourcesAtom)[id] return get(primitiveMetadataAtom).data[id]
}, (get, set, update: Update<SourceID[]>) => { }, (get, set, update: Update<SourceID[]>) => {
const _ = update instanceof Function ? update(get(currentColumnAtom)) : update const _ = update instanceof Function ? update(get(currentSourcesAtom)) : update
set(localSourcesAtom, { set(primitiveMetadataAtom, {
...get(localSourcesAtom), updatedTime: Date.now(),
[get(currentColumnIDAtom)]: _, action: "manual",
data: {
...get(primitiveMetadataAtom).data,
[get(currentColumnIDAtom)]: _,
},
}) })
}) })
export type Update<T> = T | ((prev: T) => T)
export const goToTopAtom = atom({ export const goToTopAtom = atom({
ok: false, ok: false,
fn: undefined as (() => void) | undefined, fn: undefined as (() => void) | undefined,

View File

@ -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<PrimitiveMetadata> {
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<PrimitiveMetadata>) => {
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)

1
src/atoms/types.ts Normal file
View File

@ -0,0 +1 @@
export type Update<T> = T | ((prev: T) => T)

View File

@ -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 type { UseQueryResult } from "@tanstack/react-query"
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import clsx from "clsx" import clsx from "clsx"
@ -28,7 +28,7 @@ interface NewsCardProps {
} }
interface Query { interface Query {
query: UseQueryResult<SourceInfo, Error> query: UseQueryResult<SourceResponse, Error>
} }
export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, isDragged, handleListeners, style, ...props }, dndRef) => { export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, isDragged, handleListeners, style, ...props }, dndRef) => {
@ -111,11 +111,7 @@ function NewsCard({ id, inView, handleListeners }: NewsCardProps) {
Authorization: `Bearer ${localStorage.getItem("user_jwt")}`, Authorization: `Bearer ${localStorage.getItem("user_jwt")}`,
}, },
}) })
if (response.status === "error") { return response
throw new Error(response.message)
} else {
return response.data
}
}, },
// refetch 时显示原有的数据 // refetch 时显示原有的数据
placeholderData: prev => prev, placeholderData: prev => prev,

View File

@ -17,10 +17,10 @@ import { CSS } from "@dnd-kit/utilities"
import { motion } from "framer-motion" import { motion } from "framer-motion"
import type { ItemsProps } from "./card" import type { ItemsProps } from "./card"
import { CardOverlay, CardWrapper } from "./card" import { CardOverlay, CardWrapper } from "./card"
import { currentColumnAtom } from "~/atoms" import { currentSourcesAtom } from "~/atoms"
export function Dnd() { export function Dnd() {
const [items, setItems] = useAtom(currentColumnAtom) const [items, setItems] = useAtom(currentSourcesAtom)
return ( return (
<DndWrapper items={items} setItems={setItems}> <DndWrapper items={items} setItems={setItems}>
<motion.div <motion.div

View File

@ -7,7 +7,7 @@ import type { SourceID } from "@shared/types"
import { Homepage, Version } from "@shared/consts" import { Homepage, Version } from "@shared/consts"
import { useLocalStorage } from "react-use" import { useLocalStorage } from "react-use"
import { useDark } from "~/hooks/useDark" import { useDark } from "~/hooks/useDark"
import { currentColumnAtom, goToTopAtom, refetchSourcesAtom } from "~/atoms" import { currentSourcesAtom, goToTopAtom, refetchSourcesAtom } from "~/atoms"
function ThemeToggle() { function ThemeToggle() {
const { toggleDark } = useDark() const { toggleDark } = useDark()
@ -75,19 +75,19 @@ export function GithubIcon() {
} }
function RefreshButton() { function RefreshButton() {
const currentColumn = useAtomValue(currentColumnAtom) const currentSources = useAtomValue(currentSourcesAtom)
const setRefetchSource = useSetAtom(refetchSourcesAtom) const setRefetchSource = useSetAtom(refetchSourcesAtom)
const refreshAll = useCallback(() => { const refreshAll = useCallback(() => {
const obj = Object.fromEntries(currentColumn.map(id => [id, Date.now()])) const obj = Object.fromEntries(currentSources.map(id => [id, Date.now()]))
setRefetchSource(prev => ({ setRefetchSource(prev => ({
...prev, ...prev,
...obj, ...obj,
})) }))
}, [currentColumn, setRefetchSource]) }, [currentSources, setRefetchSource])
const isFetching = useIsFetching({ const isFetching = useIsFetching({
predicate: (query) => { predicate: (query) => {
return currentColumn.includes(query.queryKey[0] as SourceID) return currentSources.includes(query.queryKey[0] as SourceID)
}, },
}) })

57
src/hooks/useSync.ts Normal file
View File

@ -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])
}

View File

@ -12,7 +12,7 @@
import { Route as rootRoute } from './routes/__root' import { Route as rootRoute } from './routes/__root'
import { Route as IndexImport } from './routes/index' 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 // Create/Update Routes
@ -21,8 +21,8 @@ const IndexRoute = IndexImport.update({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)
const SColumnRoute = SColumnImport.update({ const CColumnRoute = CColumnImport.update({
path: '/s/$column', path: '/c/$column',
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)
@ -37,11 +37,11 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexImport preLoaderRoute: typeof IndexImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/s/$column': { '/c/$column': {
id: '/s/$column' id: '/c/$column'
path: '/s/$column' path: '/c/$column'
fullPath: '/s/$column' fullPath: '/c/$column'
preLoaderRoute: typeof SColumnImport preLoaderRoute: typeof CColumnImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
} }
@ -51,37 +51,37 @@ declare module '@tanstack/react-router' {
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/s/$column': typeof SColumnRoute '/c/$column': typeof CColumnRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/s/$column': typeof SColumnRoute '/c/$column': typeof CColumnRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRoute __root__: typeof rootRoute
'/': typeof IndexRoute '/': typeof IndexRoute
'/s/$column': typeof SColumnRoute '/c/$column': typeof CColumnRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/s/$column' fullPaths: '/' | '/c/$column'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: '/' | '/s/$column' to: '/' | '/c/$column'
id: '__root__' | '/' | '/s/$column' id: '__root__' | '/' | '/c/$column'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
SColumnRoute: typeof SColumnRoute CColumnRoute: typeof CColumnRoute
} }
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
SColumnRoute: SColumnRoute, CColumnRoute: CColumnRoute,
} }
export const routeTree = rootRoute export const routeTree = rootRoute
@ -97,14 +97,14 @@ export const routeTree = rootRoute
"filePath": "__root.tsx", "filePath": "__root.tsx",
"children": [ "children": [
"/", "/",
"/s/$column" "/c/$column"
] ]
}, },
"/": { "/": {
"filePath": "index.tsx" "filePath": "index.tsx"
}, },
"/s/$column": { "/c/$column": {
"filePath": "s.$column.tsx" "filePath": "c.$column.tsx"
} }
} }
} }

View File

@ -9,6 +9,7 @@ import clsx from "clsx"
import { Header } from "~/components/header" import { Header } from "~/components/header"
import { useOnReload } from "~/hooks/useOnReload" import { useOnReload } from "~/hooks/useOnReload"
import { GlobalOverlayScrollbar } from "~/components/common/overlay-scrollbar" import { GlobalOverlayScrollbar } from "~/components/common/overlay-scrollbar"
import { useSync } from "~/hooks/useSync"
export const Route = createRootRouteWithContext<{ export const Route = createRootRouteWithContext<{
queryClient: QueryClient queryClient: QueryClient
@ -39,6 +40,7 @@ function NotFoundComponent() {
function RootComponent() { function RootComponent() {
useOnReload() useOnReload()
useSync()
return ( return (
<> <>
<GlobalOverlayScrollbar className={clsx([ <GlobalOverlayScrollbar className={clsx([

View File

@ -2,13 +2,12 @@ import { createFileRoute, redirect } from "@tanstack/react-router"
import { columnIds } from "@shared/metadata" import { columnIds } from "@shared/metadata"
import { Column } from "~/components/column" import { Column } from "~/components/column"
export const Route = createFileRoute("/s/$column")({ export const Route = createFileRoute("/c/$column")({
component: SectionComponent, component: SectionComponent,
params: { params: {
parse: (params) => { parse: (params) => {
const column = columnIds.find(x => x === params.column.toLowerCase()) const column = columnIds.find(x => x === params.column.toLowerCase())
if (!column) if (!column) throw new Error(`"${params.column}" is not a valid column.`)
throw new Error(`"${params.column}" is not a valid column.`)
return { return {
column, column,
} }

View File

@ -1,7 +1,7 @@
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute } from "@tanstack/react-router"
import { useAtomValue } from "jotai" import { useAtomValue } from "jotai"
import { useMemo } from "react" import { useMemo } from "react"
import { localSourcesAtom } from "~/atoms" import { focusSourcesAtom } from "~/atoms"
import { Column } from "~/components/column" import { Column } from "~/components/column"
import { } from "cookie-es" import { } from "cookie-es"
@ -10,9 +10,9 @@ export const Route = createFileRoute("/")({
}) })
function IndexComponent() { function IndexComponent() {
const focusSources = useAtomValue(localSourcesAtom) const focusSources = useAtomValue(focusSourcesAtom)
const id = useMemo(() => { const id = useMemo(() => {
return focusSources.focus.length ? "focus" : "realtime" return focusSources.length ? "focus" : "realtime"
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
return <Column id={id} /> return <Column id={id} />