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:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user