Skip to main content

Frontend Architecture

TalentG’s frontend is built with modern React patterns using Next.js 16 App Router, providing a scalable and maintainable codebase.

Technology Stack

Core Technologies

  • Next.js 16: React framework with App Router
  • React 19: UI library with concurrent features
  • TypeScript: Type-safe development
  • TailwindCSS: Utility-first CSS framework
  • RizzUI: Component library for consistent design

State Management

  • Jotai: Atomic state management
  • React Hook Form: Form state management
  • Zod: Schema validation
  • TanStack Query: Server state management

Development Tools

  • ESLint: Code linting
  • Prettier: Code formatting
  • Husky: Git hooks
  • TypeScript: Type checking

Loading States (Skeleton Standard)

  • Use the single Shadcn-based skeleton primitive at apps/isomorphic/src/components/ui/skeleton.tsx; do not import skeletons from other libraries.
  • For page-level fallbacks, prefer PageSkeleton variants in apps/isomorphic/src/components/ui/page-skeleton.tsx instead of ad-hoc layouts.
  • Keep suspense fallbacks lightweight: reserve skeletons for initial data fetch and avoid stacking spinners plus skeletons together.

Standardized Tables (Loading, Caching, Errors)

  • The source of truth is apps/isomorphic/src/components/ui/table-standardized.tsx; loading defaults to the shared table skeleton (spinner only when loadingStyle="spinner" is set).
  • Server tables should pass manualPagination with page/total/onPageChange/onPageSizeChange, plus cacheKey/cacheTtlMs/refreshOnMount when using useServerTableData.
  • Surface failures via errorMessage + onRetry to use the unified error state; avoid bespoke error divs.
  • Search is debounced by default (250 ms) via searchValue/onSearchChange; external filters can render alongside the built-in search without re-styling.

Project Structure

src/
├── app/                    # Next.js App Router
│   ├── (auth)/            # Auth route group
│   ├── (dashboard)/       # Dashboard route group
│   ├── api/               # API routes
│   ├── globals.css        # Global styles
│   └── layout.tsx         # Root layout
├── components/            # Reusable components
│   ├── ui/               # Base UI components
│   ├── forms/            # Form components
│   ├── layout/           # Layout components
│   └── features/         # Feature-specific components
├── lib/                  # Utility functions
│   ├── auth.ts           # Authentication utilities
│   ├── db.ts             # Database utilities
│   ├── utils.ts          # General utilities
│   └── validations.ts    # Zod schemas
├── hooks/                # Custom React hooks
├── types/                # TypeScript type definitions
└── constants/            # Application constants

Component Architecture

Component Hierarchy

Component Patterns

1. Compound Components

// components/ui/card.tsx
interface CardProps {
  children: React.ReactNode
  className?: string
}

interface CardHeaderProps {
  children: React.ReactNode
}

interface CardContentProps {
  children: React.ReactNode
}

const Card = ({ children, className }: CardProps) => {
  return (
    <div className={`rounded-lg border bg-card ${className}`}>
      {children}
    </div>
  )
}

const CardHeader = ({ children }: CardHeaderProps) => {
  return (
    <div className="flex flex-col space-y-1.5 p-6">
      {children}
    </div>
  )
}

const CardContent = ({ children }: CardContentProps) => {
  return (
    <div className="p-6 pt-0">
      {children}
    </div>
  )
}

export { Card, CardHeader, CardContent }

2. Custom Hooks

// hooks/use-auth.ts
import { useEffect, useState } from 'react'
import { User } from '@supabase/supabase-js'
import { supabase } from '@/lib/supabase'

export function useAuth() {
  const [user, setUser] = useState<User | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (event, session) => {
        setUser(session?.user ?? null)
        setLoading(false)
      }
    )

    return () => subscription.unsubscribe()
  }, [])

  return { user, loading }
}

3. Form Components

// components/forms/contact-form.tsx
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'

const contactSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  email: z.string().email('Invalid email address'),
  message: z.string().min(10, 'Message must be at least 10 characters'),
})

type ContactFormData = z.infer<typeof contactSchema>

export function ContactForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<ContactFormData>({
    resolver: zodResolver(contactSchema),
  })

  const onSubmit = async (data: ContactFormData) => {
    // Handle form submission
    console.log(data)
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
      <div>
        <label htmlFor="name">Name</label>
        <input
          {...register('name')}
          className="w-full rounded border p-2"
        />
        {errors.name && (
          <p className="text-red-500">{errors.name.message}</p>
        )}
      </div>
      
      <button
        type="submit"
        disabled={isSubmitting}
        className="rounded bg-blue-500 px-4 py-2 text-white disabled:opacity-50"
      >
        {isSubmitting ? 'Submitting...' : 'Submit'}
      </button>
    </form>
  )
}

State Management

Jotai Atoms

// atoms/auth.ts
import { atom } from 'jotai'
import { User } from '@supabase/supabase-js'

export const userAtom = atom<User | null>(null)
export const isAuthenticatedAtom = atom(
  (get) => get(userAtom) !== null
)

// atoms/ui.ts
export const sidebarOpenAtom = atom(false)
export const themeAtom = atom<'light' | 'dark'>('light')

Server State with TanStack Query

// hooks/use-leads.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { supabase } from '@/lib/supabase'

export function useLeads() {
  return useQuery({
    queryKey: ['leads'],
    queryFn: async () => {
      const { data, error } = await supabase
        .from('leads')
        .select('*')
        .order('created_at', { ascending: false })
      
      if (error) throw error
      return data
    },
  })
}

export function useCreateLead() {
  const queryClient = useQueryClient()
  
  return useMutation({
    mutationFn: async (lead: CreateLeadData) => {
      const { data, error } = await supabase
        .from('leads')
        .insert(lead)
        .select()
        .single()
      
      if (error) throw error
      return data
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['leads'] })
    },
  })
}

Styling Guidelines

TailwindCSS Configuration

// tailwind.config.js
module.exports = {
  content: [
    './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
    './src/components/**/*.{js,ts,jsx,tsx,mdx}',
    './src/app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {
      colors: {
        primary: {
          50: '#f0fdf4',
          500: '#16a34a',
          600: '#15803d',
          700: '#166534',
        },
      },
      fontFamily: {
        sans: ['Inter', 'sans-serif'],
      },
    },
  },
  plugins: [
    require('@tailwindcss/forms'),
    require('@tailwindcss/typography'),
  ],
}

Component Styling

// components/ui/button.tsx
import { cn } from '@/lib/utils'

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'outline'
  size?: 'sm' | 'md' | 'lg'
}

const buttonVariants = {
  primary: 'bg-primary-600 text-white hover:bg-primary-700',
  secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
  outline: 'border border-gray-300 text-gray-700 hover:bg-gray-50',
}

const buttonSizes = {
  sm: 'px-3 py-1.5 text-sm',
  md: 'px-4 py-2 text-base',
  lg: 'px-6 py-3 text-lg',
}

export function Button({ 
  className, 
  variant = 'primary', 
  size = 'md', 
  ...props 
}: ButtonProps) {
  return (
    <button
      className={cn(
        'rounded-md font-medium transition-colors',
        buttonVariants[variant],
        buttonSizes[size],
        className
      )}
      {...props}
    />
  )
}

Performance Optimization

Code Splitting

// Dynamic imports for route-based code splitting
import dynamic from 'next/dynamic'

const Dashboard = dynamic(() => import('@/components/dashboard'), {
  loading: () => <div>Loading dashboard...</div>,
})

const AdminPanel = dynamic(() => import('@/components/admin-panel'), {
  loading: () => <div>Loading admin panel...</div>,
})

Image Optimization

// Using Next.js Image component
import Image from 'next/image'

export function ProfileImage({ src, alt }: { src: string; alt: string }) {
  return (
    <Image
      src={src}
      alt={alt}
      width={100}
      height={100}
      className="rounded-full"
      priority // For above-the-fold images
    />
  )
}

Memoization

// Memoizing expensive calculations
import { useMemo } from 'react'

export function ExpensiveComponent({ data }: { data: any[] }) {
  const processedData = useMemo(() => {
    return data.map(item => ({
      ...item,
      processed: expensiveCalculation(item)
    }))
  }, [data])

  return <div>{/* Render processed data */}</div>
}

Testing Strategy

Component Testing

// __tests__/components/button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from '@/components/ui/button'

describe('Button', () => {
  it('renders with correct text', () => {
    render(<Button>Click me</Button>)
    expect(screen.getByText('Click me')).toBeInTheDocument()
  })

  it('calls onClick when clicked', () => {
    const handleClick = jest.fn()
    render(<Button onClick={handleClick}>Click me</Button>)
    
    fireEvent.click(screen.getByText('Click me'))
    expect(handleClick).toHaveBeenCalledTimes(1)
  })
})

Integration Testing

// __tests__/pages/dashboard.test.tsx
import { render, screen } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import Dashboard from '@/app/dashboard/page'

const createTestQueryClient = () => new QueryClient({
  defaultOptions: {
    queries: { retry: false },
    mutations: { retry: false },
  },
})

describe('Dashboard', () => {
  it('renders dashboard content', () => {
    const queryClient = createTestQueryClient()
    
    render(
      <QueryClientProvider client={queryClient}>
        <Dashboard />
      </QueryClientProvider>
    )
    
    expect(screen.getByText('Dashboard')).toBeInTheDocument()
  })
})

Development Workflow

Local Development

# Install dependencies
pnpm install

# Start development server
pnpm iso:dev

# Run type checking
pnpm type-check

# Run linting
pnpm lint

# Run tests
pnpm test

Code Quality

  • ESLint: Enforces coding standards
  • Prettier: Ensures consistent formatting
  • Husky: Pre-commit hooks for quality checks
  • TypeScript: Compile-time type checking

Git Workflow

  1. Create feature branch from main
  2. Make changes with proper commits
  3. Run quality checks before push
  4. Create pull request for review
  5. Merge after approval and CI passes

Admin Interview Prep Hub

The Admin Interview Prep Hub is a specialized dashboard section that provides admins with tools and insights for managing interview preparation activities. It was adapted from the /interview-questions page UI to create a professional, TalentG-branded experience.

Architecture

Location: apps/isomorphic/src/components/dashboard/admin/interview-prep/ Components:
  • AdminInterviewPrepHub: Main container component with grid layout
  • InterviewHeroBanner: Promotional banner for interview prep programs
  • RecentlyAccessedQuestionSets: Grid of recently used interview question sets
  • InterviewCategoriesGrid: Category browser for different interview types
  • FeaturedInterviewTracks: Highlighted interview preparation tracks
  • InterviewEngagementStats: Analytics dashboard for interview activity
  • TopInterviewCoaches: List of top-performing interview mentors
  • SavedInterviewFlows: User’s saved interview practice templates
  • UpcomingInterviewEvents: Calendar of scheduled interview sessions
Data Source: apps/isomorphic/src/data/interview-prep-data.tsx - Contains mock data for interview-related content

Integration

The hub is conditionally rendered in DashboardContent for users with the admin role:
{isAdmin() && (
  <Suspense fallback={<div className="animate-pulse bg-gray-200 min-h-[600px] rounded-lg"></div>}>
    <AdminInterviewPrepHub />
  </Suspense>
)}

Customization

To customize the mock data and content:
  1. Update Mock Data: Modify interview-prep-data.tsx with real data structures
  2. Connect to Database: Replace static data imports with Supabase queries
  3. Add Real Functionality: Implement actual interview session management, coach assignments, etc.
  4. Update Links: Change route links from placeholder URLs to actual TalentG admin routes

Future Enhancements

  • Real-time Data: Connect to live interview analytics and session data
  • Interactive Sessions: Implement actual interview practice functionality
  • Coach Management: Add admin tools for managing interview coaches
  • Session Scheduling: Integrate with calendar systems for interview events
  • Performance Tracking: Add detailed analytics for interview preparation metrics