feat(web): migrate application shell to TanStack Start
- replace the legacy client-only Vite bootstrap with TanStack Router routes, Start SSR, and Nitro output - compose the reusable layout, navigation, appearance, chats, and notifications blocks with application-owned data - persist UI preferences through server functions and initialize theme, sidebar, and breakpoint state before hydration - add React Query, hotkey, toaster, and responsive dashboard integration - add application-owned Lingui configuration and catalogs, mount the runtime provider, and lazily enable I18nDevtool in development - remove superseded HTML, main entry, theme provider, and application ESLint configuration
This commit is contained in:
@@ -1,22 +0,0 @@
|
|||||||
import js from '@eslint/js'
|
|
||||||
import globals from 'globals'
|
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
||||||
import tseslint from 'typescript-eslint'
|
|
||||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
||||||
|
|
||||||
export default defineConfig([
|
|
||||||
globalIgnores(['dist']),
|
|
||||||
{
|
|
||||||
files: ['**/*.{ts,tsx}'],
|
|
||||||
extends: [
|
|
||||||
js.configs.recommended,
|
|
||||||
tseslint.configs.recommended,
|
|
||||||
reactHooks.configs.flat.recommended,
|
|
||||||
reactRefresh.configs.vite,
|
|
||||||
],
|
|
||||||
languageOptions: {
|
|
||||||
globals: globals.browser,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"sourceLocale": "en",
|
||||||
|
"locales": ["en", "zh-Hans"],
|
||||||
|
"catalogPath": "src/locales/{locale}/messages",
|
||||||
|
"include": ["src", "../../packages/blocks/src", "../../packages/ui/src"],
|
||||||
|
"exclude": ["**/*.test.{ts,tsx}", "**/node_modules/**"]
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>vite-monorepo</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import project from "./i18n.config.json" with { type: "json" }
|
||||||
|
import { createLinguiConfig } from "@workspace/i18n/lingui-config"
|
||||||
|
|
||||||
|
export default createLinguiConfig(project)
|
||||||
+29
-13
@@ -4,32 +4,48 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite dev",
|
||||||
"build": "tsc -b && vite build",
|
"build": "vite build && tsc -b",
|
||||||
"lint": "eslint",
|
|
||||||
"format": "prettier --write \"**/*.{ts,tsx}\"",
|
"format": "prettier --write \"**/*.{ts,tsx}\"",
|
||||||
"typecheck": "tsc --noEmit",
|
"i18n": "workspace-i18n",
|
||||||
"preview": "vite preview"
|
"i18n:compile": "workspace-i18n compile",
|
||||||
|
"i18n:extract": "workspace-i18n extract",
|
||||||
|
"i18n:ui": "workspace-i18n ui",
|
||||||
|
"typecheck": "tsc -b",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"start": "node .output/server/index.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@base-ui/react": "^1.6.0",
|
||||||
|
"@base-ui/utils": "^0.3.1",
|
||||||
|
"@floating-ui/utils": "^0.2.12",
|
||||||
|
"@hugeicons/core-free-icons": "^4.2.3",
|
||||||
|
"@hugeicons/react": "^1.1.9",
|
||||||
|
"@iconify/react": "^6.0.2",
|
||||||
|
"@lingui/core": "^6",
|
||||||
|
"@tanstack/react-form": "^1.33.2",
|
||||||
|
"@tanstack/react-hotkeys": "^0.10.0",
|
||||||
|
"@tanstack/react-query": "^5.101.4",
|
||||||
|
"@tanstack/react-router": "^1.170.18",
|
||||||
|
"@tanstack/react-start": "^1.168.32",
|
||||||
|
"@workspace/blocks": "workspace:*",
|
||||||
|
"@workspace/i18n": "workspace:*",
|
||||||
"@workspace/ui": "workspace:*",
|
"@workspace/ui": "workspace:*",
|
||||||
"lucide-react": "^1.27.0",
|
"lucide-react": "^1.27.0",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.6"
|
"react-dom": "^19.2.6",
|
||||||
|
"sonner": "^2.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10",
|
|
||||||
"@tailwindcss/vite": "^4",
|
"@tailwindcss/vite": "^4",
|
||||||
|
"@testing-library/react": "^16.3.2",
|
||||||
"@types/node": "^24",
|
"@types/node": "^24",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@vitejs/plugin-react": "^6",
|
"@vitejs/plugin-react": "^6",
|
||||||
"eslint": "^10",
|
"nitro": "^3.0.260610-beta",
|
||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
|
||||||
"globals": "^17",
|
|
||||||
"typescript": "~6",
|
"typescript": "~6",
|
||||||
"typescript-eslint": "^8",
|
"vite": "^8",
|
||||||
"vite": "^8"
|
"vitest": "^4.1.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-14
@@ -1,19 +1,24 @@
|
|||||||
import { Button } from "@workspace/ui/components/button"
|
import { AppLayout } from "@workspace/blocks/layout"
|
||||||
|
|
||||||
|
import { chatThreads } from "./data/chats"
|
||||||
|
import { primaryNavigationGroups } from "./data/navigation"
|
||||||
|
import {
|
||||||
|
fetchMockNotifications,
|
||||||
|
notificationsQueryKey,
|
||||||
|
} from "./data/notifications"
|
||||||
|
import { DashboardPage } from "./pages/dashboard-page"
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-svh p-6">
|
<AppLayout
|
||||||
<div className="flex max-w-md min-w-0 flex-col gap-4 text-sm leading-loose">
|
chatThreads={chatThreads}
|
||||||
<div>
|
navigationGroups={primaryNavigationGroups}
|
||||||
<h1 className="font-medium">Project ready!</h1>
|
notifications={{
|
||||||
<p>You may now add components and start building.</p>
|
queryFn: fetchMockNotifications,
|
||||||
<p>We've already added the button component for you.</p>
|
queryKey: notificationsQueryKey,
|
||||||
<Button className="mt-2">Button</Button>
|
}}
|
||||||
</div>
|
>
|
||||||
<div className="text-muted-foreground font-mono text-xs">
|
<DashboardPage />
|
||||||
(Press <kbd>d</kbd> to toggle dark mode)
|
</AppLayout>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,230 +0,0 @@
|
|||||||
/* eslint-disable react-refresh/only-export-components */
|
|
||||||
import * as React from "react"
|
|
||||||
|
|
||||||
type Theme = "dark" | "light" | "system"
|
|
||||||
type ResolvedTheme = "dark" | "light"
|
|
||||||
|
|
||||||
type ThemeProviderProps = {
|
|
||||||
children: React.ReactNode
|
|
||||||
defaultTheme?: Theme
|
|
||||||
storageKey?: string
|
|
||||||
disableTransitionOnChange?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
type ThemeProviderState = {
|
|
||||||
theme: Theme
|
|
||||||
setTheme: (theme: Theme) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const COLOR_SCHEME_QUERY = "(prefers-color-scheme: dark)"
|
|
||||||
const THEME_VALUES: Theme[] = ["dark", "light", "system"]
|
|
||||||
|
|
||||||
const ThemeProviderContext = React.createContext<
|
|
||||||
ThemeProviderState | undefined
|
|
||||||
>(undefined)
|
|
||||||
|
|
||||||
function isTheme(value: string | null): value is Theme {
|
|
||||||
if (value === null) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return THEME_VALUES.includes(value as Theme)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSystemTheme(): ResolvedTheme {
|
|
||||||
if (window.matchMedia(COLOR_SCHEME_QUERY).matches) {
|
|
||||||
return "dark"
|
|
||||||
}
|
|
||||||
|
|
||||||
return "light"
|
|
||||||
}
|
|
||||||
|
|
||||||
function disableTransitionsTemporarily() {
|
|
||||||
const style = document.createElement("style")
|
|
||||||
style.appendChild(
|
|
||||||
document.createTextNode(
|
|
||||||
"*,*::before,*::after{-webkit-transition:none!important;transition:none!important}"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
document.head.appendChild(style)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.getComputedStyle(document.body)
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
style.remove()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isEditableTarget(target: EventTarget | null) {
|
|
||||||
if (!(target instanceof HTMLElement)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target.isContentEditable) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const editableParent = target.closest(
|
|
||||||
"input, textarea, select, [contenteditable='true']"
|
|
||||||
)
|
|
||||||
if (editableParent) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ThemeProvider({
|
|
||||||
children,
|
|
||||||
defaultTheme = "system",
|
|
||||||
storageKey = "theme",
|
|
||||||
disableTransitionOnChange = true,
|
|
||||||
...props
|
|
||||||
}: ThemeProviderProps) {
|
|
||||||
const [theme, setThemeState] = React.useState<Theme>(() => {
|
|
||||||
const storedTheme = localStorage.getItem(storageKey)
|
|
||||||
if (isTheme(storedTheme)) {
|
|
||||||
return storedTheme
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultTheme
|
|
||||||
})
|
|
||||||
|
|
||||||
const setTheme = React.useCallback(
|
|
||||||
(nextTheme: Theme) => {
|
|
||||||
localStorage.setItem(storageKey, nextTheme)
|
|
||||||
setThemeState(nextTheme)
|
|
||||||
},
|
|
||||||
[storageKey]
|
|
||||||
)
|
|
||||||
|
|
||||||
const applyTheme = React.useCallback(
|
|
||||||
(nextTheme: Theme) => {
|
|
||||||
const root = document.documentElement
|
|
||||||
const resolvedTheme =
|
|
||||||
nextTheme === "system" ? getSystemTheme() : nextTheme
|
|
||||||
const restoreTransitions = disableTransitionOnChange
|
|
||||||
? disableTransitionsTemporarily()
|
|
||||||
: null
|
|
||||||
|
|
||||||
root.classList.remove("light", "dark")
|
|
||||||
root.classList.add(resolvedTheme)
|
|
||||||
|
|
||||||
if (restoreTransitions) {
|
|
||||||
restoreTransitions()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[disableTransitionOnChange]
|
|
||||||
)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
applyTheme(theme)
|
|
||||||
|
|
||||||
if (theme !== "system") {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const mediaQuery = window.matchMedia(COLOR_SCHEME_QUERY)
|
|
||||||
const handleChange = () => {
|
|
||||||
applyTheme("system")
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaQuery.addEventListener("change", handleChange)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
mediaQuery.removeEventListener("change", handleChange)
|
|
||||||
}
|
|
||||||
}, [theme, applyTheme])
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
|
||||||
if (event.repeat) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.metaKey || event.ctrlKey || event.altKey) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEditableTarget(event.target)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key.toLowerCase() !== "d") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setThemeState((currentTheme) => {
|
|
||||||
const nextTheme =
|
|
||||||
currentTheme === "dark"
|
|
||||||
? "light"
|
|
||||||
: currentTheme === "light"
|
|
||||||
? "dark"
|
|
||||||
: getSystemTheme() === "dark"
|
|
||||||
? "light"
|
|
||||||
: "dark"
|
|
||||||
|
|
||||||
localStorage.setItem(storageKey, nextTheme)
|
|
||||||
return nextTheme
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("keydown", handleKeyDown)
|
|
||||||
}
|
|
||||||
}, [storageKey])
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const handleStorageChange = (event: StorageEvent) => {
|
|
||||||
if (event.storageArea !== localStorage) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key !== storageKey) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isTheme(event.newValue)) {
|
|
||||||
setThemeState(event.newValue)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setThemeState(defaultTheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("storage", handleStorageChange)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("storage", handleStorageChange)
|
|
||||||
}
|
|
||||||
}, [defaultTheme, storageKey])
|
|
||||||
|
|
||||||
const value = React.useMemo(
|
|
||||||
() => ({
|
|
||||||
theme,
|
|
||||||
setTheme,
|
|
||||||
}),
|
|
||||||
[theme, setTheme]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ThemeProviderContext.Provider {...props} value={value}>
|
|
||||||
{children}
|
|
||||||
</ThemeProviderContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useTheme = () => {
|
|
||||||
const context = React.useContext(ThemeProviderContext)
|
|
||||||
|
|
||||||
if (context === undefined) {
|
|
||||||
throw new Error("useTheme must be used within a ThemeProvider")
|
|
||||||
}
|
|
||||||
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useUiState } from "@workspace/blocks/appearance"
|
||||||
|
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
||||||
|
import {
|
||||||
|
CircleCheckIcon,
|
||||||
|
InfoIcon,
|
||||||
|
TriangleAlertIcon,
|
||||||
|
OctagonXIcon,
|
||||||
|
Loader2Icon,
|
||||||
|
} from "lucide-react"
|
||||||
|
|
||||||
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
|
const [themeMode] = useUiState("theme-mode")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sonner
|
||||||
|
theme={themeMode}
|
||||||
|
className="toaster group"
|
||||||
|
icons={{
|
||||||
|
success: <CircleCheckIcon className="size-4" />,
|
||||||
|
info: <InfoIcon className="size-4" />,
|
||||||
|
warning: <TriangleAlertIcon className="size-4" />,
|
||||||
|
error: <OctagonXIcon className="size-4" />,
|
||||||
|
loading: <Loader2Icon className="size-4 animate-spin" />,
|
||||||
|
}}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--normal-bg": "var(--popover)",
|
||||||
|
"--normal-text": "var(--popover-foreground)",
|
||||||
|
"--normal-border": "var(--border)",
|
||||||
|
"--border-radius": "var(--radius)",
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
toastOptions={{
|
||||||
|
classNames: {
|
||||||
|
toast: "cn-toast",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Toaster }
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import type { ChatThread } from "@workspace/blocks/chats"
|
||||||
|
|
||||||
|
export const chatThreads: readonly ChatThread[] = [
|
||||||
|
{
|
||||||
|
avatarUrl:
|
||||||
|
"https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/images/mock/avatar/avatar-1.webp",
|
||||||
|
name: "Jayvion Simon",
|
||||||
|
presence: "offline",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatarUrl:
|
||||||
|
"https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/images/mock/avatar/avatar-2.webp",
|
||||||
|
name: "Lucian Obrien",
|
||||||
|
presence: "online",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatarUrl:
|
||||||
|
"https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/images/mock/avatar/avatar-3.webp",
|
||||||
|
name: "Deja Brady",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatarUrl:
|
||||||
|
"https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/images/mock/avatar/avatar-4.webp",
|
||||||
|
name: "Harrison Stein",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatarUrl:
|
||||||
|
"https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/images/mock/avatar/avatar-5.webp",
|
||||||
|
name: "Reece Chung",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatarUrl:
|
||||||
|
"https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/images/mock/avatar/avatar-6.webp",
|
||||||
|
name: "Cristopher Cardenas",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatarUrl:
|
||||||
|
"https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/images/mock/avatar/avatar-8.webp",
|
||||||
|
name: "Melanie Noble",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatarUrl:
|
||||||
|
"https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/images/mock/avatar/avatar-9.webp",
|
||||||
|
name: "Chase Day",
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -0,0 +1,594 @@
|
|||||||
|
import {
|
||||||
|
Alert02Icon,
|
||||||
|
Analytics01Icon,
|
||||||
|
AnalyticsUpIcon,
|
||||||
|
AppWindowIcon,
|
||||||
|
ArrowDataTransferHorizontalIcon,
|
||||||
|
BubbleChatIcon,
|
||||||
|
Calculator01Icon,
|
||||||
|
CallMissed01Icon,
|
||||||
|
CancelCircleIcon,
|
||||||
|
Chart01Icon,
|
||||||
|
ChartIncreaseIcon,
|
||||||
|
CheckmarkBadge01Icon,
|
||||||
|
CheckmarkCircle01Icon,
|
||||||
|
ClipboardListIcon,
|
||||||
|
Clock01Icon,
|
||||||
|
CodeSquareIcon,
|
||||||
|
Compass01Icon,
|
||||||
|
CreditCardIcon,
|
||||||
|
CrownIcon,
|
||||||
|
Delete02Icon,
|
||||||
|
DeliveryTruck01Icon,
|
||||||
|
Discount01Icon,
|
||||||
|
File01Icon,
|
||||||
|
GalleryHorizontalIcon,
|
||||||
|
GiftIcon,
|
||||||
|
HistoryIcon,
|
||||||
|
IdentityCardIcon,
|
||||||
|
InboxDownloadIcon,
|
||||||
|
InboxUploadIcon,
|
||||||
|
Invoice03Icon,
|
||||||
|
Key01Icon,
|
||||||
|
LinkCircleIcon,
|
||||||
|
Location01Icon,
|
||||||
|
LockKeyIcon,
|
||||||
|
Medal01Icon,
|
||||||
|
MoneySendSquareIcon,
|
||||||
|
Note01Icon,
|
||||||
|
Package02Icon,
|
||||||
|
PackageIcon,
|
||||||
|
Refresh01Icon,
|
||||||
|
RoadLocation01Icon,
|
||||||
|
SecurityWarningIcon,
|
||||||
|
Settings01Icon,
|
||||||
|
Settings03Icon,
|
||||||
|
Share01Icon,
|
||||||
|
ShoppingBag01Icon,
|
||||||
|
ShoppingCart01Icon,
|
||||||
|
StarCircleIcon,
|
||||||
|
StarIcon,
|
||||||
|
Store01Icon,
|
||||||
|
Structure01Icon,
|
||||||
|
Task01Icon,
|
||||||
|
Ticket01Icon,
|
||||||
|
UserGroupIcon,
|
||||||
|
UserShield01Icon,
|
||||||
|
Wallet01Icon,
|
||||||
|
WalletCardsIcon,
|
||||||
|
} from "@hugeicons/core-free-icons"
|
||||||
|
|
||||||
|
import type { NavigationGroup } from "@workspace/blocks/navigation"
|
||||||
|
|
||||||
|
export const primaryNavigationGroups: readonly NavigationGroup[] = [
|
||||||
|
{
|
||||||
|
id: "overview",
|
||||||
|
label: "概览",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "overview-dashboard",
|
||||||
|
to: "/",
|
||||||
|
label: "经营总览",
|
||||||
|
icon: Analytics01Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "overview-realtime",
|
||||||
|
to: "/analytics/realtime",
|
||||||
|
label: "实时监控",
|
||||||
|
icon: AnalyticsUpIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "overview-tasks",
|
||||||
|
to: "/workspace/tasks",
|
||||||
|
label: "待办中心",
|
||||||
|
icon: Task01Icon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "catalog-and-inventory",
|
||||||
|
label: "商品与库存",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "catalog-products",
|
||||||
|
label: "商品中心",
|
||||||
|
icon: ShoppingBag01Icon,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "catalog-products-list",
|
||||||
|
to: "/catalog/products/list",
|
||||||
|
label: "商品列表",
|
||||||
|
icon: ShoppingBag01Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "catalog-products-drafts",
|
||||||
|
to: "/catalog/products/drafts",
|
||||||
|
label: "商品草稿",
|
||||||
|
icon: File01Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "catalog-products-bundles",
|
||||||
|
to: "/catalog/products/bundles",
|
||||||
|
label: "组合商品",
|
||||||
|
icon: Package02Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "catalog-products-attributes",
|
||||||
|
label: "商品属性",
|
||||||
|
icon: Settings03Icon,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "catalog-product-specifications",
|
||||||
|
to: "/catalog/products/attributes/specifications",
|
||||||
|
label: "规格模板",
|
||||||
|
icon: ClipboardListIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "catalog-product-brands",
|
||||||
|
to: "/catalog/products/attributes/brands",
|
||||||
|
label: "品牌资料",
|
||||||
|
icon: StarIcon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "catalog-categories",
|
||||||
|
to: "/catalog/categories",
|
||||||
|
label: "分类目录",
|
||||||
|
icon: ClipboardListIcon,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "catalog-category-tree",
|
||||||
|
to: "/catalog/categories/tree",
|
||||||
|
label: "分类树",
|
||||||
|
icon: Structure01Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "catalog-category-navigation",
|
||||||
|
to: "/catalog/categories/navigation",
|
||||||
|
label: "导航分类",
|
||||||
|
icon: Compass01Icon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "inventory-center",
|
||||||
|
to: "/inventory",
|
||||||
|
label: "库存中心",
|
||||||
|
icon: PackageIcon,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "inventory-overview",
|
||||||
|
to: "/inventory/overview",
|
||||||
|
label: "库存总览",
|
||||||
|
icon: Chart01Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "inventory-inbound",
|
||||||
|
to: "/inventory/inbound",
|
||||||
|
label: "入库单",
|
||||||
|
icon: InboxDownloadIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "inventory-outbound",
|
||||||
|
to: "/inventory/outbound",
|
||||||
|
label: "出库单",
|
||||||
|
icon: InboxUploadIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "inventory-transfers",
|
||||||
|
to: "/inventory/transfers",
|
||||||
|
label: "调拨单",
|
||||||
|
icon: ArrowDataTransferHorizontalIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "inventory-alerts",
|
||||||
|
to: "/inventory/alerts",
|
||||||
|
label: "库存预警",
|
||||||
|
icon: Alert02Icon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "content-assets",
|
||||||
|
to: "/content",
|
||||||
|
label: "内容素材",
|
||||||
|
icon: File01Icon,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "content-articles",
|
||||||
|
to: "/content/articles",
|
||||||
|
label: "内容文章",
|
||||||
|
icon: File01Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "content-media",
|
||||||
|
to: "/content/media",
|
||||||
|
label: "媒体资源",
|
||||||
|
icon: GalleryHorizontalIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "content-pages",
|
||||||
|
to: "/content/pages",
|
||||||
|
label: "自定义页面",
|
||||||
|
icon: AppWindowIcon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "trade-and-fulfillment",
|
||||||
|
label: "交易与履约",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "trade-orders",
|
||||||
|
to: "/trade/orders",
|
||||||
|
label: "订单中心",
|
||||||
|
icon: ShoppingCart01Icon,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "trade-orders-awaiting-payment",
|
||||||
|
to: "/trade/orders/awaiting-payment",
|
||||||
|
label: "待付款订单",
|
||||||
|
icon: WalletCardsIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "trade-orders-awaiting-shipment",
|
||||||
|
to: "/trade/orders/awaiting-shipment",
|
||||||
|
label: "待发货订单",
|
||||||
|
icon: PackageIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "trade-orders-shipped",
|
||||||
|
to: "/trade/orders/shipped",
|
||||||
|
label: "配送中订单",
|
||||||
|
icon: DeliveryTruck01Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "trade-orders-completed",
|
||||||
|
to: "/trade/orders/completed",
|
||||||
|
label: "已完成订单",
|
||||||
|
icon: CheckmarkCircle01Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "trade-orders-canceled",
|
||||||
|
to: "/trade/orders/canceled",
|
||||||
|
label: "已取消订单",
|
||||||
|
icon: CancelCircleIcon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "fulfillment-center",
|
||||||
|
to: "/fulfillment",
|
||||||
|
label: "配送履约",
|
||||||
|
icon: DeliveryTruck01Icon,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "fulfillment-deliveries",
|
||||||
|
to: "/fulfillment/deliveries",
|
||||||
|
label: "配送任务",
|
||||||
|
icon: Location01Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "fulfillment-pickups",
|
||||||
|
to: "/fulfillment/pickups",
|
||||||
|
label: "自提核销",
|
||||||
|
icon: Store01Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "fulfillment-freight-rules",
|
||||||
|
to: "/fulfillment/freight-rules",
|
||||||
|
label: "运费规则",
|
||||||
|
icon: RoadLocation01Icon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "after-sales-center",
|
||||||
|
to: "/after-sales",
|
||||||
|
label: "售后中心",
|
||||||
|
icon: CallMissed01Icon,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "after-sales-returns",
|
||||||
|
to: "/after-sales/returns",
|
||||||
|
label: "退货申请",
|
||||||
|
icon: Refresh01Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "after-sales-refunds",
|
||||||
|
to: "/after-sales/refunds",
|
||||||
|
label: "退款审核",
|
||||||
|
icon: MoneySendSquareIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "after-sales-disputes",
|
||||||
|
to: "/after-sales/disputes",
|
||||||
|
label: "交易纠纷",
|
||||||
|
icon: SecurityWarningIcon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "customers-and-marketing",
|
||||||
|
label: "客户与营销",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "customer-center",
|
||||||
|
to: "/customers",
|
||||||
|
label: "客户中心",
|
||||||
|
icon: IdentityCardIcon,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "customer-profiles",
|
||||||
|
to: "/customers/profiles",
|
||||||
|
label: "客户档案",
|
||||||
|
icon: IdentityCardIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "customer-segments",
|
||||||
|
to: "/customers/segments",
|
||||||
|
label: "客户分群",
|
||||||
|
icon: UserGroupIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "customer-tiers",
|
||||||
|
to: "/customers/tiers",
|
||||||
|
label: "客户等级",
|
||||||
|
icon: CrownIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "customer-risk-list",
|
||||||
|
to: "/customers/risk-list",
|
||||||
|
label: "风险名单",
|
||||||
|
icon: SecurityWarningIcon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "marketing-campaigns",
|
||||||
|
to: "/marketing/campaigns",
|
||||||
|
label: "营销活动",
|
||||||
|
icon: Discount01Icon,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "marketing-coupons",
|
||||||
|
to: "/marketing/coupons",
|
||||||
|
label: "优惠券管理",
|
||||||
|
icon: Ticket01Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "marketing-flash-sales",
|
||||||
|
to: "/marketing/flash-sales",
|
||||||
|
label: "限时折扣",
|
||||||
|
icon: Clock01Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "marketing-gift-promotions",
|
||||||
|
to: "/marketing/gift-promotions",
|
||||||
|
label: "满赠活动",
|
||||||
|
icon: GiftIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "marketing-referrals",
|
||||||
|
to: "/marketing/referrals",
|
||||||
|
label: "邀请有礼",
|
||||||
|
icon: Share01Icon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "loyalty-center",
|
||||||
|
to: "/loyalty",
|
||||||
|
label: "会员权益",
|
||||||
|
icon: Medal01Icon,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "loyalty-points",
|
||||||
|
to: "/loyalty/points",
|
||||||
|
label: "积分规则",
|
||||||
|
icon: StarCircleIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "loyalty-stored-value",
|
||||||
|
to: "/loyalty/stored-value",
|
||||||
|
label: "储值账户",
|
||||||
|
icon: Wallet01Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "loyalty-benefits",
|
||||||
|
to: "/loyalty/benefits",
|
||||||
|
label: "权益配置",
|
||||||
|
icon: CheckmarkBadge01Icon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "data-and-finance",
|
||||||
|
label: "数据与财务",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "report-center",
|
||||||
|
to: "/reports",
|
||||||
|
label: "数据报表",
|
||||||
|
icon: Chart01Icon,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "report-sales",
|
||||||
|
to: "/reports/sales",
|
||||||
|
label: "销售分析",
|
||||||
|
icon: AnalyticsUpIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-products",
|
||||||
|
to: "/reports/products",
|
||||||
|
label: "商品分析",
|
||||||
|
icon: ShoppingBag01Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-customers",
|
||||||
|
to: "/reports/customers",
|
||||||
|
label: "客群分析",
|
||||||
|
icon: UserGroupIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "report-channels",
|
||||||
|
to: "/reports/channels",
|
||||||
|
label: "渠道分析",
|
||||||
|
icon: ChartIncreaseIcon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "finance-center",
|
||||||
|
to: "/finance",
|
||||||
|
label: "财务中心",
|
||||||
|
icon: WalletCardsIcon,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "finance-settlements",
|
||||||
|
to: "/finance/settlements",
|
||||||
|
label: "结算账单",
|
||||||
|
icon: Invoice03Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "finance-reconciliation",
|
||||||
|
to: "/finance/reconciliation",
|
||||||
|
label: "对账管理",
|
||||||
|
icon: Calculator01Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "finance-invoices",
|
||||||
|
to: "/finance/invoices",
|
||||||
|
label: "发票管理",
|
||||||
|
icon: File01Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "finance-payouts",
|
||||||
|
to: "/finance/payouts",
|
||||||
|
label: "打款记录",
|
||||||
|
icon: MoneySendSquareIcon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "system-management",
|
||||||
|
label: "系统管理",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "system-members",
|
||||||
|
to: "/system/members",
|
||||||
|
label: "组织成员",
|
||||||
|
icon: UserShield01Icon,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "system-members-admins",
|
||||||
|
to: "/system/members/admins",
|
||||||
|
label: "管理员账号",
|
||||||
|
icon: UserShield01Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "system-members-roles",
|
||||||
|
to: "/system/members/roles",
|
||||||
|
label: "角色管理",
|
||||||
|
icon: UserGroupIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "system-members-permissions",
|
||||||
|
to: "/system/members/permissions",
|
||||||
|
label: "权限策略",
|
||||||
|
icon: LockKeyIcon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "system-notifications",
|
||||||
|
to: "/system/notifications",
|
||||||
|
label: "消息中心",
|
||||||
|
icon: BubbleChatIcon,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "system-notification-templates",
|
||||||
|
to: "/system/notifications/templates",
|
||||||
|
label: "通知模板",
|
||||||
|
icon: File01Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "system-notification-logs",
|
||||||
|
to: "/system/notifications/logs",
|
||||||
|
label: "发送记录",
|
||||||
|
icon: HistoryIcon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "system-audit",
|
||||||
|
to: "/system/audit",
|
||||||
|
label: "操作审计",
|
||||||
|
icon: Note01Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "system-integrations",
|
||||||
|
to: "/system/integrations",
|
||||||
|
label: "开放集成",
|
||||||
|
icon: CodeSquareIcon,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "system-integration-api-keys",
|
||||||
|
to: "/system/integrations/api-keys",
|
||||||
|
label: "API 密钥",
|
||||||
|
icon: Key01Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "system-integration-webhooks",
|
||||||
|
to: "/system/integrations/webhooks",
|
||||||
|
label: "Webhook 配置",
|
||||||
|
icon: LinkCircleIcon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "system-settings",
|
||||||
|
to: "/system-configs",
|
||||||
|
label: "系统配置",
|
||||||
|
icon: Settings01Icon,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "system-settings-store",
|
||||||
|
to: "/system-configs/store",
|
||||||
|
label: "门店资料",
|
||||||
|
icon: Store01Icon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "system-settings-payments",
|
||||||
|
to: "/system-configs/payments",
|
||||||
|
label: "支付渠道",
|
||||||
|
icon: CreditCardIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "system-settings-shipping",
|
||||||
|
to: "/system-configs/shipping",
|
||||||
|
label: "配送设置",
|
||||||
|
icon: DeliveryTruck01Icon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "system-recycle-bin",
|
||||||
|
to: "/system/recycle-bin",
|
||||||
|
label: "回收站",
|
||||||
|
icon: Delete02Icon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
import {
|
||||||
|
DeliveryTruck01Icon,
|
||||||
|
Mail01Icon,
|
||||||
|
Message01Icon,
|
||||||
|
PackageIcon,
|
||||||
|
} from "@hugeicons/core-free-icons"
|
||||||
|
import type { QueryFunctionContext } from "@tanstack/react-query"
|
||||||
|
|
||||||
|
import type { Notification } from "@workspace/blocks/notifications"
|
||||||
|
|
||||||
|
const RANDOM_USER_API_URL =
|
||||||
|
"https://randomuser.me/api/1.4/?results=4&inc=name,email,picture&nat=au,ca,de,fr,gb,nl,nz,us&noinfo"
|
||||||
|
|
||||||
|
interface RandomUser {
|
||||||
|
email: string
|
||||||
|
name: {
|
||||||
|
first: string
|
||||||
|
last: string
|
||||||
|
}
|
||||||
|
picture: {
|
||||||
|
medium: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const HOUR_IN_MILLISECONDS = 60 * 60 * 1000
|
||||||
|
|
||||||
|
function getCreatedAt(hoursAgo: number) {
|
||||||
|
return new Date(Date.now() - hoursAgo * HOUR_IN_MILLISECONDS).toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fallbackNotifications: readonly Notification[] = [
|
||||||
|
{
|
||||||
|
id: "friend-request-deja-brady",
|
||||||
|
title: "Deja Brady sent you a friend request",
|
||||||
|
createdAt: getCreatedAt(11),
|
||||||
|
category: "Communication",
|
||||||
|
isRead: false,
|
||||||
|
media: {
|
||||||
|
kind: "avatar",
|
||||||
|
src: "https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/images/mock/avatar/avatar-2.webp",
|
||||||
|
alt: "Deja Brady",
|
||||||
|
},
|
||||||
|
actions: [
|
||||||
|
{ id: "accept", label: "Accept" },
|
||||||
|
{ id: "decline", label: "Decline", intent: "neutral" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "mention-minimal-ui",
|
||||||
|
title: "Jayvon Hull mentioned you in Minimal UI",
|
||||||
|
createdAt: getCreatedAt(24),
|
||||||
|
category: "Project UI",
|
||||||
|
to: "/workspace/tasks",
|
||||||
|
isRead: false,
|
||||||
|
media: {
|
||||||
|
kind: "avatar",
|
||||||
|
src: "https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/images/mock/avatar/avatar-3.webp",
|
||||||
|
alt: "Jayvon Hull",
|
||||||
|
},
|
||||||
|
actions: [{ id: "view", label: "View", intent: "neutral" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "order-placed",
|
||||||
|
title: "Your order is placed",
|
||||||
|
description: "The order is confirmed and waiting for shipping.",
|
||||||
|
createdAt: getCreatedAt(24 * 6),
|
||||||
|
category: "Order",
|
||||||
|
isRead: true,
|
||||||
|
media: {
|
||||||
|
kind: "icon",
|
||||||
|
icon: PackageIcon,
|
||||||
|
tone: "warning",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "order-shipping",
|
||||||
|
title: "Delivery in progress",
|
||||||
|
description: "Your order has left the warehouse and is being shipped.",
|
||||||
|
createdAt: getCreatedAt(24 * 7),
|
||||||
|
category: "Order",
|
||||||
|
isRead: true,
|
||||||
|
media: {
|
||||||
|
kind: "icon",
|
||||||
|
icon: DeliveryTruck01Icon,
|
||||||
|
tone: "success",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "unread-messages",
|
||||||
|
title: "You have 5 unread messages",
|
||||||
|
createdAt: getCreatedAt(24 * 8),
|
||||||
|
category: "Communication",
|
||||||
|
to: "/workspace/tasks",
|
||||||
|
isRead: true,
|
||||||
|
media: {
|
||||||
|
kind: "icon",
|
||||||
|
icon: Message01Icon,
|
||||||
|
tone: "info",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "new-mail",
|
||||||
|
title: "You have new mail",
|
||||||
|
createdAt: getCreatedAt(24 * 9),
|
||||||
|
category: "Communication",
|
||||||
|
isRead: true,
|
||||||
|
media: {
|
||||||
|
kind: "icon",
|
||||||
|
icon: Mail01Icon,
|
||||||
|
tone: "destructive",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const notificationsQueryKey = ["notifications"] as const
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRandomUser(value: unknown): value is RandomUser {
|
||||||
|
if (!isRecord(value) || !isRecord(value.name) || !isRecord(value.picture)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
typeof value.email === "string" &&
|
||||||
|
typeof value.name.first === "string" &&
|
||||||
|
typeof value.name.last === "string" &&
|
||||||
|
typeof value.picture.medium === "string"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserName(user: RandomUser) {
|
||||||
|
return `${user.name.first} ${user.name.last}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAvatarMedia(user: RandomUser) {
|
||||||
|
return {
|
||||||
|
alt: getUserName(user),
|
||||||
|
kind: "avatar" as const,
|
||||||
|
src: user.picture.medium,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRandomUserNotifications(
|
||||||
|
users: readonly RandomUser[]
|
||||||
|
): readonly Notification[] {
|
||||||
|
const [friend, mention, commenter, collaborator] = users
|
||||||
|
|
||||||
|
if (!friend || !mention || !commenter || !collaborator) {
|
||||||
|
throw new Error("Random User API returned too few users")
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: `friend-request:${friend.email}`,
|
||||||
|
title: `${getUserName(friend)} sent you a friend request`,
|
||||||
|
createdAt: getCreatedAt(11),
|
||||||
|
category: "Communication",
|
||||||
|
isRead: false,
|
||||||
|
media: getAvatarMedia(friend),
|
||||||
|
actions: [
|
||||||
|
{ id: "accept", label: "Accept" },
|
||||||
|
{ id: "decline", label: "Decline", intent: "neutral" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `mention:${mention.email}`,
|
||||||
|
title: `${getUserName(mention)} mentioned you in Minimal UI`,
|
||||||
|
createdAt: getCreatedAt(24),
|
||||||
|
category: "Project UI",
|
||||||
|
to: "/workspace/tasks",
|
||||||
|
isRead: false,
|
||||||
|
media: getAvatarMedia(mention),
|
||||||
|
actions: [{ id: "view", label: "View", intent: "neutral" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `comment:${commenter.email}`,
|
||||||
|
title: `${getUserName(commenter)} commented on your task`,
|
||||||
|
description: "Can you review the latest changes?",
|
||||||
|
createdAt: getCreatedAt(24 * 2),
|
||||||
|
category: "Project UI",
|
||||||
|
to: "/workspace/tasks",
|
||||||
|
isRead: false,
|
||||||
|
media: getAvatarMedia(commenter),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `collaborator:${collaborator.email}`,
|
||||||
|
title: `${getUserName(collaborator)} joined your workspace`,
|
||||||
|
createdAt: getCreatedAt(24 * 4),
|
||||||
|
category: "Team",
|
||||||
|
isRead: true,
|
||||||
|
media: getAvatarMedia(collaborator),
|
||||||
|
},
|
||||||
|
...fallbackNotifications.slice(2),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchMockNotifications({
|
||||||
|
signal,
|
||||||
|
}: QueryFunctionContext): Promise<readonly Notification[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(RANDOM_USER_API_URL, { signal })
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Random User API responded with ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: unknown = await response.json()
|
||||||
|
|
||||||
|
if (!isRecord(payload) || !Array.isArray(payload.results)) {
|
||||||
|
throw new Error("Random User API returned an invalid response")
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = payload.results.filter(isRandomUser)
|
||||||
|
|
||||||
|
return createRandomUserNotifications(users)
|
||||||
|
} catch (error) {
|
||||||
|
if (signal.aborted) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallbackNotifications
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { lazy, Suspense, type PropsWithChildren } from "react"
|
||||||
|
import { I18nProvider, type LocaleDefinition } from "@workspace/i18n"
|
||||||
|
|
||||||
|
import { messages as englishMessages } from "@/locales/en/messages"
|
||||||
|
import { messages as simplifiedChineseMessages } from "@/locales/zh-Hans/messages"
|
||||||
|
|
||||||
|
const I18nDevtool = import.meta.env.DEV
|
||||||
|
? lazy(() =>
|
||||||
|
import("@workspace/i18n/devtool").then((module) => ({
|
||||||
|
default: module.I18nDevtool,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
|
||||||
|
export const appLocales = [
|
||||||
|
{
|
||||||
|
label: "English",
|
||||||
|
locale: "en",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "简体中文",
|
||||||
|
locale: "zh-Hans",
|
||||||
|
},
|
||||||
|
] as const satisfies readonly LocaleDefinition[]
|
||||||
|
|
||||||
|
export type AppLocale = (typeof appLocales)[number]["locale"]
|
||||||
|
|
||||||
|
const appCatalogs = {
|
||||||
|
en: englishMessages,
|
||||||
|
"zh-Hans": simplifiedChineseMessages,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppI18nProviderProps extends PropsWithChildren {
|
||||||
|
locale?: AppLocale
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppI18nProvider({
|
||||||
|
children,
|
||||||
|
locale = "zh-Hans",
|
||||||
|
}: AppI18nProviderProps) {
|
||||||
|
return (
|
||||||
|
<I18nProvider catalogs={appCatalogs} locale={locale} locales={appLocales}>
|
||||||
|
{children}
|
||||||
|
{I18nDevtool && (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<I18nDevtool dark={"ui\\:dark"} />
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
|
</I18nProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { QueryClient } from "@tanstack/react-query"
|
||||||
|
|
||||||
|
export const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 1000 * 60 * 5,
|
||||||
|
gcTime: 1000 * 60 * 10,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
retry: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"X-Generator: @lingui/cli\n"
|
||||||
|
"Language: en\n"
|
||||||
|
"Project-Id-Version: \n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: \n"
|
||||||
|
"PO-Revision-Date: \n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"Plural-Forms: \n"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/*eslint-disable*/ import type { Messages } from "@lingui/core"
|
||||||
|
export const messages = JSON.parse("{}") as Messages
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"X-Generator: @lingui/cli\n"
|
||||||
|
"Language: zh-Hans\n"
|
||||||
|
"Project-Id-Version: \n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: \n"
|
||||||
|
"PO-Revision-Date: \n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"Plural-Forms: \n"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/*eslint-disable*/ import type { Messages } from "@lingui/core"
|
||||||
|
export const messages = JSON.parse("{}") as Messages
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { StrictMode } from "react"
|
|
||||||
import { createRoot } from "react-dom/client"
|
|
||||||
|
|
||||||
import "@workspace/ui/globals.css"
|
|
||||||
import { App } from "./App.tsx"
|
|
||||||
import { ThemeProvider } from "@/components/theme-provider.tsx"
|
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
|
||||||
<StrictMode>
|
|
||||||
<ThemeProvider>
|
|
||||||
<App />
|
|
||||||
</ThemeProvider>
|
|
||||||
</StrictMode>
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Button } from "@workspace/ui/components/button"
|
||||||
|
import { Slider } from "@workspace/ui/components/slider"
|
||||||
|
import { Switch } from "@workspace/ui/components/switch"
|
||||||
|
|
||||||
|
export function DashboardPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
className="h-auto w-full"
|
||||||
|
src="https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/background/background-3-blur.webp"
|
||||||
|
/>
|
||||||
|
<div className="flex max-w-md min-w-0 flex-col gap-4 text-sm leading-loose">
|
||||||
|
<div>
|
||||||
|
<h1 className="font-medium">Project ready!</h1>
|
||||||
|
<p>You may now add components and start building.</p>
|
||||||
|
<p>We've already added the button component for you.</p>
|
||||||
|
<Button variant="ghost" className="mt-2">
|
||||||
|
Button
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Switch />
|
||||||
|
<Slider
|
||||||
|
defaultValue={[75]}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
className="mx-auto w-full max-w-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="font-mono text-xs text-muted-foreground">
|
||||||
|
(Press <kbd>Mod+D</kbd> to toggle dark mode)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="mr-2">breakpoint:</span>
|
||||||
|
<span className="hidden mobile:inline">mobile</span>
|
||||||
|
<span className="hidden tablet:inline">tablet</span>
|
||||||
|
<span className="hidden desktop:inline">desktop</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="mr-2">collapsed:</span>
|
||||||
|
<span className="hidden collapsed:inline">true</span>
|
||||||
|
<span className="inline collapsed:hidden">false</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
|
||||||
|
// This file was automatically generated by TanStack Router.
|
||||||
|
// You should NOT make any changes in this file as it will be overwritten.
|
||||||
|
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||||
|
|
||||||
|
import { Route as rootRouteImport } from './routes/__root'
|
||||||
|
import { Route as AppRouteImport } from './routes/_app'
|
||||||
|
import { Route as AppIndexRouteImport } from './routes/_app.index'
|
||||||
|
import { Route as AppSplatRouteImport } from './routes/_app.$'
|
||||||
|
|
||||||
|
const AppRoute = AppRouteImport.update({
|
||||||
|
id: '/_app',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const AppIndexRoute = AppIndexRouteImport.update({
|
||||||
|
id: '/',
|
||||||
|
path: '/',
|
||||||
|
getParentRoute: () => AppRoute,
|
||||||
|
} as any)
|
||||||
|
const AppSplatRoute = AppSplatRouteImport.update({
|
||||||
|
id: '/$',
|
||||||
|
path: '/$',
|
||||||
|
getParentRoute: () => AppRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
export interface FileRoutesByFullPath {
|
||||||
|
'/': typeof AppIndexRoute
|
||||||
|
'/$': typeof AppSplatRoute
|
||||||
|
}
|
||||||
|
export interface FileRoutesByTo {
|
||||||
|
'/$': typeof AppSplatRoute
|
||||||
|
'/': typeof AppIndexRoute
|
||||||
|
}
|
||||||
|
export interface FileRoutesById {
|
||||||
|
__root__: typeof rootRouteImport
|
||||||
|
'/_app': typeof AppRouteWithChildren
|
||||||
|
'/_app/$': typeof AppSplatRoute
|
||||||
|
'/_app/': typeof AppIndexRoute
|
||||||
|
}
|
||||||
|
export interface FileRouteTypes {
|
||||||
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
|
fullPaths: '/' | '/$'
|
||||||
|
fileRoutesByTo: FileRoutesByTo
|
||||||
|
to: '/$' | '/'
|
||||||
|
id: '__root__' | '/_app' | '/_app/$' | '/_app/'
|
||||||
|
fileRoutesById: FileRoutesById
|
||||||
|
}
|
||||||
|
export interface RootRouteChildren {
|
||||||
|
AppRoute: typeof AppRouteWithChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tanstack/react-router' {
|
||||||
|
interface FileRoutesByPath {
|
||||||
|
'/_app': {
|
||||||
|
id: '/_app'
|
||||||
|
path: ''
|
||||||
|
fullPath: '/'
|
||||||
|
preLoaderRoute: typeof AppRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/_app/': {
|
||||||
|
id: '/_app/'
|
||||||
|
path: '/'
|
||||||
|
fullPath: '/'
|
||||||
|
preLoaderRoute: typeof AppIndexRouteImport
|
||||||
|
parentRoute: typeof AppRoute
|
||||||
|
}
|
||||||
|
'/_app/$': {
|
||||||
|
id: '/_app/$'
|
||||||
|
path: '/$'
|
||||||
|
fullPath: '/$'
|
||||||
|
preLoaderRoute: typeof AppSplatRouteImport
|
||||||
|
parentRoute: typeof AppRoute
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppRouteChildren {
|
||||||
|
AppSplatRoute: typeof AppSplatRoute
|
||||||
|
AppIndexRoute: typeof AppIndexRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppRouteChildren: AppRouteChildren = {
|
||||||
|
AppSplatRoute: AppSplatRoute,
|
||||||
|
AppIndexRoute: AppIndexRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppRouteWithChildren = AppRoute._addFileChildren(AppRouteChildren)
|
||||||
|
|
||||||
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
|
AppRoute: AppRouteWithChildren,
|
||||||
|
}
|
||||||
|
export const routeTree = rootRouteImport
|
||||||
|
._addFileChildren(rootRouteChildren)
|
||||||
|
._addFileTypes<FileRouteTypes>()
|
||||||
|
|
||||||
|
import type { getRouter } from './router.tsx'
|
||||||
|
import type { createStart } from '@tanstack/react-start'
|
||||||
|
declare module '@tanstack/react-start' {
|
||||||
|
interface Register {
|
||||||
|
ssr: true
|
||||||
|
router: Awaited<ReturnType<typeof getRouter>>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { createRouter } from "@tanstack/react-router"
|
||||||
|
|
||||||
|
import { routeTree } from "./route-tree.gen"
|
||||||
|
|
||||||
|
export function getRouter() {
|
||||||
|
return createRouter({
|
||||||
|
routeTree,
|
||||||
|
defaultPreload: "intent",
|
||||||
|
scrollRestoration: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import type { ReactNode } from "react"
|
||||||
|
import {
|
||||||
|
HeadContent,
|
||||||
|
Outlet,
|
||||||
|
Scripts,
|
||||||
|
createRootRoute,
|
||||||
|
} from "@tanstack/react-router"
|
||||||
|
import {
|
||||||
|
BreakpointReactor,
|
||||||
|
getBreakpointInitializationScript,
|
||||||
|
} from "@workspace/ui/hooks/use-breakpoint"
|
||||||
|
|
||||||
|
import appCss from "@/styles/globals.css?url"
|
||||||
|
import {
|
||||||
|
AppearanceController,
|
||||||
|
buildThemeStyleSheet,
|
||||||
|
THEME_STYLE_ELEMENT_ID,
|
||||||
|
type UiState,
|
||||||
|
UiStateProvider,
|
||||||
|
} from "@workspace/blocks/appearance"
|
||||||
|
import { SidebarStateProvider } from "@workspace/blocks/layout"
|
||||||
|
import { getUiState, setUiState } from "@/server/ui-state.functions"
|
||||||
|
import { HotkeysProvider } from "@tanstack/react-hotkeys"
|
||||||
|
import { TooltipProvider } from "@workspace/ui/components/tooltip"
|
||||||
|
import { QueryClientProvider } from "@tanstack/react-query"
|
||||||
|
import { AppI18nProvider } from "@/i18n/app-i18n-provider"
|
||||||
|
import { queryClient } from "@/lib/query-client"
|
||||||
|
|
||||||
|
export const Route = createRootRoute({
|
||||||
|
loader: () => getUiState(),
|
||||||
|
head: () => ({
|
||||||
|
meta: [
|
||||||
|
{ charSet: "utf-8" },
|
||||||
|
{
|
||||||
|
name: "viewport",
|
||||||
|
content: "width=device-width, initial-scale=1",
|
||||||
|
},
|
||||||
|
{ title: "Simple React App Kit" },
|
||||||
|
],
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
rel: "stylesheet",
|
||||||
|
href: appCss,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
component: RootComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RootComponent() {
|
||||||
|
const initialState = Route.useLoaderData()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RootDocument initialState={initialState}>
|
||||||
|
<AppI18nProvider>
|
||||||
|
<BreakpointReactor />
|
||||||
|
<UiStateProvider
|
||||||
|
initialState={initialState}
|
||||||
|
onStateChange={(update) => setUiState({ data: update })}
|
||||||
|
>
|
||||||
|
<SidebarStateProvider>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<HotkeysProvider>
|
||||||
|
<TooltipProvider>
|
||||||
|
<AppearanceController />
|
||||||
|
<Outlet />
|
||||||
|
</TooltipProvider>
|
||||||
|
</HotkeysProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</SidebarStateProvider>
|
||||||
|
</UiStateProvider>
|
||||||
|
</AppI18nProvider>
|
||||||
|
</RootDocument>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUiInitializationScript(state: UiState) {
|
||||||
|
return `(() => {
|
||||||
|
const theme = ${JSON.stringify(state["theme-mode"])};
|
||||||
|
const resolvedTheme =
|
||||||
|
theme === "system"
|
||||||
|
? window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
? "dark"
|
||||||
|
: "light"
|
||||||
|
: theme;
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.classList.remove("ui:light", "ui:dark");
|
||||||
|
root.classList.add("ui:" + resolvedTheme);
|
||||||
|
root.classList.toggle("ui:compacted", ${state["main-compacted"]});
|
||||||
|
root.classList.toggle("ui:collapsed", ${state["sidebar-collapsed"]});
|
||||||
|
root.style.colorScheme = resolvedTheme;
|
||||||
|
})();`
|
||||||
|
}
|
||||||
|
|
||||||
|
function RootDocument({
|
||||||
|
children,
|
||||||
|
initialState,
|
||||||
|
}: Readonly<{
|
||||||
|
children: ReactNode
|
||||||
|
initialState: UiState
|
||||||
|
}>) {
|
||||||
|
const themeMode = initialState["theme-mode"]
|
||||||
|
const className = [
|
||||||
|
themeMode === "system" ? null : `ui:${themeMode}`,
|
||||||
|
initialState["main-compacted"] ? "ui:compacted" : null,
|
||||||
|
initialState["sidebar-collapsed"] ? "ui:collapsed" : null,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html
|
||||||
|
lang="zh-CN"
|
||||||
|
suppressHydrationWarning
|
||||||
|
className={className || undefined}
|
||||||
|
style={themeMode === "system" ? undefined : { colorScheme: themeMode }}
|
||||||
|
>
|
||||||
|
<head>
|
||||||
|
<script
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: getBreakpointInitializationScript(),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<script
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: getUiInitializationScript(initialState),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<HeadContent />
|
||||||
|
<style
|
||||||
|
id={THEME_STYLE_ELEMENT_ID}
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: buildThemeStyleSheet(
|
||||||
|
initialState["theme-light"],
|
||||||
|
initialState["theme-dark"]
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{children}
|
||||||
|
<Scripts />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
|
||||||
|
import { DashboardPage } from "@/pages/dashboard-page"
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_app/$")({
|
||||||
|
component: DashboardPage,
|
||||||
|
})
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
|
||||||
|
import { DashboardPage } from "@/pages/dashboard-page"
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_app/")({
|
||||||
|
component: DashboardPage,
|
||||||
|
})
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Outlet, createFileRoute } from "@tanstack/react-router"
|
||||||
|
|
||||||
|
import { AppLayout } from "@workspace/blocks/layout"
|
||||||
|
|
||||||
|
import { chatThreads } from "@/data/chats"
|
||||||
|
import { primaryNavigationGroups } from "@/data/navigation"
|
||||||
|
import {
|
||||||
|
fetchMockNotifications,
|
||||||
|
notificationsQueryKey,
|
||||||
|
} from "@/data/notifications"
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_app")({
|
||||||
|
component: ApplicationLayoutRoute,
|
||||||
|
})
|
||||||
|
|
||||||
|
function ApplicationLayoutRoute() {
|
||||||
|
return (
|
||||||
|
<AppLayout
|
||||||
|
chatThreads={chatThreads}
|
||||||
|
navigationGroups={primaryNavigationGroups}
|
||||||
|
notifications={{
|
||||||
|
queryFn: fetchMockNotifications,
|
||||||
|
queryKey: notificationsQueryKey,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Outlet />
|
||||||
|
</AppLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { createServerFn } from "@tanstack/react-start"
|
||||||
|
import { isUiStateUpdate } from "@workspace/blocks/appearance"
|
||||||
|
import { readUiState, writeUiState } from "./ui-state.server"
|
||||||
|
|
||||||
|
export const getUiState = createServerFn({ method: "GET" }).handler(() => {
|
||||||
|
return readUiState()
|
||||||
|
})
|
||||||
|
|
||||||
|
export const setUiState = createServerFn({ method: "POST" })
|
||||||
|
.validator((value: unknown) => {
|
||||||
|
if (!isUiStateUpdate(value)) {
|
||||||
|
throw new Error("Invalid UI state update")
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
})
|
||||||
|
.handler(({ data }) => {
|
||||||
|
writeUiState(data.key, data.value)
|
||||||
|
})
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import "@tanstack/react-start/server-only"
|
||||||
|
import { getCookie, setCookie } from "@tanstack/react-start/server"
|
||||||
|
import {
|
||||||
|
type UiState,
|
||||||
|
type UiStateKey,
|
||||||
|
uiStateDefinitions,
|
||||||
|
} from "@workspace/blocks/appearance"
|
||||||
|
|
||||||
|
const LEGACY_THEME_COOKIE = "simple-shop-admin-theme"
|
||||||
|
|
||||||
|
export function readUiState(): UiState {
|
||||||
|
return {
|
||||||
|
"main-compacted": uiStateDefinitions["main-compacted"].parse(
|
||||||
|
getCookie(uiStateDefinitions["main-compacted"].cookie)
|
||||||
|
),
|
||||||
|
"sidebar-collapsed": uiStateDefinitions["sidebar-collapsed"].parse(
|
||||||
|
getCookie(uiStateDefinitions["sidebar-collapsed"].cookie)
|
||||||
|
),
|
||||||
|
"theme-mode": uiStateDefinitions["theme-mode"].parse(
|
||||||
|
getCookie(uiStateDefinitions["theme-mode"].cookie) ??
|
||||||
|
getCookie(LEGACY_THEME_COOKIE)
|
||||||
|
),
|
||||||
|
"theme-light": uiStateDefinitions["theme-light"].parse(
|
||||||
|
getCookie(uiStateDefinitions["theme-light"].cookie)
|
||||||
|
),
|
||||||
|
"theme-dark": uiStateDefinitions["theme-dark"].parse(
|
||||||
|
getCookie(uiStateDefinitions["theme-dark"].cookie)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeUiState<K extends UiStateKey>(key: K, value: UiState[K]) {
|
||||||
|
const definition = uiStateDefinitions[key]
|
||||||
|
setCookie(definition.cookie, definition.serialize(value), {
|
||||||
|
path: "/",
|
||||||
|
sameSite: "lax",
|
||||||
|
maxAge: 60 * 60 * 24 * 365,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
@import "@workspace/ui/globals.css";
|
||||||
|
@import "@workspace/blocks/globals.css";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--ui-content-top-gap: --spacing(2);
|
||||||
|
--ui-header-height: --spacing(18);
|
||||||
|
--ui-sidebar-collapsed-width: --spacing(22);
|
||||||
|
--ui-sidebar-width: --spacing(74);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui\:mobile {
|
||||||
|
--ui-header-height: --spacing(16);
|
||||||
|
--ui-sidebar-width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui\:tablet,
|
||||||
|
.ui\:collapsed:not(.ui\:mobile) {
|
||||||
|
--ui-sidebar-width: var(--ui-sidebar-collapsed-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui\:theme-changing *,
|
||||||
|
.ui\:theme-changing *::before,
|
||||||
|
.ui\:theme-changing *::after {
|
||||||
|
-webkit-transition: none !important;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ui-show-scroll-divider {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility scroll-top-divider {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (animation-timeline: scroll()) {
|
||||||
|
.scroll-top-divider {
|
||||||
|
animation: ui-show-scroll-divider 1s steps(1, end) both;
|
||||||
|
animation-range: 0px var(--ui-content-top-gap, 1px);
|
||||||
|
animation-timeline: scroll(root block);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": false,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"],
|
"@/*": ["./src/*"],
|
||||||
|
"@workspace/blocks/*": ["../../packages/blocks/src/*"],
|
||||||
"@workspace/ui/*": ["../../packages/ui/src/*"]
|
"@workspace/ui/*": ["../../packages/ui/src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"],
|
"@/*": ["./src/*"],
|
||||||
|
"@workspace/blocks/*": ["../../packages/blocks/src/*"],
|
||||||
"@workspace/ui/*": ["../../packages/ui/src/*"]
|
"@workspace/ui/*": ["../../packages/ui/src/*"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
@@ -20,5 +21,5 @@
|
|||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": true,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true
|
||||||
},
|
},
|
||||||
"include": ["vite.config.ts"]
|
"include": ["vite.config.ts", "lingui.config.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-1
@@ -1,14 +1,36 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import tailwindcss from "@tailwindcss/vite"
|
import tailwindcss from "@tailwindcss/vite"
|
||||||
|
import { tanstackStart } from "@tanstack/react-start/plugin/vite"
|
||||||
|
import { i18n } from "@workspace/i18n/vite"
|
||||||
import react from "@vitejs/plugin-react"
|
import react from "@vitejs/plugin-react"
|
||||||
|
import { nitro } from "nitro/vite"
|
||||||
import { defineConfig } from "vite"
|
import { defineConfig } from "vite"
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss()],
|
|
||||||
resolve: {
|
resolve: {
|
||||||
|
tsconfigPaths: true,
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
include: ["use-sync-external-store/shim/with-selector"],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
i18n(),
|
||||||
|
tailwindcss(),
|
||||||
|
tanstackStart({
|
||||||
|
router: {
|
||||||
|
generatedRouteTree: "route-tree.gen.ts",
|
||||||
|
},
|
||||||
|
srcDirectory: "src",
|
||||||
|
}),
|
||||||
|
react(),
|
||||||
|
nitro(),
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
host: "0.0.0.0",
|
||||||
|
port: 31573,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user