mirror of
https://github.com/ourongxing/newsnow.git
synced 2025-01-19 03:09:14 +08:00
feat: sync user action
This commit is contained in:
parent
051c827ac1
commit
e8a2a7e6f0
@ -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",
|
||||
|
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@ -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
|
||||
|
@ -1,6 +0,0 @@
|
||||
DROP TABLE IF EXISTS cache;
|
||||
CREATE TABLE IF NOT EXISTS cache (
|
||||
id TEXT PRIMARY KEY,
|
||||
updated INTEGER,
|
||||
data TEXT
|
||||
);
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
5
server/routes/me/index.ts
Normal file
5
server/routes/me/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default defineEventHandler(() => {
|
||||
return {
|
||||
hello: "world",
|
||||
}
|
||||
})
|
33
server/routes/me/sync.ts
Normal file
33
server/routes/me/sync.ts
Normal 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",
|
||||
})
|
||||
}
|
||||
})
|
@ -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")
|
||||
|
||||
|
@ -13,7 +13,7 @@ export default defineEventHandler(async (event): Promise<SourceResponse> => {
|
||||
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<SourceResponse> => {
|
||||
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<SourceResponse> => {
|
||||
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<SourceResponse> => {
|
||||
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",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -18,6 +18,12 @@ export type SourceID = {
|
||||
export type ColumnID = (typeof columnIds)[number]
|
||||
export type Metadata = Record<ColumnID, Column>
|
||||
|
||||
export interface PrimitiveMetadata {
|
||||
updatedTime: number
|
||||
data: Record<ColumnID, SourceID[]>
|
||||
action: "init" | "manual" | "sync"
|
||||
}
|
||||
|
||||
export interface OriginSource {
|
||||
name: string
|
||||
title?: string
|
||||
@ -63,16 +69,8 @@ export interface NewsItem {
|
||||
extra?: Record<string, any>
|
||||
}
|
||||
|
||||
// 路由数据
|
||||
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
|
||||
}
|
||||
|
8
shared/verify.ts
Normal file
8
shared/verify.ts
Normal 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)
|
||||
}
|
@ -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)
|
@ -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<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 { primitiveMetadataAtom, preprocessMetadata } from "./primitiveMetadataAtom"
|
||||
|
||||
export const focusSourcesAtom = atom((get) => {
|
||||
return get(localSourcesAtom).focus
|
||||
return get(primitiveMetadataAtom).data.focus
|
||||
}, (get, set, update: Update<SourceID[]>) => {
|
||||
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<ColumnID>("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<SourceID[]>) => {
|
||||
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> = T | ((prev: T) => T)
|
||||
|
||||
export const goToTopAtom = atom({
|
||||
ok: false,
|
||||
fn: undefined as (() => void) | undefined,
|
||||
|
62
src/atoms/primitiveMetadataAtom.ts
Normal file
62
src/atoms/primitiveMetadataAtom.ts
Normal 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
1
src/atoms/types.ts
Normal file
@ -0,0 +1 @@
|
||||
export type Update<T> = T | ((prev: T) => T)
|
@ -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<SourceInfo, Error>
|
||||
query: UseQueryResult<SourceResponse, Error>
|
||||
}
|
||||
|
||||
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")}`,
|
||||
},
|
||||
})
|
||||
if (response.status === "error") {
|
||||
throw new Error(response.message)
|
||||
} else {
|
||||
return response.data
|
||||
}
|
||||
return response
|
||||
},
|
||||
// refetch 时显示原有的数据
|
||||
placeholderData: prev => prev,
|
||||
|
@ -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 (
|
||||
<DndWrapper items={items} setItems={setItems}>
|
||||
<motion.div
|
||||
|
@ -7,7 +7,7 @@ import type { SourceID } from "@shared/types"
|
||||
import { Homepage, Version } from "@shared/consts"
|
||||
import { useLocalStorage } from "react-use"
|
||||
import { useDark } from "~/hooks/useDark"
|
||||
import { currentColumnAtom, goToTopAtom, refetchSourcesAtom } from "~/atoms"
|
||||
import { currentSourcesAtom, goToTopAtom, refetchSourcesAtom } from "~/atoms"
|
||||
|
||||
function ThemeToggle() {
|
||||
const { toggleDark } = useDark()
|
||||
@ -75,19 +75,19 @@ export function GithubIcon() {
|
||||
}
|
||||
|
||||
function RefreshButton() {
|
||||
const currentColumn = useAtomValue(currentColumnAtom)
|
||||
const currentSources = useAtomValue(currentSourcesAtom)
|
||||
const setRefetchSource = useSetAtom(refetchSourcesAtom)
|
||||
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 => ({
|
||||
...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)
|
||||
},
|
||||
})
|
||||
|
||||
|
57
src/hooks/useSync.ts
Normal file
57
src/hooks/useSync.ts
Normal 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])
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 (
|
||||
<>
|
||||
<GlobalOverlayScrollbar className={clsx([
|
||||
|
@ -2,13 +2,12 @@ import { createFileRoute, redirect } from "@tanstack/react-router"
|
||||
import { columnIds } from "@shared/metadata"
|
||||
import { Column } from "~/components/column"
|
||||
|
||||
export const Route = createFileRoute("/s/$column")({
|
||||
export const Route = createFileRoute("/c/$column")({
|
||||
component: SectionComponent,
|
||||
params: {
|
||||
parse: (params) => {
|
||||
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,
|
||||
}
|
@ -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 <Column id={id} />
|
||||
|
Loading…
x
Reference in New Issue
Block a user