refactor(with-lingui): move theme and session, merge mock utils

- Theme hooks: tokenBuilders + useAppTheme under hooks; drop theme barrel
- Session fetch helper under utils; update entry/login imports
- Mock filter + avatar helpers in mocks/utils; avatars from API in UI
- README tree updates

Made-with: Cursor
This commit is contained in:
zuiidea
2026-04-14 19:12:48 +08:00
parent 47fd1aea90
commit 29888fd1ad
15 changed files with 23 additions and 28 deletions
+3 -4
View File
@@ -104,6 +104,8 @@ src/
│ ├── UserMenu/ # User dropdown in sidebar: index.tsx + index.css
│ └── Header/ # Top bar: index.tsx
├── hooks/
│ ├── tokenBuilders.ts # Shared Ant Design token/config builders
│ ├── useAppTheme.ts # Theme selection hook (ConfigProvider)
│ ├── usePermission.ts # Permission check hook
│ └── useResourceCRUD.ts # Shared CRUD query/mutation wiring
├── utils/
@@ -116,7 +118,7 @@ src/
│ ├── browser.ts # MSW worker setup
│ ├── createHandler.ts # Shared MSW success/error/delay helpers
│ ├── data.ts # Mock seed data (users, menus)
│ ├── filterUtils.ts # Mock-only filtering and pagination helpers
│ ├── utils.ts # Mock-only helpers (filters, pagination, demo avatar URLs)
│ └── handlers/ # Request handlers (auth, user CRUD)
├── routes/ # TanStack Router file-based routes
│ ├── __root.tsx # Root layout (QueryClient, ConfigProvider, I18n)
@@ -131,9 +133,6 @@ src/
│ ├── auth.ts # Auth store (tokens, user, menus, permissions)
│ ├── createPersistentStore.ts # Shared persisted-store factory
│ └── settings.ts # Settings store (darkMode, locale, sidebar)
├── theme/
│ ├── tokenBuilders.ts # Shared Ant Design token/config builders
│ └── useAppTheme.ts # Theme selection hook
└── main.tsx # Entry point (MSW init → React render)
tests/
+3 -4
View File
@@ -104,6 +104,8 @@ src/
│ ├── UserMenu/ # User dropdown in sidebar: index.tsx + index.css
│ └── Header/ # Top bar: index.tsx
├── hooks/
│ ├── tokenBuilders.ts # Shared Ant Design token/config builders
│ ├── useAppTheme.ts # Theme selection hook (ConfigProvider)
│ ├── usePermission.ts # Permission check hook
│ └── useResourceCRUD.ts # Shared CRUD query/mutation wiring
├── utils/
@@ -116,7 +118,7 @@ src/
│ ├── browser.ts # MSW worker setup
│ ├── createHandler.ts # Shared MSW success/error/delay helpers
│ ├── data.ts # Mock seed data (users, menus)
│ ├── filterUtils.ts # Mock-only filtering and pagination helpers
│ ├── utils.ts # Mock-only helpers (filters, pagination, demo avatar URLs)
│ └── handlers/ # Request handlers (auth, user CRUD)
├── routes/ # TanStack Router file-based routes
│ ├── __root.tsx # Root layout (QueryClient, ConfigProvider, I18n)
@@ -131,9 +133,6 @@ src/
│ ├── auth.ts # Auth store (tokens, user, menus, permissions)
│ ├── createPersistentStore.ts # Shared persisted-store factory
│ └── settings.ts # Settings store (darkMode, locale, sidebar)
├── theme/
│ ├── tokenBuilders.ts # Shared Ant Design token/config builders
│ └── useAppTheme.ts # Theme selection hook
└── main.tsx # Entry point (MSW init → React render)
tests/
@@ -1,7 +1,6 @@
import { Avatar, Button, Dropdown, Flex, Typography, theme } from "antd";
import type { MenuProps } from "antd";
import type { User } from "@/api/schemas";
import { vercelAvatarUrl } from "@/utils/avatarUrl";
import { MoreVertical } from "lucide-react";
import "./index.css";
@@ -16,8 +15,7 @@ interface UserMenuProps {
export function UserMenu({ collapsed, user, userMenuItems }: UserMenuProps) {
const { token } = theme.useToken();
const avatarSrc =
(user?.avatar ?? "").trim() || (user?.username ? vercelAvatarUrl(user.username) : undefined);
const avatarSrc = (user?.avatar ?? "").trim() || undefined;
const avatarSize = collapsed ? 32 : token.controlHeight;
const avatar = (
@@ -3,6 +3,10 @@ import type { ConfigProviderProps } from "antd";
import { buildLightThemeConfig, buildDarkThemeConfig } from "./tokenBuilders";
import { useSettingsStore } from "@/stores/settings";
/**
* Theme selection for ConfigProvider: light/dark share radius, font, control tweaks.
* @see https://ant.design/docs/react/customize-theme
*/
export function useAppTheme(): ConfigProviderProps {
const darkMode = useSettingsStore((s) => s.darkMode);
+1 -1
View File
@@ -7,7 +7,7 @@ import { messages as enMessages } from "./locales/en/messages.po";
import { messages as zhMessages } from "./locales/zh/messages.po";
import { useSettingsStore } from "./stores/settings";
import { useAuthStore } from "./stores/auth";
import { fetchSessionAndApplyToStore } from "./auth/session";
import { fetchSessionAndApplyToStore } from "./utils/session";
i18n.load("en", enMessages);
i18n.load("zh", zhMessages);
+1 -1
View File
@@ -1,5 +1,5 @@
import type { User } from "@/api/schemas";
import { vercelAvatarUrl } from "@/utils/avatarUrl";
import { vercelAvatarUrl } from "./utils";
/** Demo logins look like real org accounts (first.last @ northstar.io). Index 0 stays `admin` for mock login. */
const MOCK_IDENTITIES: ReadonlyArray<[string, string]> = [
+1 -1
View File
@@ -1,6 +1,6 @@
import { http } from "msw";
import { MOCK_USERS } from "../data";
import { filterUsers, paginateList, parsePaginationParams } from "../filterUtils";
import { filterUsers, paginateList, parsePaginationParams } from "../utils";
import { withDelay, successResponse, errorResponse, ERROR_CODES } from "../createHandler";
let users = [...MOCK_USERS];
@@ -1,7 +1,12 @@
/**
* Common filtering utilities for mock user handlers.
* Mock-only helpers: demo avatars, list filtering, pagination.
*/
/** Deterministic demo avatar URL from username (mock seed data only). */
export function vercelAvatarUrl(username: string): string {
return `https://vercel.com/api/www/avatar?u=${encodeURIComponent(username)}`;
}
export interface User {
id: string;
username: string;
+1 -1
View File
@@ -7,7 +7,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { useSettingsStore } from "@/stores/settings";
import { useAppTheme } from "@/theme";
import { useAppTheme } from "@/hooks/useAppTheme";
import { NotFound } from "@/components/NotFound";
import "@/index.css";
@@ -13,7 +13,6 @@ import { DataTable } from "@/components/DataTable";
import { useResourceCRUD } from "@/hooks/useResourceCRUD";
import { Toolbar } from "./-Toolbar";
import { FormModal } from "./-FormModal";
import { vercelAvatarUrl } from "@/utils/avatarUrl";
const UserSearchParamsSchema = z.object({
limit: z.number().int().positive().catch(100),
@@ -188,7 +187,7 @@ function UsersPage() {
sorter: true,
sortOrder: search.sortField === "username" ? search.sortOrder : null,
render: (_: unknown, record: User) => {
const src = record.avatar?.trim() || vercelAvatarUrl(record.username);
const src = (record.avatar ?? "").trim() || undefined;
return (
<Flex align="center" gap={token.marginSM} style={{ minWidth: 0 }}>
<Avatar size={24} src={src} shape="circle" style={{ flexShrink: 0 }}>
+1 -1
View File
@@ -8,7 +8,7 @@ import { useAuthStore } from "@/stores/auth";
import { useSettingsStore } from "@/stores/settings";
import { AUTH_ENDPOINTS } from "@/api/auth";
import { LoginRequestSchema, AuthTokensSchema } from "@/api/schemas";
import { fetchSessionAndApplyToStore } from "@/auth/session";
import { fetchSessionAndApplyToStore } from "@/utils/session";
import type { LoginRequest } from "@/api/schemas";
import { APP_BRAND_NAME, APP_FAVICON_SRC } from "@/utils/constants";
import { Languages } from "lucide-react";
-5
View File
@@ -1,5 +0,0 @@
/**
* Theme entry: light/dark share radius, font, control tweaks; each sets primary/layout/border neutrals.
* @see https://ant.design/docs/react/customize-theme
*/
export { useAppTheme } from "./useAppTheme";
-4
View File
@@ -1,4 +0,0 @@
/** Deterministic avatar image from username (Vercel avatar API). */
export function vercelAvatarUrl(username: string): string {
return `https://vercel.com/api/www/avatar?u=${encodeURIComponent(username)}`;
}