Skip to content

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.ts

  • Modify: frontend/apps/admin/src/contexts/AuthContext.tsx

  • Modify: frontend/apps/admin/src/components/Users/UserList.tsx

  • Modify: frontend/apps/admin/src/components/Users/UserDetail.tsx

  • Modify: frontend/apps/admin/src/components/Users/Modals/UserActionDialog.tsx

  • Modify: frontend/apps/admin/src/components/Users/Modals/UserDeleteDialog.tsx

  • Modify: frontend/apps/admin/src/routes/index.tsx

  • [ ] Step 1: Create shared types file

typescript
// 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 User interface (lines ~10-16)

  • Import AuthUser from @/types/user

  • Replace all User references with AuthUser

  • [ ] Step 3: Update UserList to use shared User type

In frontend/apps/admin/src/components/Users/UserList.tsx:

  • Remove the local User interface

  • Import User from @/types/user

  • Keep 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 UserDetailData interface

  • Import User from @/types/user

  • Replace UserDetailData with User

  • [ ] 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/UserList to @/types/user

  • [ ] Step 8: Verify TypeScript compiles

Run: cd frontend/apps/admin && npx tsc --noEmit Expected: No errors

  • [ ] Step 9: Commit
bash
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.tsx

  • Modify: frontend/apps/admin/src/main.tsx

  • Modify: frontend/apps/admin/src/components/Users/UserList.tsx

  • Modify: frontend/apps/admin/src/components/Users/UserDetail.tsx

  • Modify: frontend/apps/admin/src/components/Users/Modals/UserActionDialog.tsx

  • Modify: 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
tsx
// 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 Toaster from @/components/Toaster

  • Add <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 } from sonner

  • In 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 } from sonner

  • Replace alert() calls with toast.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 } from sonner

  • After 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 } from sonner

  • After 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
bash
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.tsx

  • Modify: frontend/apps/admin/src/components/Users/UserList.tsx

  • Modify: frontend/apps/admin/src/components/Users/UserDetail.tsx

  • Modify: frontend/apps/admin/src/routes/index.tsx

  • [ ] Step 1: Create skeleton components

tsx
// 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/skeletons

  • Replace 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/skeletons

  • Replace 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/skeletons

  • Add 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
bash
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.ts

  • Modify: frontend/apps/admin/src/api/apiClient.ts

  • [ ] Step 1: Create error helper

typescript
// 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 extractErrorMessage from @/lib/errors

  • In apiFetch, update the error throwing to use extractErrorMessage

  • [ ] Step 3: Verify TypeScript compiles

Run: cd frontend/apps/admin && npx tsc --noEmit Expected: No errors

  • [ ] Step 4: Commit
bash
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.tsx

  • Modify: 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 data

  • Option B: If no invite endpoint exists, disable the button with a tooltip "Convite indisponível"

  • Replace setTimeout mock with actual apiFetch call or disabled state

  • [ ] Step 3: Verify TypeScript compiles

Run: cd frontend/apps/admin && npx tsc --noEmit Expected: No errors

  • [ ] Step 4: Commit
bash
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, Settings

  • Add nav groups for:

    • Conteúdo: Posts, Events, Courses (grayed out / disabled: true)
    • Moderação: Reports (grayed out)
    • Sistema: Settings (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
bash
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:

  1. Run cd frontend/apps/admin && npx tsc --noEmit — should pass
  2. Run cd frontend/apps/admin && bun run dev — should start without errors
  3. Verify:
    • Toast appears on user create/edit/delete
    • Skeleton screens show during loading
    • No alert() or console.log for user feedback
    • Fast-login section only visible in dev mode
    • All User types come from @/types/user

Strum — Documentação.