Admin Foundation Fixes Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use compose:subagent (recommended) or compose:execute to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Fix the admin SPA foundation by consolidating types, adding toast notifications, replacing text loading states with skeletons, and standardizing error handling.
Architecture: Create shared types, add sonner toast provider, build skeleton components, replace all alert()/confirm() calls, and unify error handling across the admin SPA.
Tech Stack: React 19, TanStack Router, TanStack Query, shadcn/ui, sonner (toast), Tailwind CSS
Task 1: Shared User Types
Covers: Foundation — type consolidation
Files:
Create:
frontend/apps/admin/src/types/user.tsModify:
frontend/apps/admin/src/contexts/AuthContext.tsxModify:
frontend/apps/admin/src/components/Users/UserList.tsxModify:
frontend/apps/admin/src/components/Users/UserDetail.tsxModify:
frontend/apps/admin/src/components/Users/Modals/UserActionDialog.tsxModify:
frontend/apps/admin/src/components/Users/Modals/UserDeleteDialog.tsxModify:
frontend/apps/admin/src/routes/index.tsx[ ] Step 1: Create shared types file
// frontend/apps/admin/src/types/user.ts
export interface User {
id: number;
email: string;
first_name: string;
last_name: string;
get_display_name: string;
avatar_url: string | null;
cover_photo_url?: string | null;
date_joined: string;
is_active: boolean;
is_staff: boolean;
}
export interface AuthUser {
id: number;
name: string;
email: string;
avatar_url: string | null;
role: string;
}- [ ] Step 2: Update AuthContext to use AuthUser type
In frontend/apps/admin/src/contexts/AuthContext.tsx:
Remove the inline
Userinterface (lines ~10-16)Import
AuthUserfrom@/types/userReplace all
Userreferences withAuthUser[ ] Step 3: Update UserList to use shared User type
In frontend/apps/admin/src/components/Users/UserList.tsx:
Remove the local
UserinterfaceImport
Userfrom@/types/userKeep the re-export:
export type { User } from '@/types/user'for backwards compatibility[ ] Step 4: Update UserDetail to use shared User type
In frontend/apps/admin/src/components/Users/UserDetail.tsx:
Remove the local
UserDetailDatainterfaceImport
Userfrom@/types/userReplace
UserDetailDatawithUser[ ] Step 5: Update UserActionDialog imports
In frontend/apps/admin/src/components/Users/Modals/UserActionDialog.tsx:
Change import from
'../UserList'to@/types/user[ ] Step 6: Update UserDeleteDialog imports
In frontend/apps/admin/src/components/Users/Modals/UserDeleteDialog.tsx:
Change import from
'../UserList'to@/types/user[ ] Step 7: Update Dashboard imports
In frontend/apps/admin/src/routes/index.tsx:
Change import from
@/components/Users/UserListto@/types/user[ ] Step 8: Verify TypeScript compiles
Run: cd frontend/apps/admin && npx tsc --noEmit Expected: No errors
- [ ] Step 9: Commit
git add frontend/apps/admin/src/types/user.ts frontend/apps/admin/src/contexts/AuthContext.tsx frontend/apps/admin/src/components/Users/
git commit -m "refactor(admin): consolidate User types into shared types file"Task 2: Toast System
Covers: Foundation — notification feedback
Files:
Create:
frontend/apps/admin/src/components/Toaster.tsxModify:
frontend/apps/admin/src/main.tsxModify:
frontend/apps/admin/src/components/Users/UserList.tsxModify:
frontend/apps/admin/src/components/Users/UserDetail.tsxModify:
frontend/apps/admin/src/components/Users/Modals/UserActionDialog.tsxModify:
frontend/apps/admin/src/components/Users/Modals/UserDeleteDialog.tsx[ ] Step 1: Install sonner
Run: cd frontend/apps/admin && bun add sonner
- [ ] Step 2: Create Toaster component
// frontend/apps/admin/src/components/Toaster.tsx
import { Toaster as SonnerToaster } from 'sonner';
export function Toaster() {
return (
<SonnerToaster
position="bottom-right"
toastOptions={{
className: 'text-sm',
}}
/>
);
}- [ ] Step 3: Add Toaster to root layout
In frontend/apps/admin/src/main.tsx (or the root route component):
Import
Toasterfrom@/components/ToasterAdd
<Toaster />as sibling to the app content[ ] Step 4: Add toast to UserList bulk delete
In frontend/apps/admin/src/components/Users/UserList.tsx:
Import
{ toast }fromsonnerIn
handleBulkDelete, after the delete loop:toast.success(\${deletedCount} usuário(s) excluído(s)`)`- On error:
toast.error('Erro ao excluir usuários')
[ ] Step 5: Add toast to UserDetail actions
In frontend/apps/admin/src/components/Users/UserDetail.tsx:
Import
{ toast }fromsonnerReplace
alert()calls withtoast.success()/toast.error()Replace
confirm()with a proper dialog or keep confirm but add toast on success[ ] Step 6: Add toast to UserActionDialog
In frontend/apps/admin/src/components/Users/Modals/UserActionDialog.tsx:
Import
{ toast }fromsonnerAfter successful create:
toast.success('Usuário criado com sucesso')After successful update:
toast.success('Usuário atualizado com sucesso')On error:
toast.error(message)[ ] Step 7: Add toast to UserDeleteDialog
In frontend/apps/admin/src/components/Users/Modals/UserDeleteDialog.tsx:
Import
{ toast }fromsonnerAfter successful delete:
toast.success('Usuário excluído')On error:
toast.error('Erro ao excluir usuário')[ ] Step 8: Verify TypeScript compiles
Run: cd frontend/apps/admin && npx tsc --noEmit Expected: No errors
- [ ] Step 9: Commit
git add frontend/apps/admin/src/components/Toaster.tsx frontend/apps/admin/src/main.tsx frontend/apps/admin/src/components/Users/
git commit -m "feat(admin): add sonner toast system for user feedback"Task 3: Skeleton Loading Components
Covers: Foundation — loading states
Files:
Create:
frontend/apps/admin/src/components/skeletons.tsxModify:
frontend/apps/admin/src/components/Users/UserList.tsxModify:
frontend/apps/admin/src/components/Users/UserDetail.tsxModify:
frontend/apps/admin/src/routes/index.tsx[ ] Step 1: Create skeleton components
// frontend/apps/admin/src/components/skeletons.tsx
import { Card, CardContent, CardHeader } from '@repo/ui';
import { Skeleton } from '@repo/ui';
export function UserListSkeleton() {
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-8 w-32" />
</div>
<Card>
<CardContent className="p-0">
<div className="space-y-1 p-4">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="flex items-center gap-4 py-3">
<Skeleton className="h-4 w-4" />
<Skeleton className="h-9 w-9 rounded-full" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-48" />
</div>
<Skeleton className="h-6 w-16" />
<Skeleton className="h-8 w-8" />
</div>
))}
</div>
</CardContent>
</Card>
</div>
);
}
export function UserDetailSkeleton() {
return (
<div className="space-y-6">
<Skeleton className="h-48 w-full rounded-lg" />
<div className="flex items-center gap-6 -mt-16 px-6">
<Skeleton className="h-24 w-24 rounded-full border-4 border-background" />
<div className="space-y-2 pt-12">
<Skeleton className="h-6 w-48" />
<Skeleton className="h-4 w-32" />
</div>
</div>
<div className="grid gap-6 px-6 md:grid-cols-3">
{Array.from({ length: 3 }).map((_, i) => (
<Card key={i}>
<CardHeader>
<Skeleton className="h-4 w-24" />
</CardHeader>
<CardContent className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</CardContent>
</Card>
))}
</div>
</div>
);
}
export function DashboardSkeleton() {
return (
<div className="space-y-6">
<Skeleton className="h-8 w-48" />
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<Card key={i}>
<CardHeader className="space-y-0 pb-2">
<Skeleton className="h-4 w-24" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-16 mb-2" />
<Skeleton className="h-3 w-32" />
</CardContent>
</Card>
))}
</div>
<div className="grid gap-4 lg:grid-cols-7">
<Card className="lg:col-span-4">
<CardHeader>
<Skeleton className="h-5 w-40" />
</CardHeader>
<CardContent>
<Skeleton className="h-[320px] w-full" />
</CardContent>
</Card>
<Card className="lg:col-span-3">
<CardHeader>
<Skeleton className="h-5 w-36" />
</CardHeader>
<CardContent className="space-y-4">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="flex items-center gap-4">
<Skeleton className="h-9 w-9 rounded-full" />
<div className="flex-1 space-y-1">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-3 w-32" />
</div>
</div>
))}
</CardContent>
</Card>
</div>
</div>
);
}- [ ] Step 2: Replace loading state in UserList
In frontend/apps/admin/src/components/Users/UserList.tsx:
Import
{ UserListSkeleton }from@/components/skeletonsReplace the loading text block with:
if (isLoading) return <UserListSkeleton />;[ ] Step 3: Replace loading state in UserDetail
In frontend/apps/admin/src/components/Users/UserDetail.tsx:
Import
{ UserDetailSkeleton }from@/components/skeletonsReplace the loading text block with:
if (isLoading) return <UserDetailSkeleton />;[ ] Step 4: Replace loading state in Dashboard
In frontend/apps/admin/src/routes/index.tsx:
Import
{ DashboardSkeleton }from@/components/skeletonsAdd loading check before the main content:
if (isLoading) return <DashboardSkeleton />;[ ] Step 5: Verify TypeScript compiles
Run: cd frontend/apps/admin && npx tsc --noEmit Expected: No errors
- [ ] Step 6: Commit
git add frontend/apps/admin/src/components/skeletons.tsx frontend/apps/admin/src/components/Users/UserList.tsx frontend/apps/admin/src/components/Users/UserDetail.tsx frontend/apps/admin/src/routes/index.tsx
git commit -m "feat(admin): add skeleton loading states for all views"Task 4: Error Handling Helper
Covers: Foundation — standardized error handling
Files:
Create:
frontend/apps/admin/src/lib/errors.tsModify:
frontend/apps/admin/src/api/apiClient.ts[ ] Step 1: Create error helper
// frontend/apps/admin/src/lib/errors.ts
export interface ApiError {
detail?: string;
message?: string;
[key: string]: unknown;
}
export function extractErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
if (typeof error === 'object' && error !== null) {
const apiError = error as ApiError;
return apiError.detail || apiError.message || 'Erro desconhecido';
}
return 'Erro desconhecido';
}
export function isApiError(error: unknown): error is Error {
return error instanceof Error;
}- [ ] Step 2: Update apiClient error handling
In frontend/apps/admin/src/api/apiClient.ts:
Import
extractErrorMessagefrom@/lib/errorsIn
apiFetch, update the error throwing to useextractErrorMessage[ ] Step 3: Verify TypeScript compiles
Run: cd frontend/apps/admin && npx tsc --noEmit Expected: No errors
- [ ] Step 4: Commit
git add frontend/apps/admin/src/lib/errors.ts frontend/apps/admin/src/api/apiClient.ts
git commit -m "refactor(admin): add centralized error handling helper"Task 5: Cleanup Mocks and Security
Covers: Foundation — remove mocks and security fixes
Files:
Modify:
frontend/apps/admin/src/routes/login.tsxModify:
frontend/apps/admin/src/components/Users/Modals/UserInviteDialog.tsx[ ] Step 1: Hide fast-login behind DEV flag
In frontend/apps/admin/src/routes/login.tsx:
Wrap the fast-login section with
{import.meta.env.DEV && (...)}Keep the section functional but invisible in production
[ ] Step 2: Fix UserInviteDialog to use real API (or disable)
In frontend/apps/admin/src/components/Users/Modals/UserInviteDialog.tsx:
Option A: Connect to
POST /api/v1/users/with the invite dataOption B: If no invite endpoint exists, disable the button with a tooltip "Convite indisponível"
Replace
setTimeoutmock with actualapiFetchcall or disabled state[ ] Step 3: Verify TypeScript compiles
Run: cd frontend/apps/admin && npx tsc --noEmit Expected: No errors
- [ ] Step 4: Commit
git add frontend/apps/admin/src/routes/login.tsx frontend/apps/admin/src/components/Users/Modals/UserInviteDialog.tsx
git commit -m "fix(admin): hide dev-only fast-login, remove invite mock"Task 6: Update Sidebar Navigation
Covers: Foundation — prepare for future features
Files:
Modify:
frontend/apps/admin/src/components/layout/data/sidebar-data.ts[ ] Step 1: Add placeholder nav items for future features
In frontend/apps/admin/src/components/layout/data/sidebar-data.ts:
Import additional icons:
MessageSquare,Calendar,BookOpen,Flag,SettingsAdd nav groups for:
- Conteúdo: Posts, Events, Courses (grayed out /
disabled: true) - Moderação: Reports (grayed out)
- Sistema: Settings (grayed out)
- Conteúdo: Posts, Events, Courses (grayed out /
Keep them visually present but non-functional until implemented
[ ] Step 2: Verify TypeScript compiles
Run: cd frontend/apps/admin && npx tsc --noEmit Expected: No errors
- [ ] Step 3: Commit
git add frontend/apps/admin/src/components/layout/data/sidebar-data.ts
git commit -m "feat(admin): add placeholder sidebar items for upcoming features"Verification
After all tasks are complete:
- Run
cd frontend/apps/admin && npx tsc --noEmit— should pass - Run
cd frontend/apps/admin && bun run dev— should start without errors - Verify:
- Toast appears on user create/edit/delete
- Skeleton screens show during loading
- No
alert()orconsole.logfor user feedback - Fast-login section only visible in dev mode
- All User types come from
@/types/user