mirror of
https://github.com/Afilmory/afilmory
synced 2026-04-24 23:05:05 +00:00
fix: remove unnecessary global flag from regex patterns
Co-authored-by: Innei <41265413+Innei@users.noreply.github.com>
This commit is contained in:
@@ -442,7 +442,7 @@ export class PhotoAssetService {
|
||||
return null
|
||||
}
|
||||
|
||||
const normalized = trimmed.replaceAll('\\', '/').replaceAll(/^\/+|\/+$/g, '')
|
||||
const normalized = trimmed.replaceAll('\\', '/').replaceAll(/^\/+|\/+$/, '')
|
||||
return normalized.length > 0 ? normalized : null
|
||||
}
|
||||
|
||||
@@ -452,7 +452,7 @@ export class PhotoAssetService {
|
||||
if (!segment) {
|
||||
continue
|
||||
}
|
||||
filtered.push(segment.replaceAll(/^\/+|\/+$/g, ''))
|
||||
filtered.push(segment.replaceAll(/^\/+|\/+$/, ''))
|
||||
}
|
||||
|
||||
if (filtered.length === 0) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const sortObjectKeys = (obj) => {
|
||||
function sortObjectKeys(obj) {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return obj
|
||||
}
|
||||
|
||||
@@ -13,14 +13,14 @@ export const App: FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const AppLayer = () => {
|
||||
function AppLayer() {
|
||||
usePageRedirect()
|
||||
|
||||
const appIsReady = true
|
||||
return appIsReady ? <Outlet /> : <AppSkeleton />
|
||||
}
|
||||
|
||||
const AppSkeleton = () => {
|
||||
function AppSkeleton() {
|
||||
return null
|
||||
}
|
||||
export default App
|
||||
|
||||
@@ -9,22 +9,22 @@ export const [authUserAtom, useAuthUser, useAuthUserValue, useSetAuthUser, getAu
|
||||
createAtomHooks(baseAuthUserAtom)
|
||||
|
||||
// Selectors
|
||||
export const useIsAuthenticated = () => {
|
||||
export function useIsAuthenticated() {
|
||||
const user = useAuthUserValue()
|
||||
return !!user
|
||||
}
|
||||
|
||||
export const useUserRole = () => {
|
||||
export function useUserRole() {
|
||||
const user = useAuthUserValue()
|
||||
return user?.role ?? null
|
||||
}
|
||||
|
||||
export const useIsAdmin = () => {
|
||||
export function useIsAdmin() {
|
||||
const user = useAuthUserValue()
|
||||
return user?.role === 'admin' || user?.role === 'superadmin'
|
||||
}
|
||||
|
||||
export const useIsSuperAdmin = () => {
|
||||
export function useIsSuperAdmin() {
|
||||
const user = useAuthUserValue()
|
||||
return user?.role === 'superadmin'
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@ export const [contextMenuAtom, useContextMenuState, useContextMenuValue, useSetC
|
||||
atom<ContextMenuState>({ open: false }),
|
||||
)
|
||||
|
||||
const useShowWebContextMenu = () => {
|
||||
function useShowWebContextMenu() {
|
||||
const setContextMenu = useSetContextMenu()
|
||||
|
||||
const showWebContextMenu = useCallback(
|
||||
async (menuItems: Array<FollowMenuItem>, e: MouseEvent | React.MouseEvent) => {
|
||||
async (menuItems: FollowMenuItem[], e: MouseEvent | React.MouseEvent) => {
|
||||
const abortController = new AbortController()
|
||||
const resolvers = Promise.withResolvers<void>()
|
||||
setContextMenu({
|
||||
@@ -75,11 +75,11 @@ export enum MenuItemType {
|
||||
Action,
|
||||
}
|
||||
|
||||
export const useShowContextMenu = () => {
|
||||
export function useShowContextMenu() {
|
||||
const showWebContextMenu = useShowWebContextMenu()
|
||||
|
||||
const showContextMenu = useCallback(
|
||||
async (inputMenu: Array<MenuItemInput>, e: MouseEvent | React.MouseEvent) => {
|
||||
async (inputMenu: MenuItemInput[], e: MouseEvent | React.MouseEvent) => {
|
||||
const menuItems = filterNullableMenuItems(inputMenu)
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
@@ -25,7 +25,7 @@ export const [routeAtom, , , , getReadonlyRoute, setRoute] = createAtomHooks(
|
||||
}),
|
||||
)
|
||||
|
||||
export const useReadonlyRouteSelector = <T>(selector: (route: RouteAtom) => T): T => {
|
||||
export function useReadonlyRouteSelector<T>(selector: (route: RouteAtom) => T): T {
|
||||
const memoizedAtom = useMemo(() => selectAtom(routeAtom, (route) => selector(route)), [selector])
|
||||
|
||||
return useAtomValue(memoizedAtom)
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { clsxm } from '@afilmory/utils'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export const LinearBorderPanel = ({ className, children }: { className?: string; children: ReactNode }) => (
|
||||
<div className={clsxm('group relative overflow-hidden', className)}>
|
||||
{/* Linear gradient borders - sharp edges */}
|
||||
<div className="via-text/20 absolute top-0 right-0 left-0 h-[0.5px] bg-linear-to-r from-transparent to-transparent" />
|
||||
<div className="via-text/20 absolute top-0 right-0 bottom-0 w-[0.5px] bg-linear-to-b from-transparent to-transparent" />
|
||||
<div className="via-text/20 absolute right-0 bottom-0 left-0 h-[0.5px] bg-linear-to-r from-transparent to-transparent" />
|
||||
<div className="via-text/20 absolute top-0 bottom-0 left-0 w-[0.5px] bg-linear-to-b from-transparent to-transparent" />
|
||||
export function LinearBorderPanel({ className, children }: { className?: string; children: ReactNode }) {
|
||||
return (
|
||||
<div className={clsxm('group relative overflow-hidden', className)}>
|
||||
{/* Linear gradient borders - sharp edges */}
|
||||
<div className="via-text/20 absolute top-0 right-0 left-0 h-[0.5px] bg-linear-to-r from-transparent to-transparent" />
|
||||
<div className="via-text/20 absolute top-0 right-0 bottom-0 w-[0.5px] bg-linear-to-b from-transparent to-transparent" />
|
||||
<div className="via-text/20 absolute right-0 bottom-0 left-0 h-[0.5px] bg-linear-to-r from-transparent to-transparent" />
|
||||
<div className="via-text/20 absolute top-0 bottom-0 left-0 w-[0.5px] bg-linear-to-b from-transparent to-transparent" />
|
||||
|
||||
<div className="relative">{children}</div>
|
||||
</div>
|
||||
)
|
||||
<div className="relative">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from '@afilmory/ui'
|
||||
import { useLocation, useNavigate } from 'react-router'
|
||||
|
||||
export const NotFound = () => {
|
||||
export function NotFound() {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ type MainPageLayoutContextValue = {
|
||||
|
||||
const MainPageLayoutContext = createContext<MainPageLayoutContextValue | null>(null)
|
||||
|
||||
export const useMainPageLayout = () => {
|
||||
export function useMainPageLayout() {
|
||||
const context = use(MainPageLayoutContext)
|
||||
|
||||
if (!context) {
|
||||
@@ -41,7 +41,7 @@ type MainPageLayoutProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const MainPageLayoutBase = ({ title, description, actions, footer, children }: MainPageLayoutProps) => {
|
||||
function MainPageLayoutBase({ title, description, actions, footer, children }: MainPageLayoutProps) {
|
||||
const [headerActionsContainer, setHeaderActionsContainer] = useState<HTMLDivElement | null>(null)
|
||||
const [portalMountCount, setPortalMountCount] = useState(0)
|
||||
const [headerActionState, setHeaderActionState] = useState<HeaderActionState>(defaultHeaderActionState)
|
||||
@@ -109,7 +109,7 @@ type MainPageLayoutActionsProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const MainPageLayoutActions = ({ children }: MainPageLayoutActionsProps) => {
|
||||
function MainPageLayoutActions({ children }: MainPageLayoutActionsProps) {
|
||||
const { headerActionsContainer, registerPortalPresence } = useMainPageLayout()
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -18,7 +18,7 @@ export interface PageTabsProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const PageTabs = ({ items, activeId, onSelect, className }: PageTabsProps) => {
|
||||
export function PageTabs({ items, activeId, onSelect, className }: PageTabsProps) {
|
||||
const renderTabContent = (selected: boolean, label: ReactNode) => (
|
||||
<span
|
||||
className={clsxm(
|
||||
|
||||
@@ -18,7 +18,7 @@ const DEFAULT_CONFIRM_TEXT = '继续离开'
|
||||
const DEFAULT_CANCEL_TEXT = '留在此页'
|
||||
const DEFAULT_BEFORE_UNLOAD_MESSAGE = '您有未保存的更改,确定要离开吗?'
|
||||
|
||||
export const useBlock = ({
|
||||
export function useBlock({
|
||||
when,
|
||||
title = DEFAULT_TITLE,
|
||||
description = DEFAULT_DESCRIPTION,
|
||||
@@ -26,7 +26,7 @@ export const useBlock = ({
|
||||
cancelText = DEFAULT_CANCEL_TEXT,
|
||||
variant = 'danger',
|
||||
beforeUnloadMessage = DEFAULT_BEFORE_UNLOAD_MESSAGE,
|
||||
}: UseBlockOptions) => {
|
||||
}: UseBlockOptions) {
|
||||
const promptIdRef = useRef<string | null>(null)
|
||||
const isPromptOpenRef = useRef(false)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ const AUTH_FAILURE_STATUSES = new Set([401, 403, 419])
|
||||
|
||||
const PUBLIC_PATHS = new Set([DEFAULT_LOGIN_PATH, DEFAULT_ONBOARDING_PATH])
|
||||
|
||||
export const usePageRedirect = () => {
|
||||
export function usePageRedirect() {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const CAMELIZE_PATTERN = /[_.-](\w|$)/g
|
||||
|
||||
const camelCase = (value: string): string => {
|
||||
function camelCase(value: string): string {
|
||||
if (!value || (!value.includes('_') && !value.includes('-') && !value.includes('.'))) {
|
||||
return value
|
||||
}
|
||||
@@ -8,7 +8,7 @@ const camelCase = (value: string): string => {
|
||||
return value.replaceAll(CAMELIZE_PATTERN, (_match, group: string) => group.toUpperCase())
|
||||
}
|
||||
|
||||
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return false
|
||||
}
|
||||
@@ -17,7 +17,7 @@ const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
||||
return prototype === Object.prototype || prototype === null
|
||||
}
|
||||
|
||||
export const camelCaseKeys = <T>(input: unknown): T => {
|
||||
export function camelCaseKeys<T>(input: unknown): T {
|
||||
if (Array.isArray(input)) {
|
||||
return input.map((item) => camelCaseKeys(item)) as unknown as T
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
declare const APP_DEV_CWD: string
|
||||
export const attachOpenInEditor = (stack: string) => {
|
||||
export function attachOpenInEditor(stack: string) {
|
||||
const lines = stack.split('\n')
|
||||
return lines.map((line) => {
|
||||
// A line like this: at App (http://localhost:5173/src/App.tsx?t=1720527056591:41:9)
|
||||
@@ -47,6 +47,6 @@ export const attachOpenInEditor = (stack: string) => {
|
||||
})
|
||||
}
|
||||
// http://localhost:5173/src/App.tsx?t=1720527056591:41:9
|
||||
const openInEditor = (file: string) => {
|
||||
function openInEditor(file: string) {
|
||||
fetch(`/__open-in-editor?file=${encodeURIComponent(`${file}`)}`)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ export const stopPropagation: ReactEventHandler<any> = (e) => e.stopPropagation(
|
||||
|
||||
export const preventDefault: ReactEventHandler<any> = (e) => e.preventDefault()
|
||||
|
||||
export const nextFrame = (fn: (...args: any[]) => any) => {
|
||||
export function nextFrame(fn: (...args: any[]) => any) {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
fn()
|
||||
@@ -12,7 +12,7 @@ export const nextFrame = (fn: (...args: any[]) => any) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const getElementTop = (element: HTMLElement) => {
|
||||
export function getElementTop(element: HTMLElement) {
|
||||
let actualTop = element.offsetTop
|
||||
let current = element.offsetParent as HTMLElement
|
||||
while (current !== null) {
|
||||
|
||||
@@ -5,24 +5,26 @@ import { useMemo } from 'react'
|
||||
|
||||
export const jotaiStore = createStore()
|
||||
|
||||
export const createAtomAccessor = <T>(atom: PrimitiveAtom<T>) =>
|
||||
[() => jotaiStore.get(atom), (value: T) => jotaiStore.set(atom, value)] as const
|
||||
export function createAtomAccessor<T>(atom: PrimitiveAtom<T>) {
|
||||
return [() => jotaiStore.get(atom), (value: T) => jotaiStore.set(atom, value)] as const
|
||||
}
|
||||
|
||||
const options = { store: jotaiStore }
|
||||
/**
|
||||
* @param atom - jotai
|
||||
* @returns - [atom, useAtom, useAtomValue, useSetAtom, jotaiStore.get, jotaiStore.set]
|
||||
*/
|
||||
export const createAtomHooks = <T>(atom: PrimitiveAtom<T>) =>
|
||||
[
|
||||
export function createAtomHooks<T>(atom: PrimitiveAtom<T>) {
|
||||
return [
|
||||
atom,
|
||||
() => useAtom(atom, options),
|
||||
() => useAtomValue(atom, options),
|
||||
() => useSetAtom(atom, options),
|
||||
...createAtomAccessor(atom),
|
||||
] as const
|
||||
}
|
||||
|
||||
export const createAtomSelector = <T>(atom: Atom<T>) => {
|
||||
export function createAtomSelector<T>(atom: Atom<T>) {
|
||||
const useHook = <R>(selector: (a: T) => R) => {
|
||||
const memoizedAtom = useMemo(() => selectAtom(atom, (value) => selector(value)), [selector])
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const ns = 'app'
|
||||
export const getStorageNS = (key: string) => `${ns}:${key}`
|
||||
|
||||
export const clearStorage = () => {
|
||||
export function clearStorage() {
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i)
|
||||
if (key && key.startsWith(ns)) {
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface LoginResponse extends SessionResponse {
|
||||
* @param data - Login credentials with email and password
|
||||
* @returns Session response with user and session data
|
||||
*/
|
||||
export const login = async (data: LoginRequest): Promise<LoginResponse> => {
|
||||
export async function login(data: LoginRequest): Promise<LoginResponse> {
|
||||
// better-auth returns Response object, we need to parse it
|
||||
const response = await coreApi<Response>('/auth/sign-in/email', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -9,7 +9,8 @@ export type SessionResponse = {
|
||||
|
||||
export const AUTH_SESSION_QUERY_KEY = ['auth', 'session'] as const
|
||||
|
||||
export const fetchSession = async () =>
|
||||
await coreApi<SessionResponse>('/auth/session', {
|
||||
export async function fetchSession() {
|
||||
return await coreApi<SessionResponse>('/auth/session', {
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface LoginRequest {
|
||||
rememberMe?: boolean
|
||||
}
|
||||
|
||||
export const useLogin = () => {
|
||||
export function useLogin() {
|
||||
const navigate = useNavigate()
|
||||
const queryClient = useQueryClient()
|
||||
const setAuthUser = useSetAuthUser()
|
||||
|
||||
@@ -5,9 +5,10 @@ import type { DashboardOverviewResponse } from './types'
|
||||
|
||||
const DASHBOARD_OVERVIEW_ENDPOINT = '/dashboard/overview'
|
||||
|
||||
export const fetchDashboardOverview = async () =>
|
||||
camelCaseKeys<DashboardOverviewResponse>(
|
||||
export async function fetchDashboardOverview() {
|
||||
return camelCaseKeys<DashboardOverviewResponse>(
|
||||
await coreApi<DashboardOverviewResponse>(DASHBOARD_OVERVIEW_ENDPOINT, {
|
||||
method: 'GET',
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -125,30 +125,34 @@ const EMPTY_STATS = {
|
||||
},
|
||||
} as const
|
||||
|
||||
const ActivitySkeleton = () => (
|
||||
<div className="bg-fill/10 animate-pulse rounded-lg border border-fill-tertiary px-3.5 py-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="bg-fill/20 h-11 w-11 shrink-0 rounded-lg" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="bg-fill/20 h-3.5 w-32 rounded-full" />
|
||||
<div className="bg-fill/15 h-3 w-48 rounded-full" />
|
||||
<div className="bg-fill/15 h-3 w-40 rounded-full" />
|
||||
function ActivitySkeleton() {
|
||||
return (
|
||||
<div className="bg-fill/10 border-fill-tertiary animate-pulse rounded-lg border px-3.5 py-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="bg-fill/20 h-11 w-11 shrink-0 rounded-lg" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="bg-fill/20 h-3.5 w-32 rounded-full" />
|
||||
<div className="bg-fill/15 h-3 w-48 rounded-full" />
|
||||
<div className="bg-fill/15 h-3 w-40 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const StatSkeleton = () => (
|
||||
<LinearBorderPanel className="bg-background-tertiary/60 relative overflow-hidden p-5">
|
||||
<div className="space-y-2.5">
|
||||
<div className="bg-fill/20 h-3 w-20 rounded-full" />
|
||||
<div className="bg-fill/30 h-7 w-24 rounded-md" />
|
||||
<div className="bg-fill/20 h-3 w-32 rounded-full" />
|
||||
</div>
|
||||
</LinearBorderPanel>
|
||||
)
|
||||
function StatSkeleton() {
|
||||
return (
|
||||
<LinearBorderPanel className="bg-background-tertiary/60 relative overflow-hidden p-5">
|
||||
<div className="space-y-2.5">
|
||||
<div className="bg-fill/20 h-3 w-20 rounded-full" />
|
||||
<div className="bg-fill/30 h-7 w-24 rounded-md" />
|
||||
<div className="bg-fill/20 h-3 w-32 rounded-full" />
|
||||
</div>
|
||||
</LinearBorderPanel>
|
||||
)
|
||||
}
|
||||
|
||||
const ActivityList = ({ items }: { items: DashboardRecentActivityItem[] }) => {
|
||||
function ActivityList({ items }: { items: DashboardRecentActivityItem[] }) {
|
||||
if (items.length === 0) {
|
||||
return <div className="text-text-tertiary mt-5 text-sm">暂无最近活动,上传照片后即可看到这里的动态。</div>
|
||||
}
|
||||
@@ -165,7 +169,7 @@ const ActivityList = ({ items }: { items: DashboardRecentActivityItem[] }) => {
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ ...Spring.presets.snappy, delay: index * 0.04 }}
|
||||
className="bg-fill/5 hover:bg-fill/10 group rounded-lg border border-fill-tertiary px-3.5 py-3 transition-colors duration-200"
|
||||
className="bg-fill/5 hover:bg-fill/10 group border-fill-tertiary rounded-lg border px-3.5 py-3 transition-colors duration-200"
|
||||
>
|
||||
<div className="flex flex-col gap-2.5 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
@@ -221,7 +225,7 @@ const ActivityList = ({ items }: { items: DashboardRecentActivityItem[] }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const DashboardOverview = () => {
|
||||
export function DashboardOverview() {
|
||||
const { data, isLoading, isError } = useDashboardOverviewQuery()
|
||||
|
||||
const stats = data?.stats ?? EMPTY_STATS
|
||||
@@ -317,7 +321,7 @@ export const DashboardOverview = () => {
|
||||
))}
|
||||
</div>
|
||||
) : isError ? (
|
||||
<div className="text-red-400 mt-5 text-sm">无法获取活动数据,请稍后再试。</div>
|
||||
<div className="mt-5 text-sm text-red-400">无法获取活动数据,请稍后再试。</div>
|
||||
) : (
|
||||
<ActivityList items={data?.recentActivity ?? []} />
|
||||
)}
|
||||
@@ -357,7 +361,7 @@ export const DashboardOverview = () => {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="text-text-secondary mt-5 rounded-lg border border-fill-tertiary bg-fill/5 px-3.5 py-2.5 text-xs leading-relaxed">
|
||||
<div className="text-text-secondary border-fill-tertiary bg-fill/5 mt-5 rounded-lg border px-3.5 py-2.5 text-xs leading-relaxed">
|
||||
{statusTotal === 0
|
||||
? '暂无同步任务,添加照片后即可查看同步健康度。'
|
||||
: syncCompletion !== null && syncCompletion >= 0.85
|
||||
|
||||
@@ -5,9 +5,10 @@ import type { DashboardOverviewResponse } from './types'
|
||||
|
||||
export const DASHBOARD_OVERVIEW_QUERY_KEY = ['dashboard', 'overview'] as const
|
||||
|
||||
export const useDashboardOverviewQuery = () =>
|
||||
useQuery<DashboardOverviewResponse>({
|
||||
export function useDashboardOverviewQuery() {
|
||||
return useQuery<DashboardOverviewResponse>({
|
||||
queryKey: DASHBOARD_OVERVIEW_QUERY_KEY,
|
||||
queryFn: fetchDashboardOverview,
|
||||
staleTime: 60 * 1000,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,13 +30,15 @@ export type OnboardingInitResponse = {
|
||||
superAdminUserId: string
|
||||
}
|
||||
|
||||
export const getOnboardingStatus = async () =>
|
||||
await coreApi<OnboardingStatusResponse>('/onboarding/status', {
|
||||
export async function getOnboardingStatus() {
|
||||
return await coreApi<OnboardingStatusResponse>('/onboarding/status', {
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
export const postOnboardingInit = async (payload: OnboardingInitPayload) =>
|
||||
await coreApi<OnboardingInitResponse>('/onboarding/init', {
|
||||
export async function postOnboardingInit(payload: OnboardingInitPayload) {
|
||||
return await coreApi<OnboardingInitResponse>('/onboarding/init', {
|
||||
method: 'POST',
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { createInitialSettingsState, getFieldByKey, isLikelyEmail, maskSecret, s
|
||||
|
||||
const INITIAL_STEP_INDEX = 0
|
||||
|
||||
export const useOnboardingWizard = () => {
|
||||
export function useOnboardingWizard() {
|
||||
const [currentStepIndex, setCurrentStepIndex] = useState(INITIAL_STEP_INDEX)
|
||||
const [tenant, setTenant] = useState<TenantFormState>({
|
||||
name: '',
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { OnboardingSettingKey, SettingFieldDefinition } from './constants'
|
||||
import { ONBOARDING_SETTING_SECTIONS, ONBOARDING_STEPS } from './constants'
|
||||
import type { SettingFormState } from './types'
|
||||
|
||||
export const createInitialSettingsState = (): SettingFormState => {
|
||||
export function createInitialSettingsState(): SettingFormState {
|
||||
const state = {} as SettingFormState
|
||||
for (const section of ONBOARDING_SETTING_SECTIONS) {
|
||||
for (const field of section.fields) {
|
||||
@@ -14,15 +14,16 @@ export const createInitialSettingsState = (): SettingFormState => {
|
||||
|
||||
export const maskSecret = (value: string) => (value ? '•'.repeat(Math.min(10, value.length)) : '')
|
||||
|
||||
export const slugify = (value: string) =>
|
||||
value
|
||||
export function slugify(value: string) {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replaceAll(/[^a-z0-9-]+/g, '-')
|
||||
.replaceAll(/-{2,}/g, '-')
|
||||
.replaceAll(/^-+|-+$/g, '')
|
||||
}
|
||||
|
||||
export const isLikelyEmail = (value: string) => {
|
||||
export function isLikelyEmail(value: string) {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed.includes('@')) {
|
||||
return false
|
||||
@@ -36,7 +37,7 @@ export const isLikelyEmail = (value: string) => {
|
||||
|
||||
export const stepProgress = (index: number) => Math.round((index / (ONBOARDING_STEPS.length - 1 || 1)) * 100)
|
||||
|
||||
export const getFieldByKey = (key: OnboardingSettingKey): SettingFieldDefinition => {
|
||||
export function getFieldByKey(key: OnboardingSettingKey): SettingFieldDefinition {
|
||||
for (const section of ONBOARDING_SETTING_SECTIONS) {
|
||||
for (const field of section.fields) {
|
||||
if (field.key === key) {
|
||||
|
||||
@@ -19,10 +19,10 @@ type RunPhotoSyncOptions = {
|
||||
onEvent?: (event: PhotoSyncProgressEvent) => void
|
||||
}
|
||||
|
||||
export const runPhotoSync = async (
|
||||
export async function runPhotoSync(
|
||||
payload: RunPhotoSyncPayload,
|
||||
options?: RunPhotoSyncOptions,
|
||||
): Promise<PhotoSyncResult> => {
|
||||
): Promise<PhotoSyncResult> {
|
||||
const response = await fetch(`${coreApiBaseURL}/data-sync/run`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -134,15 +134,15 @@ export const runPhotoSync = async (
|
||||
return camelCaseKeys<PhotoSyncResult>(finalResult)
|
||||
}
|
||||
|
||||
export const listPhotoSyncConflicts = async (): Promise<PhotoSyncConflict[]> => {
|
||||
export async function listPhotoSyncConflicts(): Promise<PhotoSyncConflict[]> {
|
||||
const conflicts = await coreApi<PhotoSyncConflict[]>('/data-sync/conflicts')
|
||||
return camelCaseKeys<PhotoSyncConflict[]>(conflicts)
|
||||
}
|
||||
|
||||
export const resolvePhotoSyncConflict = async (
|
||||
export async function resolvePhotoSyncConflict(
|
||||
id: string,
|
||||
payload: { strategy: PhotoSyncResolution; dryRun?: boolean },
|
||||
): Promise<PhotoSyncAction> => {
|
||||
): Promise<PhotoSyncAction> {
|
||||
const result = await coreApi<PhotoSyncAction>(`/data-sync/conflicts/${id}/resolve`, {
|
||||
method: 'POST',
|
||||
body: payload,
|
||||
@@ -151,29 +151,29 @@ export const resolvePhotoSyncConflict = async (
|
||||
return camelCaseKeys<PhotoSyncAction>(result)
|
||||
}
|
||||
|
||||
export const listPhotoAssets = async (): Promise<PhotoAssetListItem[]> => {
|
||||
export async function listPhotoAssets(): Promise<PhotoAssetListItem[]> {
|
||||
const assets = await coreApi<PhotoAssetListItem[]>('/photos/assets')
|
||||
|
||||
return camelCaseKeys<PhotoAssetListItem[]>(assets)
|
||||
}
|
||||
|
||||
export const getPhotoAssetSummary = async (): Promise<PhotoAssetSummary> => {
|
||||
export async function getPhotoAssetSummary(): Promise<PhotoAssetSummary> {
|
||||
const summary = await coreApi<PhotoAssetSummary>('/photos/assets/summary')
|
||||
|
||||
return camelCaseKeys<PhotoAssetSummary>(summary)
|
||||
}
|
||||
|
||||
export const deletePhotoAssets = async (ids: string[]): Promise<void> => {
|
||||
export async function deletePhotoAssets(ids: string[]): Promise<void> {
|
||||
await coreApi('/photos/assets', {
|
||||
method: 'DELETE',
|
||||
body: { ids },
|
||||
})
|
||||
}
|
||||
|
||||
export const uploadPhotoAssets = async (
|
||||
export async function uploadPhotoAssets(
|
||||
files: File[],
|
||||
options?: { directory?: string },
|
||||
): Promise<PhotoAssetListItem[]> => {
|
||||
): Promise<PhotoAssetListItem[]> {
|
||||
const formData = new FormData()
|
||||
|
||||
if (options?.directory) {
|
||||
@@ -194,7 +194,7 @@ export const uploadPhotoAssets = async (
|
||||
return data.assets
|
||||
}
|
||||
|
||||
export const getPhotoStorageUrl = async (storageKey: string): Promise<string> => {
|
||||
export async function getPhotoStorageUrl(storageKey: string): Promise<string> {
|
||||
const result = await coreApi<{ url: string }>('/photos/storage-url', {
|
||||
method: 'GET',
|
||||
query: { key: storageKey },
|
||||
|
||||
@@ -40,8 +40,8 @@ const STAGE_ORDER: PhotoSyncProgressStage[] = [
|
||||
'status-reconciliation',
|
||||
]
|
||||
|
||||
const createInitialStages = (totals: PhotoSyncProgressState['totals']): PhotoSyncProgressState['stages'] =>
|
||||
STAGE_ORDER.reduce<PhotoSyncProgressState['stages']>(
|
||||
function createInitialStages(totals: PhotoSyncProgressState['totals']): PhotoSyncProgressState['stages'] {
|
||||
return STAGE_ORDER.reduce<PhotoSyncProgressState['stages']>(
|
||||
(acc, stage) => {
|
||||
const total = totals[stage]
|
||||
acc[stage] = {
|
||||
@@ -53,8 +53,9 @@ const createInitialStages = (totals: PhotoSyncProgressState['totals']): PhotoSyn
|
||||
},
|
||||
{} as PhotoSyncProgressState['stages'],
|
||||
)
|
||||
}
|
||||
|
||||
export const PhotoPage = () => {
|
||||
export function PhotoPage() {
|
||||
const [activeTab, setActiveTab] = useState<PhotoPageTab>('sync')
|
||||
const [result, setResult] = useState<PhotoSyncResult | null>(null)
|
||||
const [lastWasDryRun, setLastWasDryRun] = useState<boolean | null>(null)
|
||||
|
||||
@@ -26,7 +26,7 @@ export interface MasonryRef {
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
export const Masonry = <Item,>(props: MasonryProps<Item> & { ref?: React.Ref<MasonryRef> }) => {
|
||||
export function Masonry<Item>(props: MasonryProps<Item> & { ref?: React.Ref<MasonryRef> }) {
|
||||
const [scrollTop, setScrollTop] = React.useState(0)
|
||||
const [isScrolling, setIsScrolling] = React.useState(false)
|
||||
const scrollElement = useScrollViewElement()
|
||||
|
||||
@@ -13,14 +13,14 @@ type PhotoLibraryActionBarProps = {
|
||||
onClearSelection: () => void
|
||||
}
|
||||
|
||||
export const PhotoLibraryActionBar = ({
|
||||
export function PhotoLibraryActionBar({
|
||||
selectionCount,
|
||||
isUploading,
|
||||
isDeleting,
|
||||
onUpload,
|
||||
onDeleteSelected,
|
||||
onClearSelection,
|
||||
}: PhotoLibraryActionBarProps) => {
|
||||
}: PhotoLibraryActionBarProps) {
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const handleUploadClick = () => {
|
||||
|
||||
@@ -15,7 +15,7 @@ type PhotoLibraryGridProps = {
|
||||
isDeleting?: boolean
|
||||
}
|
||||
|
||||
const PhotoGridItem = ({
|
||||
function PhotoGridItem({
|
||||
asset,
|
||||
isSelected,
|
||||
onToggleSelect,
|
||||
@@ -29,7 +29,7 @@ const PhotoGridItem = ({
|
||||
onOpenAsset: (asset: PhotoAssetListItem) => void
|
||||
onDeleteAsset: (asset: PhotoAssetListItem) => Promise<void> | void
|
||||
isDeleting?: boolean
|
||||
}) => {
|
||||
}) {
|
||||
const manifest = asset.manifest?.data
|
||||
const previewUrl = manifest?.thumbnailUrl ?? manifest?.originalUrl ?? asset.publicUrl
|
||||
const deviceLabel = manifest?.exif?.Model || manifest?.exif?.Make || '未知设备'
|
||||
@@ -142,7 +142,7 @@ const PhotoGridItem = ({
|
||||
)
|
||||
}
|
||||
|
||||
export const PhotoLibraryGrid = ({
|
||||
export function PhotoLibraryGrid({
|
||||
assets,
|
||||
isLoading,
|
||||
selectedIds,
|
||||
@@ -150,7 +150,7 @@ export const PhotoLibraryGrid = ({
|
||||
onOpenAsset,
|
||||
onDeleteAsset,
|
||||
isDeleting,
|
||||
}: PhotoLibraryGridProps) => {
|
||||
}: PhotoLibraryGridProps) {
|
||||
if (isLoading) {
|
||||
const skeletonKeys = [
|
||||
'photo-skeleton-1',
|
||||
|
||||
@@ -14,7 +14,7 @@ type PhotoSyncActionsProps = {
|
||||
onError?: (error: Error) => void
|
||||
}
|
||||
|
||||
export const PhotoSyncActions = ({ onCompleted, onProgress, onError }: PhotoSyncActionsProps) => {
|
||||
export function PhotoSyncActions({ onCompleted, onProgress, onError }: PhotoSyncActionsProps) {
|
||||
const { setHeaderActionState } = useMainPageLayout()
|
||||
const [pendingMode, setPendingMode] = useState<'dry-run' | 'apply' | null>(null)
|
||||
const abortRef = useRef<AbortController | null>(null)
|
||||
|
||||
@@ -18,7 +18,7 @@ type PhotoSyncConflictsPanelProps = {
|
||||
onRequestStorageUrl?: (storageKey: string) => Promise<string>
|
||||
}
|
||||
|
||||
const formatDate = (value: string) => {
|
||||
function formatDate(value: string) {
|
||||
try {
|
||||
return new Date(value).toLocaleString()
|
||||
} catch {
|
||||
@@ -26,7 +26,7 @@ const formatDate = (value: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const PhotoSyncConflictsPanel = ({
|
||||
export function PhotoSyncConflictsPanel({
|
||||
conflicts,
|
||||
isLoading,
|
||||
resolvingId,
|
||||
@@ -34,7 +34,7 @@ export const PhotoSyncConflictsPanel = ({
|
||||
onResolve,
|
||||
onResolveBatch,
|
||||
onRequestStorageUrl,
|
||||
}: PhotoSyncConflictsPanelProps) => {
|
||||
}: PhotoSyncConflictsPanelProps) {
|
||||
const sortedConflicts = useMemo(() => {
|
||||
if (!conflicts) return []
|
||||
return conflicts.toSorted((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
|
||||
@@ -453,7 +453,7 @@ export const PhotoSyncConflictsPanel = ({
|
||||
)
|
||||
}
|
||||
|
||||
const ConflictManifestPreview = ({
|
||||
function ConflictManifestPreview({
|
||||
manifest,
|
||||
disabled,
|
||||
onOpenOriginal,
|
||||
@@ -461,7 +461,7 @@ const ConflictManifestPreview = ({
|
||||
manifest: PhotoSyncConflict['manifest']['data'] | null | undefined
|
||||
disabled?: boolean
|
||||
onOpenOriginal?: () => void
|
||||
}) => {
|
||||
}) {
|
||||
if (!manifest) {
|
||||
return (
|
||||
<div className="border-border/20 bg-background-secondary/60 text-text-tertiary rounded-md border p-3 text-xs">
|
||||
@@ -511,7 +511,7 @@ const ConflictManifestPreview = ({
|
||||
)
|
||||
}
|
||||
|
||||
const ConflictStoragePreview = ({
|
||||
function ConflictStoragePreview({
|
||||
storageKey,
|
||||
snapshot,
|
||||
disabled,
|
||||
@@ -521,20 +521,22 @@ const ConflictStoragePreview = ({
|
||||
snapshot: PhotoSyncSnapshot | null | undefined
|
||||
disabled?: boolean
|
||||
onOpenStorage?: () => void
|
||||
}) => (
|
||||
<div className="border-border/20 bg-background-secondary/60 text-text-tertiary rounded-md border p-3 text-xs">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-text text-sm font-semibold">存储对象</p>
|
||||
{onOpenStorage ? (
|
||||
<Button type="button" variant="ghost" size="xs" disabled={disabled} onClick={onOpenStorage}>
|
||||
打开
|
||||
</Button>
|
||||
) : null}
|
||||
}) {
|
||||
return (
|
||||
<div className="border-border/20 bg-background-secondary/60 text-text-tertiary rounded-md border p-3 text-xs">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-text text-sm font-semibold">存储对象</p>
|
||||
{onOpenStorage ? (
|
||||
<Button type="button" variant="ghost" size="xs" disabled={disabled} onClick={onOpenStorage}>
|
||||
打开
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="mt-1 break-all">
|
||||
Key:
|
||||
<span className="text-text font-mono text-[11px]">{storageKey}</span>
|
||||
</p>
|
||||
<MetadataSnapshot snapshot={snapshot ?? null} />
|
||||
</div>
|
||||
<p className="mt-1 break-all">
|
||||
Key:
|
||||
<span className="text-text font-mono text-[11px]">{storageKey}</span>
|
||||
</p>
|
||||
<MetadataSnapshot snapshot={snapshot ?? null} />
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ type PhotoSyncProgressPanelProps = {
|
||||
progress: PhotoSyncProgressState
|
||||
}
|
||||
|
||||
const formatActionLabel = (action: PhotoSyncAction) => {
|
||||
function formatActionLabel(action: PhotoSyncAction) {
|
||||
const config = PHOTO_ACTION_TYPE_CONFIG[action.type]
|
||||
if (action.type === 'conflict' && action.conflictPayload) {
|
||||
const conflictLabel = getConflictTypeLabel(action.conflictPayload.type)
|
||||
@@ -60,7 +60,7 @@ const formatActionLabel = (action: PhotoSyncAction) => {
|
||||
return config?.label ?? action.type
|
||||
}
|
||||
|
||||
export const PhotoSyncProgressPanel = ({ progress }: PhotoSyncProgressPanelProps) => {
|
||||
export function PhotoSyncProgressPanel({ progress }: PhotoSyncProgressPanelProps) {
|
||||
const isErrored = Boolean(progress.error)
|
||||
const heading = isErrored ? '同步失败' : progress.dryRun ? '同步预览进行中' : '同步进行中'
|
||||
const subtitle = isErrored
|
||||
|
||||
@@ -6,14 +6,16 @@ import { useMemo, useState } from 'react'
|
||||
import { getConflictTypeLabel, PHOTO_ACTION_TYPE_CONFIG } from '../../constants'
|
||||
import type { PhotoAssetSummary, PhotoSyncAction, PhotoSyncResult, PhotoSyncSnapshot } from '../../types'
|
||||
|
||||
export const BorderOverlay = () => (
|
||||
<>
|
||||
<div className="via-text/20 absolute top-0 right-0 left-0 h-[0.5px] bg-linear-to-r from-transparent to-transparent" />
|
||||
<div className="via-text/20 absolute top-0 right-0 bottom-0 w-[0.5px] bg-linear-to-b from-transparent to-transparent" />
|
||||
<div className="via-text/20 absolute right-0 bottom-0 left-0 h-[0.5px] bg-linear-to-r from-transparent to-transparent" />
|
||||
<div className="via-text/20 absolute top-0 bottom-0 left-0 w-[0.5px] bg-linear-to-b from-transparent to-transparent" />
|
||||
</>
|
||||
)
|
||||
export function BorderOverlay() {
|
||||
return (
|
||||
<>
|
||||
<div className="via-text/20 absolute top-0 right-0 left-0 h-[0.5px] bg-linear-to-r from-transparent to-transparent" />
|
||||
<div className="via-text/20 absolute top-0 right-0 bottom-0 w-[0.5px] bg-linear-to-b from-transparent to-transparent" />
|
||||
<div className="via-text/20 absolute right-0 bottom-0 left-0 h-[0.5px] bg-linear-to-r from-transparent to-transparent" />
|
||||
<div className="via-text/20 absolute top-0 bottom-0 left-0 w-[0.5px] bg-linear-to-b from-transparent to-transparent" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type SummaryCardProps = {
|
||||
label: string
|
||||
@@ -21,7 +23,7 @@ type SummaryCardProps = {
|
||||
tone?: 'accent' | 'warning' | 'muted'
|
||||
}
|
||||
|
||||
const SummaryCard = ({ label, value, tone }: SummaryCardProps) => {
|
||||
function SummaryCard({ label, value, tone }: SummaryCardProps) {
|
||||
const toneClass =
|
||||
tone === 'accent'
|
||||
? 'text-accent'
|
||||
@@ -52,13 +54,13 @@ const actionTypeConfig = PHOTO_ACTION_TYPE_CONFIG
|
||||
|
||||
const SUMMARY_SKELETON_KEYS = ['summary-skeleton-1', 'summary-skeleton-2', 'summary-skeleton-3', 'summary-skeleton-4']
|
||||
|
||||
export const PhotoSyncResultPanel = ({
|
||||
export function PhotoSyncResultPanel({
|
||||
result,
|
||||
lastWasDryRun,
|
||||
baselineSummary,
|
||||
isSummaryLoading,
|
||||
onRequestStorageUrl,
|
||||
}: PhotoSyncResultPanelProps) => {
|
||||
}: PhotoSyncResultPanelProps) {
|
||||
const summaryItems = useMemo(() => {
|
||||
if (result) {
|
||||
return [
|
||||
@@ -439,7 +441,7 @@ export const PhotoSyncResultPanel = ({
|
||||
)
|
||||
}
|
||||
|
||||
const ManifestPreview = ({
|
||||
function ManifestPreview({
|
||||
title,
|
||||
manifest,
|
||||
onOpenOriginal,
|
||||
@@ -447,7 +449,7 @@ const ManifestPreview = ({
|
||||
title: string
|
||||
manifest: PhotoSyncAction['manifestAfter'] | PhotoSyncAction['manifestBefore']
|
||||
onOpenOriginal?: () => void
|
||||
}) => {
|
||||
}) {
|
||||
if (!manifest) {
|
||||
return (
|
||||
<div className="border-border/20 bg-background-secondary/60 text-text-tertiary rounded-md border p-3 text-xs">
|
||||
@@ -501,7 +503,7 @@ type MetadataSnapshotProps = {
|
||||
snapshot: PhotoSyncSnapshot | null | undefined
|
||||
}
|
||||
|
||||
export const MetadataSnapshot = ({ snapshot }: MetadataSnapshotProps) => {
|
||||
export function MetadataSnapshot({ snapshot }: MetadataSnapshotProps) {
|
||||
if (!snapshot) return null
|
||||
return (
|
||||
<dl className="mt-2 space-y-1">
|
||||
|
||||
@@ -15,7 +15,7 @@ export const PHOTO_CONFLICT_TYPE_CONFIG: Record<PhotoSyncConflictType, { label:
|
||||
},
|
||||
}
|
||||
|
||||
export const getConflictTypeLabel = (type: PhotoSyncConflictType | null | undefined): string => {
|
||||
export function getConflictTypeLabel(type: PhotoSyncConflictType | null | undefined): string {
|
||||
if (!type) {
|
||||
return '冲突'
|
||||
}
|
||||
|
||||
@@ -14,14 +14,14 @@ export const PHOTO_ASSET_SUMMARY_QUERY_KEY = ['photo-assets', 'summary'] as cons
|
||||
export const PHOTO_ASSET_LIST_QUERY_KEY = ['photo-assets', 'list'] as const
|
||||
export const PHOTO_SYNC_CONFLICTS_QUERY_KEY = ['photo-sync', 'conflicts'] as const
|
||||
|
||||
export const usePhotoAssetSummaryQuery = () => {
|
||||
export function usePhotoAssetSummaryQuery() {
|
||||
return useQuery({
|
||||
queryKey: PHOTO_ASSET_SUMMARY_QUERY_KEY,
|
||||
queryFn: getPhotoAssetSummary,
|
||||
})
|
||||
}
|
||||
|
||||
export const usePhotoAssetListQuery = (options?: { enabled?: boolean }) => {
|
||||
export function usePhotoAssetListQuery(options?: { enabled?: boolean }) {
|
||||
return useQuery({
|
||||
queryKey: PHOTO_ASSET_LIST_QUERY_KEY,
|
||||
queryFn: listPhotoAssets,
|
||||
@@ -29,7 +29,7 @@ export const usePhotoAssetListQuery = (options?: { enabled?: boolean }) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const usePhotoSyncConflictsQuery = (options?: { enabled?: boolean }) => {
|
||||
export function usePhotoSyncConflictsQuery(options?: { enabled?: boolean }) {
|
||||
return useQuery({
|
||||
queryKey: PHOTO_SYNC_CONFLICTS_QUERY_KEY,
|
||||
queryFn: listPhotoSyncConflicts,
|
||||
@@ -37,7 +37,7 @@ export const usePhotoSyncConflictsQuery = (options?: { enabled?: boolean }) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeletePhotoAssetsMutation = () => {
|
||||
export function useDeletePhotoAssetsMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
@@ -61,7 +61,7 @@ export const useDeletePhotoAssetsMutation = () => {
|
||||
})
|
||||
}
|
||||
|
||||
export const useUploadPhotoAssetsMutation = () => {
|
||||
export function useUploadPhotoAssetsMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
@@ -79,7 +79,7 @@ export const useUploadPhotoAssetsMutation = () => {
|
||||
})
|
||||
}
|
||||
|
||||
export const useResolvePhotoSyncConflictMutation = () => {
|
||||
export function useResolvePhotoSyncConflictMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
|
||||
@@ -28,7 +28,7 @@ import type {
|
||||
UiSlotComponent,
|
||||
} from './types'
|
||||
|
||||
const FieldDescription = ({ description }: { description?: string | null }) => {
|
||||
function FieldDescription({ description }: { description?: string | null }) {
|
||||
if (!description) {
|
||||
return null
|
||||
}
|
||||
@@ -36,7 +36,7 @@ const FieldDescription = ({ description }: { description?: string | null }) => {
|
||||
return <p className="text-text-tertiary mt-1 text-xs">{description}</p>
|
||||
}
|
||||
|
||||
const SchemaIcon = ({ name, className }: { name?: string | null; className?: string }) => {
|
||||
function SchemaIcon({ name, className }: { name?: string | null; className?: string }) {
|
||||
if (!name) {
|
||||
return null
|
||||
}
|
||||
@@ -44,7 +44,7 @@ const SchemaIcon = ({ name, className }: { name?: string | null; className?: str
|
||||
return <DynamicIcon name={name as any} className={clsxm('h-4 w-4', className)} />
|
||||
}
|
||||
|
||||
const SecretFieldInput = <Key extends string>({
|
||||
function SecretFieldInput<Key extends string>({
|
||||
component,
|
||||
fieldKey,
|
||||
value,
|
||||
@@ -54,7 +54,7 @@ const SecretFieldInput = <Key extends string>({
|
||||
fieldKey: Key
|
||||
value: string
|
||||
onChange: (key: Key, value: SchemaFormValue) => void
|
||||
}) => {
|
||||
}) {
|
||||
const [revealed, setRevealed] = useState(false)
|
||||
|
||||
return (
|
||||
@@ -96,13 +96,7 @@ type FieldRendererProps<Key extends string> = {
|
||||
context: SchemaRendererContext<Key>
|
||||
}
|
||||
|
||||
const FieldRenderer = <Key extends string>({
|
||||
field,
|
||||
value,
|
||||
onChange,
|
||||
renderSlot,
|
||||
context,
|
||||
}: FieldRendererProps<Key>) => {
|
||||
function FieldRenderer<Key extends string>({ field, value, onChange, renderSlot, context }: FieldRendererProps<Key>) {
|
||||
const { component } = field
|
||||
|
||||
if (component.type === 'slot') {
|
||||
@@ -173,14 +167,14 @@ const FieldRenderer = <Key extends string>({
|
||||
)
|
||||
}
|
||||
|
||||
const renderGroup = <Key extends string>(
|
||||
function renderGroup<Key extends string>(
|
||||
node: UiGroupNode<Key>,
|
||||
context: SchemaRendererContext<Key>,
|
||||
formState: SchemaFormState<Key>,
|
||||
handleChange: (key: Key, value: SchemaFormValue) => void,
|
||||
shouldRenderNode?: SchemaFormRendererProps<Key>['shouldRenderNode'],
|
||||
renderSlot?: SlotRenderer<Key>,
|
||||
) => {
|
||||
) {
|
||||
const renderedChildren = node.children
|
||||
.map((child) => renderNode(child, context, formState, handleChange, shouldRenderNode, renderSlot))
|
||||
.filter(Boolean)
|
||||
@@ -208,13 +202,13 @@ const renderGroup = <Key extends string>(
|
||||
)
|
||||
}
|
||||
|
||||
const renderField = <Key extends string>(
|
||||
function renderField<Key extends string>(
|
||||
field: UiFieldNode<Key>,
|
||||
formState: SchemaFormState<Key>,
|
||||
handleChange: (key: Key, value: SchemaFormValue) => void,
|
||||
renderSlot: SlotRenderer<Key> | undefined,
|
||||
context: SchemaRendererContext<Key>,
|
||||
) => {
|
||||
) {
|
||||
if (field.hidden) {
|
||||
return null
|
||||
}
|
||||
@@ -266,14 +260,14 @@ const renderField = <Key extends string>(
|
||||
)
|
||||
}
|
||||
|
||||
const renderNode = <Key extends string>(
|
||||
function renderNode<Key extends string>(
|
||||
node: UiNode<Key>,
|
||||
context: SchemaRendererContext<Key>,
|
||||
formState: SchemaFormState<Key>,
|
||||
handleChange: (key: Key, value: SchemaFormValue) => void,
|
||||
shouldRenderNode?: SchemaFormRendererProps<Key>['shouldRenderNode'],
|
||||
renderSlot?: SlotRenderer<Key>,
|
||||
): ReactNode => {
|
||||
): ReactNode {
|
||||
if (shouldRenderNode && !shouldRenderNode(node, context)) {
|
||||
return null
|
||||
}
|
||||
@@ -318,13 +312,13 @@ export interface SchemaFormRendererProps<Key extends string> {
|
||||
renderSlot?: SlotRenderer<Key>
|
||||
}
|
||||
|
||||
export const SchemaFormRenderer = <Key extends string>({
|
||||
export function SchemaFormRenderer<Key extends string>({
|
||||
schema,
|
||||
values,
|
||||
onChange,
|
||||
shouldRenderNode,
|
||||
renderSlot,
|
||||
}: SchemaFormRendererProps<Key>) => {
|
||||
}: SchemaFormRendererProps<Key>) {
|
||||
const context: SchemaRendererContext<Key> = { values }
|
||||
|
||||
return (
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface UiTextareaComponent extends UiFieldComponentBase<'textarea'> {
|
||||
|
||||
export interface UiSelectComponent extends UiFieldComponentBase<'select'> {
|
||||
readonly placeholder?: string
|
||||
readonly options?: ReadonlyArray<string>
|
||||
readonly options?: readonly string[]
|
||||
readonly allowCustom?: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { UiFieldNode, UiNode } from './types'
|
||||
|
||||
export const collectFieldNodes = <Key extends string>(nodes: ReadonlyArray<UiNode<Key>>): UiFieldNode<Key>[] => {
|
||||
const fields: UiFieldNode<Key>[] = []
|
||||
export function collectFieldNodes<Key extends string>(nodes: ReadonlyArray<UiNode<Key>>): Array<UiFieldNode<Key>> {
|
||||
const fields: Array<UiFieldNode<Key>> = []
|
||||
|
||||
for (const node of nodes) {
|
||||
if (node.type === 'field') {
|
||||
|
||||
@@ -2,11 +2,11 @@ import { coreApi } from '~/lib/api-client'
|
||||
|
||||
import type { SettingEntryInput, SettingUiSchemaResponse } from './types'
|
||||
|
||||
export const getSettingUiSchema = async () => {
|
||||
export async function getSettingUiSchema() {
|
||||
return await coreApi<SettingUiSchemaResponse>('/settings/ui-schema')
|
||||
}
|
||||
|
||||
export const getSettings = async (keys: ReadonlyArray<string>) => {
|
||||
export async function getSettings(keys: readonly string[]) {
|
||||
return await coreApi<{
|
||||
keys: string[]
|
||||
values: Record<string, string | null>
|
||||
@@ -16,8 +16,8 @@ export const getSettings = async (keys: ReadonlyArray<string>) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const updateSettings = async (entries: ReadonlyArray<SettingEntryInput>) => {
|
||||
return await coreApi<{ updated: ReadonlyArray<SettingEntryInput> }>('/settings', {
|
||||
export async function updateSettings(entries: readonly SettingEntryInput[]) {
|
||||
return await coreApi<{ updated: readonly SettingEntryInput[] }>('/settings', {
|
||||
method: 'POST',
|
||||
body: { entries },
|
||||
})
|
||||
|
||||
@@ -20,10 +20,10 @@ const providerGroupVisibility: Record<string, string> = {
|
||||
'builder-storage-eagle': 'eagle',
|
||||
}
|
||||
|
||||
const buildInitialState = (
|
||||
function buildInitialState(
|
||||
schema: SettingUiSchemaResponse['schema'],
|
||||
values: SettingUiSchemaResponse['values'],
|
||||
): SettingValueState<string> => {
|
||||
): SettingValueState<string> {
|
||||
const state: SettingValueState<string> = {} as SettingValueState<string>
|
||||
const fields = collectFieldNodes(schema.sections)
|
||||
|
||||
@@ -35,7 +35,7 @@ const buildInitialState = (
|
||||
return state
|
||||
}
|
||||
|
||||
export const SettingsForm = () => {
|
||||
export function SettingsForm() {
|
||||
const { data, isLoading, isError, error } = useSettingUiSchemaQuery()
|
||||
const updateSettingsMutation = useUpdateSettingsMutation()
|
||||
const { setHeaderActionState } = useMainPageLayout()
|
||||
|
||||
@@ -19,7 +19,7 @@ type SettingsNavigationProps = {
|
||||
active: (typeof SETTINGS_TABS)[number]['id']
|
||||
}
|
||||
|
||||
export const SettingsNavigation = ({ active }: SettingsNavigationProps) => {
|
||||
export function SettingsNavigation({ active }: SettingsNavigationProps) {
|
||||
return (
|
||||
<PageTabs
|
||||
activeId={active}
|
||||
|
||||
@@ -5,18 +5,18 @@ import type { SettingEntryInput } from './types'
|
||||
|
||||
export const SETTING_UI_SCHEMA_QUERY_KEY = ['settings', 'ui-schema'] as const
|
||||
|
||||
export const useSettingUiSchemaQuery = () => {
|
||||
export function useSettingUiSchemaQuery() {
|
||||
return useQuery({
|
||||
queryKey: SETTING_UI_SCHEMA_QUERY_KEY,
|
||||
queryFn: getSettingUiSchema,
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateSettingsMutation = () => {
|
||||
export function useUpdateSettingsMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (entries: ReadonlyArray<SettingEntryInput>) => {
|
||||
mutationFn: async (entries: readonly SettingEntryInput[]) => {
|
||||
await updateSettings(entries)
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
||||
@@ -29,13 +29,13 @@ type ProviderEditModalProps = ModalComponentProps & {
|
||||
onSetActive: (id: string) => void
|
||||
}
|
||||
|
||||
export const ProviderEditModal = ({
|
||||
export function ProviderEditModal({
|
||||
provider,
|
||||
|
||||
onSave,
|
||||
|
||||
dismiss,
|
||||
}: ProviderEditModalProps) => {
|
||||
}: ProviderEditModalProps) {
|
||||
const [formData, setFormData] = useState<StorageProvider | null>(provider)
|
||||
const [isDirty, setIsDirty] = useState(false)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { createEmptyProvider, reorderProvidersByActive } from '../utils'
|
||||
import { ProviderCard } from './ProviderCard'
|
||||
import { ProviderEditModal } from './ProviderEditModal'
|
||||
|
||||
export const StorageProvidersManager = () => {
|
||||
export function StorageProvidersManager() {
|
||||
const { data, isLoading, isError, error } = useStorageProvidersQuery()
|
||||
const updateMutation = useUpdateStorageProvidersMutation()
|
||||
const { setHeaderActionState } = useMainPageLayout()
|
||||
|
||||
@@ -19,7 +19,7 @@ export const STORAGE_PROVIDER_TYPE_OPTIONS: ReadonlyArray<{
|
||||
|
||||
export const STORAGE_PROVIDER_FIELD_DEFINITIONS: Record<
|
||||
StorageProviderType,
|
||||
ReadonlyArray<StorageProviderFieldDefinition>
|
||||
readonly StorageProviderFieldDefinition[]
|
||||
> = {
|
||||
s3: [
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
|
||||
export const STORAGE_PROVIDERS_QUERY_KEY = ['settings', 'storage-providers'] as const
|
||||
|
||||
export const useStorageProvidersQuery = () => {
|
||||
export function useStorageProvidersQuery() {
|
||||
return useQuery({
|
||||
queryKey: STORAGE_PROVIDERS_QUERY_KEY,
|
||||
queryFn: async () => {
|
||||
@@ -33,7 +33,7 @@ export const useStorageProvidersQuery = () => {
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateStorageProvidersMutation = () => {
|
||||
export function useUpdateStorageProvidersMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
@@ -65,10 +65,10 @@ export const useUpdateStorageProvidersMutation = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const restoreProviderSecrets = (
|
||||
function restoreProviderSecrets(
|
||||
nextProviders: StorageProvider[],
|
||||
previousProviders: StorageProvider[],
|
||||
): StorageProvider[] => {
|
||||
): StorageProvider[] {
|
||||
const previousMap = new Map(previousProviders.map((provider) => [provider.id, provider]))
|
||||
|
||||
return nextProviders.map((provider) => {
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { STORAGE_PROVIDER_FIELD_DEFINITIONS, STORAGE_PROVIDER_TYPES } from './constants'
|
||||
import type { StorageProvider, StorageProviderType } from './types'
|
||||
|
||||
const generateId = () => {
|
||||
function generateId() {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID()
|
||||
}
|
||||
return Math.random().toString(36).slice(2, 10)
|
||||
}
|
||||
|
||||
export const isStorageProviderType = (value: unknown): value is StorageProviderType => {
|
||||
export function isStorageProviderType(value: unknown): value is StorageProviderType {
|
||||
return STORAGE_PROVIDER_TYPES.includes(value as StorageProviderType)
|
||||
}
|
||||
|
||||
const normaliseConfigForType = (type: StorageProviderType, config: Record<string, unknown>): Record<string, string> => {
|
||||
function normaliseConfigForType(type: StorageProviderType, config: Record<string, unknown>): Record<string, string> {
|
||||
return STORAGE_PROVIDER_FIELD_DEFINITIONS[type].reduce<Record<string, string>>((acc, field) => {
|
||||
const raw = config[field.key]
|
||||
acc[field.key] = typeof raw === 'string' ? raw : raw == null ? '' : String(raw)
|
||||
@@ -20,7 +20,7 @@ const normaliseConfigForType = (type: StorageProviderType, config: Record<string
|
||||
}, {})
|
||||
}
|
||||
|
||||
const coerceProvider = (input: unknown): StorageProvider | null => {
|
||||
function coerceProvider(input: unknown): StorageProvider | null {
|
||||
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
||||
return null
|
||||
}
|
||||
@@ -50,7 +50,7 @@ const coerceProvider = (input: unknown): StorageProvider | null => {
|
||||
return provider
|
||||
}
|
||||
|
||||
export const parseStorageProviders = (raw: string | null): StorageProvider[] => {
|
||||
export function parseStorageProviders(raw: string | null): StorageProvider[] {
|
||||
if (!raw) {
|
||||
return []
|
||||
}
|
||||
@@ -67,7 +67,7 @@ export const parseStorageProviders = (raw: string | null): StorageProvider[] =>
|
||||
}
|
||||
}
|
||||
|
||||
export const serializeStorageProviders = (providers: ReadonlyArray<StorageProvider>): string => {
|
||||
export function serializeStorageProviders(providers: readonly StorageProvider[]): string {
|
||||
return JSON.stringify(
|
||||
providers.map((provider) => ({
|
||||
...provider,
|
||||
@@ -76,21 +76,21 @@ export const serializeStorageProviders = (providers: ReadonlyArray<StorageProvid
|
||||
)
|
||||
}
|
||||
|
||||
export const normalizeStorageProviderConfig = (provider: StorageProvider): StorageProvider => {
|
||||
export function normalizeStorageProviderConfig(provider: StorageProvider): StorageProvider {
|
||||
return {
|
||||
...provider,
|
||||
config: normaliseConfigForType(provider.type, provider.config),
|
||||
}
|
||||
}
|
||||
|
||||
export const getDefaultConfigForType = (type: StorageProviderType): Record<string, string> => {
|
||||
export function getDefaultConfigForType(type: StorageProviderType): Record<string, string> {
|
||||
return STORAGE_PROVIDER_FIELD_DEFINITIONS[type].reduce<Record<string, string>>((acc, field) => {
|
||||
acc[field.key] = ''
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
export const createEmptyProvider = (type: StorageProviderType): StorageProvider => {
|
||||
export function createEmptyProvider(type: StorageProviderType): StorageProvider {
|
||||
const timestamp = new Date().toISOString()
|
||||
return {
|
||||
id: '',
|
||||
@@ -102,10 +102,7 @@ export const createEmptyProvider = (type: StorageProviderType): StorageProvider
|
||||
}
|
||||
}
|
||||
|
||||
export const ensureActiveProviderId = (
|
||||
providers: ReadonlyArray<StorageProvider>,
|
||||
activeId: string | null,
|
||||
): string | null => {
|
||||
export function ensureActiveProviderId(providers: readonly StorageProvider[], activeId: string | null): string | null {
|
||||
if (!activeId) {
|
||||
return null
|
||||
}
|
||||
@@ -113,10 +110,10 @@ export const ensureActiveProviderId = (
|
||||
return providers.some((provider) => provider.id === activeId) ? activeId : null
|
||||
}
|
||||
|
||||
export const reorderProvidersByActive = (
|
||||
providers: ReadonlyArray<StorageProvider>,
|
||||
export function reorderProvidersByActive(
|
||||
providers: readonly StorageProvider[],
|
||||
activeId: string | null,
|
||||
): StorageProvider[] => {
|
||||
): StorageProvider[] {
|
||||
if (!activeId) {
|
||||
return [...providers]
|
||||
}
|
||||
|
||||
@@ -4,12 +4,13 @@ import type { SuperAdminSettingsResponse, UpdateSuperAdminSettingsPayload } from
|
||||
|
||||
const SUPER_ADMIN_SETTINGS_ENDPOINT = '/super-admin/settings'
|
||||
|
||||
export const fetchSuperAdminSettings = async () =>
|
||||
await coreApi<SuperAdminSettingsResponse>(`${SUPER_ADMIN_SETTINGS_ENDPOINT}`, {
|
||||
export async function fetchSuperAdminSettings() {
|
||||
return await coreApi<SuperAdminSettingsResponse>(`${SUPER_ADMIN_SETTINGS_ENDPOINT}`, {
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
export const updateSuperAdminSettings = async (payload: UpdateSuperAdminSettingsPayload) => {
|
||||
export async function updateSuperAdminSettings(payload: UpdateSuperAdminSettingsPayload) {
|
||||
const sanitizedEntries = Object.entries(payload).filter(([, value]) => value !== undefined)
|
||||
const body = Object.fromEntries(sanitizedEntries)
|
||||
|
||||
|
||||
@@ -19,13 +19,15 @@ type FormState = Record<SuperAdminSettingField, SchemaFormValue>
|
||||
|
||||
const BOOLEAN_FIELDS = new Set<SuperAdminSettingField>(['allowRegistration', 'localProviderEnabled'])
|
||||
|
||||
const toFormState = (settings: SuperAdminSettings): FormState => ({
|
||||
allowRegistration: settings.allowRegistration,
|
||||
localProviderEnabled: settings.localProviderEnabled,
|
||||
maxRegistrableUsers: settings.maxRegistrableUsers === null ? '' : String(settings.maxRegistrableUsers),
|
||||
})
|
||||
function toFormState(settings: SuperAdminSettings): FormState {
|
||||
return {
|
||||
allowRegistration: settings.allowRegistration,
|
||||
localProviderEnabled: settings.localProviderEnabled,
|
||||
maxRegistrableUsers: settings.maxRegistrableUsers === null ? '' : String(settings.maxRegistrableUsers),
|
||||
}
|
||||
}
|
||||
|
||||
const areFormStatesEqual = (left: FormState | null, right: FormState | null): boolean => {
|
||||
function areFormStatesEqual(left: FormState | null, right: FormState | null): boolean {
|
||||
if (left === right) {
|
||||
return true
|
||||
}
|
||||
@@ -41,7 +43,7 @@ const areFormStatesEqual = (left: FormState | null, right: FormState | null): bo
|
||||
)
|
||||
}
|
||||
|
||||
const normalizeMaxUsers = (value: SchemaFormValue): string => {
|
||||
function normalizeMaxUsers(value: SchemaFormValue): string {
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
}
|
||||
@@ -61,7 +63,7 @@ type PossiblySnakeCaseSettings = Partial<
|
||||
}
|
||||
>
|
||||
|
||||
const coerceMaxUsers = (value: unknown): number | null => {
|
||||
function coerceMaxUsers(value: unknown): number | null {
|
||||
if (value === undefined || value === null) {
|
||||
return null
|
||||
}
|
||||
@@ -74,7 +76,7 @@ const coerceMaxUsers = (value: unknown): number | null => {
|
||||
return Number.isFinite(parsed) ? parsed : null
|
||||
}
|
||||
|
||||
const normalizeServerSettings = (input: PossiblySnakeCaseSettings | null): SuperAdminSettings | null => {
|
||||
function normalizeServerSettings(input: PossiblySnakeCaseSettings | null): SuperAdminSettings | null {
|
||||
if (!input) {
|
||||
return null
|
||||
}
|
||||
@@ -98,7 +100,7 @@ const normalizeServerSettings = (input: PossiblySnakeCaseSettings | null): Super
|
||||
return null
|
||||
}
|
||||
|
||||
const extractServerValues = (payload: SuperAdminSettingsResponse): SuperAdminSettings | null => {
|
||||
function extractServerValues(payload: SuperAdminSettingsResponse): SuperAdminSettings | null {
|
||||
if ('values' in payload) {
|
||||
return normalizeServerSettings(payload.values ?? null)
|
||||
}
|
||||
@@ -110,7 +112,7 @@ const extractServerValues = (payload: SuperAdminSettingsResponse): SuperAdminSet
|
||||
return null
|
||||
}
|
||||
|
||||
export const SuperAdminSettingsForm = () => {
|
||||
export function SuperAdminSettingsForm() {
|
||||
const { data, isLoading, isError, error } = useSuperAdminSettingsQuery()
|
||||
const [formState, setFormState] = useState<FormState | null>(null)
|
||||
const [initialState, setInitialState] = useState<FormState | null>(null)
|
||||
|
||||
@@ -5,18 +5,19 @@ import type { SuperAdminSettingsResponse, UpdateSuperAdminSettingsPayload } from
|
||||
|
||||
export const SUPER_ADMIN_SETTINGS_QUERY_KEY = ['super-admin', 'settings'] as const
|
||||
|
||||
export const useSuperAdminSettingsQuery = () =>
|
||||
useQuery<SuperAdminSettingsResponse>({
|
||||
export function useSuperAdminSettingsQuery() {
|
||||
return useQuery<SuperAdminSettingsResponse>({
|
||||
queryKey: SUPER_ADMIN_SETTINGS_QUERY_KEY,
|
||||
queryFn: fetchSuperAdminSettings,
|
||||
staleTime: 60 * 1000,
|
||||
})
|
||||
}
|
||||
|
||||
type SuperAdminSettingsMutationOptions = {
|
||||
onSuccess?: (data: SuperAdminSettingsResponse) => void
|
||||
}
|
||||
|
||||
export const useUpdateSuperAdminSettingsMutation = (options?: SuperAdminSettingsMutationOptions) => {
|
||||
export function useUpdateSuperAdminSettingsMutation(options?: SuperAdminSettingsMutationOptions) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MainPageLayout } from '~/components/layouts/MainPageLayout'
|
||||
|
||||
export const Component = () => {
|
||||
export function Component() {
|
||||
return (
|
||||
<MainPageLayout title="Analytics" description="Track your photo collection statistics and trends">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
|
||||
@@ -13,7 +13,7 @@ const navigationTabs = [
|
||||
{ label: 'Analytics', path: '/analytics' },
|
||||
] as const
|
||||
|
||||
export const Component = () => {
|
||||
export function Component() {
|
||||
const { logout } = usePageRedirect()
|
||||
const user = useAuthUserValue()
|
||||
const [isLoggingOut, setIsLoggingOut] = useState(false)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PhotoPage } from '~/modules/photos'
|
||||
|
||||
export const Component = () => {
|
||||
export function Component() {
|
||||
return <PhotoPage />
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MainPageLayout } from '~/components/layouts/MainPageLayout'
|
||||
import { SettingsForm, SettingsNavigation } from '~/modules/settings'
|
||||
|
||||
export const Component = () => {
|
||||
export function Component() {
|
||||
return (
|
||||
<MainPageLayout title="系统设置" description="管理后台与核心功能的通用配置,修改后会立即同步生效。">
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -2,7 +2,7 @@ import { MainPageLayout } from '~/components/layouts/MainPageLayout'
|
||||
import { SettingsNavigation } from '~/modules/settings'
|
||||
import { StorageProvidersManager } from '~/modules/storage-providers'
|
||||
|
||||
export const Component = () => {
|
||||
export function Component() {
|
||||
return (
|
||||
<MainPageLayout
|
||||
title="素材存储与 Builder"
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useState } from 'react'
|
||||
import { useLogin } from '~/modules/auth/hooks/useLogin'
|
||||
import { LinearBorderContainer } from '~/modules/onboarding/components/LinearBorderContainer'
|
||||
|
||||
export const Component = () => {
|
||||
export function Component() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const { login, isLoading, error, clearError } = useLogin()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Navigate } from 'react-router'
|
||||
|
||||
export const Component = () => {
|
||||
export function Component() {
|
||||
return <Navigate to="/superadmin/settings" replace />
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { usePageRedirect } from '~/hooks/usePageRedirect'
|
||||
|
||||
const navigationTabs = [{ label: '系统设置', path: '/superadmin/settings' }] as const
|
||||
|
||||
export const Component = () => {
|
||||
export function Component() {
|
||||
const { logout } = usePageRedirect()
|
||||
const user = useAuthUserValue()
|
||||
const isSuperAdmin = useIsSuperAdmin()
|
||||
|
||||
@@ -3,7 +3,7 @@ import { m } from 'motion/react'
|
||||
|
||||
import { SuperAdminSettingsForm } from '~/modules/super-admin'
|
||||
|
||||
export const Component = () => {
|
||||
export function Component() {
|
||||
return (
|
||||
<m.div
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
|
||||
@@ -24,7 +24,7 @@ export const ContextMenuProvider: Component = ({ children }) => (
|
||||
</>
|
||||
)
|
||||
|
||||
const Handler = () => {
|
||||
function Handler() {
|
||||
const ref = useRef<HTMLSpanElement>(null)
|
||||
const [contextMenuState, setContextMenuState] = useContextMenuState()
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ window.router = {
|
||||
* And use our router hooks will not re-render the component when the router has any changes.
|
||||
* Also it can access values outside of the component and provide a value selector
|
||||
*/
|
||||
export const StableRouterProvider = () => {
|
||||
export function StableRouterProvider() {
|
||||
const [searchParams] = useSearchParams()
|
||||
const params = useParams()
|
||||
const nav = useNavigate()
|
||||
|
||||
Reference in New Issue
Block a user