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:
Maofeng
2026-07-29 21:23:17 +08:00
parent 9e3f1d6b59
commit a9480ad9aa
32 changed files with 1555 additions and 309 deletions
+226
View File
@@ -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
}
}