diff --git a/apps/web/src/design-preview/access-sheet.tsx b/apps/web/src/design-preview/access-sheet.tsx index 08e8d11..6f3ac46 100644 --- a/apps/web/src/design-preview/access-sheet.tsx +++ b/apps/web/src/design-preview/access-sheet.tsx @@ -1,4 +1,5 @@ import { useMemo, useState } from 'react' +import { ArrowUpRightIcon } from 'lucide-react' import type { AdminUser } from '@authportal/shared' import { Button } from '@authportal/ui/components/button' import { AccessEditor } from './access-editor' @@ -10,10 +11,12 @@ export function AccessSheet({ user, onClose, onSave, + onOpenProfile, }: { user: AdminUser onClose: () => void onSave: (value: AccessValue) => void + onOpenProfile?: () => void }) { const [value, setValue] = useState({ apps: user.apps, permissions: user.permissions }) const changes = useMemo(() => diffAccess({ apps: user.apps, permissions: user.permissions }, value).length, [user, value]) @@ -28,6 +31,12 @@ export function AccessSheet({ description="Доступ к приложениям и разделам · иерархия: админ ⊃ запись ⊃ чтение" footer={ <> + {onOpenProfile ? ( + + ) : null} {changes > 0 ? `Изменений: ${changes}` : 'Без изменений'} diff --git a/apps/web/src/design-preview/pages/user-profile-page.tsx b/apps/web/src/design-preview/pages/user-profile-page.tsx new file mode 100644 index 0000000..c1ad43e --- /dev/null +++ b/apps/web/src/design-preview/pages/user-profile-page.tsx @@ -0,0 +1,304 @@ +import { useMemo, useState } from 'react' +import { Link, useNavigate, useParams } from '@tanstack/react-router' +import { + ArrowLeftIcon, + FingerprintIcon, + HistoryIcon, + MonitorSmartphoneIcon, + ShieldCheckIcon, + TriangleAlertIcon, +} from 'lucide-react' +import { toast } from 'sonner' +import type { AdminUser } from '@authportal/shared' +import { AuditLogTimeline } from '@/components/reui-kit/audit-log-timeline' +import { Frame, FrameDescription, FrameFooter, FrameHeader, FramePanel, FrameTitle } from '@/components/reui/frame' +import { Avatar, AvatarFallback } from '@authportal/ui/components/avatar' +import { Button } from '@authportal/ui/components/button' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@authportal/ui/components/tabs' +import { AccessEditor } from '../access-editor' +import { diffAccess, type AccessValue } from '../access-model' +import { KvFrame, PendingChip, StatusBadge, TypedConfirm } from '../bits' +import { MOCK_AUDIT, MOCK_SESSIONS, USER_META, type SessionRow } from '../mock' +import { usePreviewStore } from '../preview-store' +import { PreviewSectionGrid, TwoLineCell, type GridColumn } from '../section-grid' +import { usePatchUsers } from '../user-actions' + +function initials(u: AdminUser) { + const base = (u.name || u.email).trim() + const parts = base.split(/\s+/) + return (parts.length > 1 ? `${parts[0]![0]}${parts[1]![0]}` : base.slice(0, 2)).toUpperCase() +} + +function statusOf(u: AdminUser) { + if (u.disabled) return { tone: 'mu' as const, label: 'Отключён' } + if (u.passkey_count === 0) return { tone: 'wn' as const, label: 'Без passkey' } + return { tone: 'ok' as const, label: 'Активен' } +} + +/** Страница пользователя (solution-users-2): рейка вкладок — права, вход, сессии, активность, опасная зона. */ +export function UserProfilePage() { + const { userId } = useParams({ from: '/design-preview/_shell/users/$userId' }) + const { users, setUsers, addChange, marksFor } = usePreviewStore() + const navigate = useNavigate() + const patch = usePatchUsers() + const user = users.find((u) => u.id === userId) + const mark = marksFor('users')[userId] + + const [draft, setDraft] = useState<{ id: string; value: AccessValue } | null>(null) + const [sessions, setSessions] = useState(MOCK_SESSIONS) + const [resetOpen, setResetOpen] = useState(false) + const [deleteOpen, setDeleteOpen] = useState(false) + + const sessionRows = useMemo(() => sessions.filter((s) => s.user === user?.email), [sessions, user]) + const events = useMemo( + () => MOCK_AUDIT.filter((e) => e.actor_email === user?.email || e.summary.includes(user?.email ?? '\0')), + [user], + ) + + if (!user) { + return ( + + +

Пользователь не найден

+

Возможно, учётная запись уже удалена.

+ +
+ + ) + } + + const status = statusOf(user) + const base: AccessValue = { apps: user.apps, permissions: user.permissions } + const value = draft?.id === user.id ? draft.value : base + const changes = diffAccess(base, value).length + + const sessionColumns: GridColumn[] = [ + { id: 'ip', title: 'IP · клиент', size: 260, cell: (r) => }, + { id: 'created', title: 'Создана', size: 140, cell: (r) => {r.created} }, + { id: 'exp', title: 'Истекает', size: 120, cell: (r) => {r.expires} }, + { + id: 'act', + title: '', + size: 130, + cell: (r) => ( + + ), + }, + ] + + return ( + <> + + + + {initials(user)} + +
+

{user.email}

+

{USER_META[user.id]?.sub ?? user.name}

+
+ {status.label} + {user.is_admin ? 'Админ' : 'Пользователь'} + {mark ? : null} +
+
+ +
+ + + + + + + Права доступа + + + + Вход и passkeys + + + + Сессии · {sessionRows.length} + + + + Активность + + + + Опасная зона + + + +
+ + + + Права доступа + Приложения и разделы · иерархия: админ ⊃ запись ⊃ чтение + + + setDraft({ id: user.id, value: next })} /> + + + {changes > 0 ? `Изменений: ${changes}` : 'Без изменений'} + + + + + + + + + + +
+ Сбросить passkeys… + Войти можно будет только по паролю, пока не добавлен новый ключ. +
+ +
+ +
+ + + r.id} + emptyMessage="Нет активных сессий." + /> + + + + + + Активность + События пользователя из журнала портала + + + {events.length === 0 ? ( +

Событий нет.

+ ) : ( + + )} +
+ +
+ + + + + Опасная зона + Действия попадают в «Не сохранено» и в журнал + + +
+
+ {user.disabled ? 'Включить учётную запись' : 'Отключить учётную запись'} + Вход закрывается, учётная запись и права сохраняются. +
+ +
+
+
+ Удалить пользователя… + Учётная запись и все права будут удалены безвозвратно. +
+ +
+
+ +
+
+
+ + { + patch([user.id], (u) => ({ ...u, passkey_count: 0 }), `Сброс passkeys: ${user.email}`, (b) => [`− passkeys: ${b.passkey_count}`, '+ passkeys: 0']) + setResetOpen(false) + }} + /> + { + addChange({ + kind: 'del', + title: `Удалён ${user.email}`, + lines: [`− email: ${user.email}`, `− apps: ${user.apps.length}`], + resource: 'users', + marks: { [user.id]: 'deleted' }, + commit: () => setUsers((prev) => prev.filter((u) => u.id !== user.id)), + }) + setDeleteOpen(false) + void navigate({ to: '/design-preview/users' }) + }} + /> + + ) +} diff --git a/apps/web/src/design-preview/pages/users-page.tsx b/apps/web/src/design-preview/pages/users-page.tsx index 35e8507..b4be228 100644 --- a/apps/web/src/design-preview/pages/users-page.tsx +++ b/apps/web/src/design-preview/pages/users-page.tsx @@ -1,5 +1,6 @@ import { UsersIcon, ShieldCheckIcon, FingerprintIcon, UserXIcon } from 'lucide-react' import { useCallback, useEffect, useRef, useState } from 'react' +import { useNavigate } from '@tanstack/react-router' import { toast } from 'sonner' import type { AdminUser, CreateUserRequest } from '@authportal/shared' import { CreateUserSheet } from '@/components/reui-kit/create-user-sheet' @@ -11,20 +12,22 @@ import { AccessSheet } from '../access-sheet' import { diffAccess, type AccessValue } from '../access-model' import { CodeBlock, KvList, PageHead, PreviewSheet, TypedConfirm } from '../bits' import { kpi } from '../kpi' -import { ALL_APP_IDS, APP_SHORT, MOCK_USERS, USER_JOURNAL, type Tone } from '../mock' +import { ALL_APP_IDS, APP_SHORT, USER_JOURNAL, type Tone } from '../mock' import { PreviewSectionGrid, TwoLineCell, type GridColumn } from '../section-grid' import { StatusBadge } from '../bits' import { UsersGrid, type UsersGridHandlers } from '../users-grid' import { usePreviewStore } from '../preview-store' +import { usePatchUsers } from '../user-actions' export function UsersPage() { - const { addChange, marksFor } = usePreviewStore() + const { addChange, marksFor, users, setUsers } = usePreviewStore() const marks = marksFor('users') - const [users, setUsers] = useState(MOCK_USERS) const usersRef = useRef(users) useEffect(() => { usersRef.current = users }, [users]) + const patch = usePatchUsers() + const navigate = useNavigate() const [createOpen, setCreateOpen] = useState(false) const [journalFor, setJournalFor] = useState(null) @@ -33,25 +36,6 @@ export function UsersPage() { const [resetFor, setResetFor] = useState(null) const [bulkDeleteIds, setBulkDeleteIds] = useState(null) - /** Меняет поля у выбранных пользователей и кладёт изменение в «Не сохранено». */ - const patch = useCallback( - (ids: string[], apply: (u: AdminUser) => AdminUser, title: string, lineFor: (before: AdminUser, after: AdminUser) => string[]) => { - const before = usersRef.current.filter((u) => ids.includes(u.id)) - const after = before.map(apply) - const lines = before.flatMap((b, i) => [` ${b.email}`, ...lineFor(b, after[i]!)]) - setUsers((prev) => prev.map((u) => after.find((a) => a.id === u.id) ?? u)) - addChange({ - kind: 'chg', - title, - lines, - resource: 'users', - marks: Object.fromEntries(before.map((b) => [b.id, 'dirty' as const])), - revert: () => setUsers((prev) => prev.map((u) => before.find((b) => b.id === u.id) ?? u)), - }) - }, - [addChange], - ) - const onDeactivate = useCallback( (user: AdminUser) => patch([user.id], (u) => ({ ...u, disabled: !u.disabled }), `${user.disabled ? 'Включён' : 'Отключён'} ${user.email}`, (b, a) => [ @@ -89,7 +73,7 @@ export function UsersPage() { setCreateOpen(false) setAccessFor(created) }, - [addChange], + [addChange, setUsers], ) const onDelete = useCallback((user: AdminUser) => setDeleteFor(user), []) @@ -103,6 +87,7 @@ export function UsersPage() { onToggleDisabled: onDeactivate, onResetPasskeys, onDelete, + onOpenProfile: (u) => void navigate({ to: '/design-preview/users/$userId', params: { userId: u.id } }), onBulkDelete: (ids) => setBulkDeleteIds(ids), onBulkDisabled: (ids, disabled) => patch(ids, (u) => ({ ...u, disabled }), `${disabled ? 'Отключено' : 'Включено'} пользователей: ${ids.length}`, () => [ @@ -169,6 +154,9 @@ export function UsersPage() { key={accessFor.id} user={accessFor} onClose={() => setAccessFor(null)} + onOpenProfile={() => { + void navigate({ to: '/design-preview/users/$userId', params: { userId: accessFor.id } }) + }} onSave={(value: AccessValue) => { const target = accessFor patch( diff --git a/apps/web/src/design-preview/preview-chrome.tsx b/apps/web/src/design-preview/preview-chrome.tsx index 50c365f..968f897 100644 --- a/apps/web/src/design-preview/preview-chrome.tsx +++ b/apps/web/src/design-preview/preview-chrome.tsx @@ -31,8 +31,10 @@ import { Avatar, AvatarFallback } from '@authportal/ui/components/avatar' import { Breadcrumb, BreadcrumbItem, + BreadcrumbLink, BreadcrumbList, BreadcrumbPage, + BreadcrumbSeparator, } from '@authportal/ui/components/breadcrumb' import { Button } from '@authportal/ui/components/button' import { @@ -472,7 +474,9 @@ function FreshnessPopover() { export function PreviewHeader() { const pathname = useRouterState({ select: (s) => s.location.pathname }) - const { setPaletteOpen } = usePreviewStore() + const { setPaletteOpen, users } = usePreviewStore() + const profileId = /^\/design-preview\/users\/([^/]+)/.exec(pathname)?.[1] + const profileUser = profileId ? users.find((u) => u.id === profileId) : undefined const current = NAV_ITEMS.find((i) => pathname === i.to || pathname.startsWith(`${i.to}/`)) return ( @@ -481,9 +485,21 @@ export function PreviewHeader() { - - {current?.label ?? 'Макет'} - + {profileUser ? ( + <> + + }>Пользователи + + + + {profileUser.email} + + + ) : ( + + {current?.label ?? 'Макет'} + + )}
diff --git a/apps/web/src/design-preview/preview-provider.tsx b/apps/web/src/design-preview/preview-provider.tsx index e31d472..5ff02f3 100644 --- a/apps/web/src/design-preview/preview-provider.tsx +++ b/apps/web/src/design-preview/preview-provider.tsx @@ -1,5 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react' import { toast } from 'sonner' +import type { AdminUser } from '@authportal/shared' +import { MOCK_USERS } from './mock' import { PreviewStoreContext, type Contrast, @@ -16,6 +18,7 @@ import { export function PreviewProvider({ children }: { children: ReactNode }) { const [pending, setPending] = useState([]) const [paletteOpen, setPaletteOpen] = useState(false) + const [users, setUsers] = useState(MOCK_USERS) const [density, setDensity] = useState('comfortable') const [contrast, setContrast] = useState('standard') const [pins, setPins] = useState(() => { @@ -109,6 +112,8 @@ export function PreviewProvider({ children }: { children: ReactNode }) { applyAll, paletteOpen, setPaletteOpen, + users, + setUsers, pins, togglePin, density, @@ -116,7 +121,7 @@ export function PreviewProvider({ children }: { children: ReactNode }) { contrast, setContrast, }), - [pending, marksFor, addChange, discardAll, applyAll, paletteOpen, pins, togglePin, density, contrast], + [pending, marksFor, addChange, discardAll, applyAll, paletteOpen, users, pins, togglePin, density, contrast], ) return ( diff --git a/apps/web/src/design-preview/preview-store.ts b/apps/web/src/design-preview/preview-store.ts index 8d2063d..a2ca1db 100644 --- a/apps/web/src/design-preview/preview-store.ts +++ b/apps/web/src/design-preview/preview-store.ts @@ -1,4 +1,5 @@ -import { createContext, useContext } from 'react' +import { createContext, useContext, type Dispatch, type SetStateAction } from 'react' +import type { AdminUser } from '@authportal/shared' export type ChangeKind = 'add' | 'chg' | 'del' @@ -32,6 +33,9 @@ export type PreviewStore = { applyAll: () => void paletteOpen: boolean setPaletteOpen: (open: boolean) => void + /** Каталог пользователей: общий для списка и профиля. */ + users: AdminUser[] + setUsers: Dispatch> pins: string[] togglePin: (id: string) => void density: Density diff --git a/apps/web/src/design-preview/user-actions.ts b/apps/web/src/design-preview/user-actions.ts new file mode 100644 index 0000000..ddc1b0f --- /dev/null +++ b/apps/web/src/design-preview/user-actions.ts @@ -0,0 +1,35 @@ +import { useCallback, useEffect, useRef } from 'react' +import type { AdminUser } from '@authportal/shared' +import { usePreviewStore } from './preview-store' + +/** Меняет поля у выбранных пользователей и кладёт правку в «Не сохранено» (с откатом). */ +export function usePatchUsers() { + const { users, setUsers, addChange } = usePreviewStore() + const usersRef = useRef(users) + useEffect(() => { + usersRef.current = users + }, [users]) + + return useCallback( + ( + ids: string[], + apply: (u: AdminUser) => AdminUser, + title: string, + lineFor: (before: AdminUser, after: AdminUser) => string[], + ) => { + const before = usersRef.current.filter((u) => ids.includes(u.id)) + const after = before.map(apply) + const lines = before.flatMap((b, i) => [` ${b.email}`, ...lineFor(b, after[i]!)]) + setUsers((prev) => prev.map((u) => after.find((a) => a.id === u.id) ?? u)) + addChange({ + kind: 'chg', + title, + lines, + resource: 'users', + marks: Object.fromEntries(before.map((b) => [b.id, 'dirty' as const])), + revert: () => setUsers((prev) => prev.map((u) => before.find((b) => b.id === u.id) ?? u)), + }) + }, + [addChange, setUsers], + ) +} diff --git a/apps/web/src/design-preview/users-grid.tsx b/apps/web/src/design-preview/users-grid.tsx index abcc677..12c8125 100644 --- a/apps/web/src/design-preview/users-grid.tsx +++ b/apps/web/src/design-preview/users-grid.tsx @@ -6,6 +6,7 @@ import { ScrollTextIcon, ShieldCheckIcon, Trash2Icon, + UserRoundIcon, FingerprintIcon, } from 'lucide-react' import type { AdminUser } from '@authportal/shared' @@ -42,6 +43,7 @@ function passkeyLabel(n: number) { export type UsersGridHandlers = { onOpenAccess: (u: AdminUser) => void + onOpenProfile: (u: AdminUser) => void onOpenJournal: (u: AdminUser) => void onToggleDisabled: (u: AdminUser) => void onResetPasskeys: (u: AdminUser) => void @@ -89,6 +91,10 @@ export function UsersGrid({ users, handlers, marks }: { users: AdminUser[]; hand Права доступа + handlers.onOpenProfile(u)}> + + Профиль + handlers.onOpenJournal(u)}> Журнал diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index 6d68fcd..36d1e78 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -30,8 +30,9 @@ import { Route as DesignPreviewShellAppsRouteImport } from './routes/design-prev import { Route as DesignPreviewShellAuditRouteImport } from './routes/design-preview._shell.audit' import { Route as DesignPreviewShellLoginsRouteImport } from './routes/design-preview._shell.logins' import { Route as DesignPreviewShellOidcRouteImport } from './routes/design-preview._shell.oidc' -import { Route as DesignPreviewShellUsersRouteImport } from './routes/design-preview._shell.users' import { Route as AuthAdminUsersUserIdRouteImport } from './routes/_auth.admin.users.$userId' +import { Route as DesignPreviewShellUsersIndexRouteImport } from './routes/design-preview._shell.users.index' +import { Route as DesignPreviewShellUsersUserIdRouteImport } from './routes/design-preview._shell.users.$userId' const IndexRoute = IndexRouteImport.update({ id: '/', @@ -139,16 +140,23 @@ const DesignPreviewShellOidcRoute = DesignPreviewShellOidcRouteImport.update({ path: '/oidc', getParentRoute: () => DesignPreviewShellRoute, } as any) -const DesignPreviewShellUsersRoute = DesignPreviewShellUsersRouteImport.update({ - id: '/users', - path: '/users', - getParentRoute: () => DesignPreviewShellRoute, -} as any) const AuthAdminUsersUserIdRoute = AuthAdminUsersUserIdRouteImport.update({ id: '/users/$userId', path: '/users/$userId', getParentRoute: () => AuthAdminRoute, } as any) +const DesignPreviewShellUsersIndexRoute = + DesignPreviewShellUsersIndexRouteImport.update({ + id: '/users/', + path: '/users/', + getParentRoute: () => DesignPreviewShellRoute, + } as any) +const DesignPreviewShellUsersUserIdRoute = + DesignPreviewShellUsersUserIdRouteImport.update({ + id: '/users/$userId', + path: '/users/$userId', + getParentRoute: () => DesignPreviewShellRoute, + } as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute @@ -169,9 +177,10 @@ export interface FileRoutesByFullPath { '/design-preview/audit': typeof DesignPreviewShellAuditRoute '/design-preview/logins': typeof DesignPreviewShellLoginsRoute '/design-preview/oidc': typeof DesignPreviewShellOidcRoute - '/design-preview/users': typeof DesignPreviewShellUsersRoute '/admin/': typeof AuthAdminIndexRoute '/admin/users/$userId': typeof AuthAdminUsersUserIdRoute + '/design-preview/users/$userId': typeof DesignPreviewShellUsersUserIdRoute + '/design-preview/users/': typeof DesignPreviewShellUsersIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute @@ -190,9 +199,10 @@ export interface FileRoutesByTo { '/design-preview/audit': typeof DesignPreviewShellAuditRoute '/design-preview/logins': typeof DesignPreviewShellLoginsRoute '/design-preview/oidc': typeof DesignPreviewShellOidcRoute - '/design-preview/users': typeof DesignPreviewShellUsersRoute '/admin': typeof AuthAdminIndexRoute '/admin/users/$userId': typeof AuthAdminUsersUserIdRoute + '/design-preview/users/$userId': typeof DesignPreviewShellUsersUserIdRoute + '/design-preview/users': typeof DesignPreviewShellUsersIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -216,9 +226,10 @@ export interface FileRoutesById { '/design-preview/_shell/audit': typeof DesignPreviewShellAuditRoute '/design-preview/_shell/logins': typeof DesignPreviewShellLoginsRoute '/design-preview/_shell/oidc': typeof DesignPreviewShellOidcRoute - '/design-preview/_shell/users': typeof DesignPreviewShellUsersRoute '/_auth/admin/': typeof AuthAdminIndexRoute '/_auth/admin/users/$userId': typeof AuthAdminUsersUserIdRoute + '/design-preview/_shell/users/$userId': typeof DesignPreviewShellUsersUserIdRoute + '/design-preview/_shell/users/': typeof DesignPreviewShellUsersIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -241,9 +252,10 @@ export interface FileRouteTypes { | '/design-preview/audit' | '/design-preview/logins' | '/design-preview/oidc' - | '/design-preview/users' | '/admin/' | '/admin/users/$userId' + | '/design-preview/users/$userId' + | '/design-preview/users/' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -262,9 +274,10 @@ export interface FileRouteTypes { | '/design-preview/audit' | '/design-preview/logins' | '/design-preview/oidc' - | '/design-preview/users' | '/admin' | '/admin/users/$userId' + | '/design-preview/users/$userId' + | '/design-preview/users' id: | '__root__' | '/' @@ -287,9 +300,10 @@ export interface FileRouteTypes { | '/design-preview/_shell/audit' | '/design-preview/_shell/logins' | '/design-preview/_shell/oidc' - | '/design-preview/_shell/users' | '/_auth/admin/' | '/_auth/admin/users/$userId' + | '/design-preview/_shell/users/$userId' + | '/design-preview/_shell/users/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -448,13 +462,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DesignPreviewShellOidcRouteImport parentRoute: typeof DesignPreviewShellRoute } - '/design-preview/_shell/users': { - id: '/design-preview/_shell/users' - path: '/users' - fullPath: '/design-preview/users' - preLoaderRoute: typeof DesignPreviewShellUsersRouteImport - parentRoute: typeof DesignPreviewShellRoute - } '/_auth/admin/users/$userId': { id: '/_auth/admin/users/$userId' path: '/users/$userId' @@ -462,6 +469,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthAdminUsersUserIdRouteImport parentRoute: typeof AuthAdminRoute } + '/design-preview/_shell/users/': { + id: '/design-preview/_shell/users/' + path: '/users' + fullPath: '/design-preview/users/' + preLoaderRoute: typeof DesignPreviewShellUsersIndexRouteImport + parentRoute: typeof DesignPreviewShellRoute + } + '/design-preview/_shell/users/$userId': { + id: '/design-preview/_shell/users/$userId' + path: '/users/$userId' + fullPath: '/design-preview/users/$userId' + preLoaderRoute: typeof DesignPreviewShellUsersUserIdRouteImport + parentRoute: typeof DesignPreviewShellRoute + } } } @@ -508,7 +529,8 @@ interface DesignPreviewShellRouteChildren { DesignPreviewShellAuditRoute: typeof DesignPreviewShellAuditRoute DesignPreviewShellLoginsRoute: typeof DesignPreviewShellLoginsRoute DesignPreviewShellOidcRoute: typeof DesignPreviewShellOidcRoute - DesignPreviewShellUsersRoute: typeof DesignPreviewShellUsersRoute + DesignPreviewShellUsersUserIdRoute: typeof DesignPreviewShellUsersUserIdRoute + DesignPreviewShellUsersIndexRoute: typeof DesignPreviewShellUsersIndexRoute } const DesignPreviewShellRouteChildren: DesignPreviewShellRouteChildren = { @@ -518,7 +540,8 @@ const DesignPreviewShellRouteChildren: DesignPreviewShellRouteChildren = { DesignPreviewShellAuditRoute: DesignPreviewShellAuditRoute, DesignPreviewShellLoginsRoute: DesignPreviewShellLoginsRoute, DesignPreviewShellOidcRoute: DesignPreviewShellOidcRoute, - DesignPreviewShellUsersRoute: DesignPreviewShellUsersRoute, + DesignPreviewShellUsersUserIdRoute: DesignPreviewShellUsersUserIdRoute, + DesignPreviewShellUsersIndexRoute: DesignPreviewShellUsersIndexRoute, } const DesignPreviewShellRouteWithChildren = diff --git a/apps/web/src/routes/design-preview._shell.users.$userId.tsx b/apps/web/src/routes/design-preview._shell.users.$userId.tsx new file mode 100644 index 0000000..a20380f --- /dev/null +++ b/apps/web/src/routes/design-preview._shell.users.$userId.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/react-router' +import { UserProfilePage } from '@/design-preview/pages/user-profile-page' + +export const Route = createFileRoute('/design-preview/_shell/users/$userId')({ + component: UserProfilePage, +}) diff --git a/apps/web/src/routes/design-preview._shell.users.tsx b/apps/web/src/routes/design-preview._shell.users.index.tsx similarity index 96% rename from apps/web/src/routes/design-preview._shell.users.tsx rename to apps/web/src/routes/design-preview._shell.users.index.tsx index 66bbf53..70838c9 100644 --- a/apps/web/src/routes/design-preview._shell.users.tsx +++ b/apps/web/src/routes/design-preview._shell.users.index.tsx @@ -1,6 +1,6 @@ import { createFileRoute } from '@tanstack/react-router' import { UsersPage } from '@/design-preview/pages/users-page' -export const Route = createFileRoute('/design-preview/_shell/users')({ +export const Route = createFileRoute('/design-preview/_shell/users/')({ component: UsersPage, })