Skip to main content

Frontend Architecture

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

Technology Stack

Core Technologies

  • Next.js 14: React framework with App Router
  • React 18: 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

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