BISO Sites
Web App

Components

Guide to the component architecture, patterns, and organization in the BISO Sites web app.

Components

The Web App uses a well-organized component architecture that separates concerns and promotes reusability. Components are organized by feature and follow consistent patterns for both Server and Client Components.

Component Organization

Components are organized by feature/domain in the src/components/ directory:

📁components/
📁layout/ - Header, footer, navigation
📁home/ - Homepage components
📁events/ - Event-related components
📁shop/ - E-commerce components
📁jobs/ - Job board components
📁news/ - News/press components
📁profile/ - User profile components
📁expense/ - Expense system components
📁ai/ - AI assistant components
📁context/ - React Context providers
📄locale-switcher.tsx - Language switcher
📄privacy-controls.tsx - GDPR controls
â„šī¸
Component Naming Convention

Components with a *Client suffix (e.g., events-list-client.tsx) are Client Components that use "use client" directive. Components without this suffix are Server Components by default.

Component Patterns

Server Components (Default)

Server Components fetch data and render on the server. They cannot use React hooks or browser APIs.

// components/events/event-card.tsx
import { Link } from 'next/navigation';
import { Event } from '@/types';

interface EventCardProps {
  event: Event;
}

export function EventCard({ event }: EventCardProps) {
  return (
    <Link href={`/events/${event.$id}`} className="block">
      <div className="rounded-lg border p-4 hover:shadow-lg transition">
        <h3 className="font-bold text-lg">{event.title}</h3>
        <p className="text-muted-foreground">{event.date}</p>
        <p className="mt-2">{event.description}</p>
      </div>
    </Link>
  );
}

Client Components

Client Components handle interactivity, state, and browser APIs. Mark them with "use client":

// components/events/events-list-client.tsx
'use client';

import { useState } from 'react';
import { EventCard } from './event-card';
import { Event } from '@/types';

interface EventsListClientProps {
  initialEvents: Event[];
}

export function EventsListClient({ initialEvents }: EventsListClientProps) {
  const [filter, setFilter] = useState('all');
  const [events, setEvents] = useState(initialEvents);
  
  const filteredEvents = events.filter(event => {
    if (filter === 'all') return true;
    return event.category === filter;
  });
  
  return (
    <div>
      <div className="flex gap-2 mb-4">
        <button onClick={() => setFilter('all')}>All</button>
        <button onClick={() => setFilter('upcoming')}>Upcoming</button>
        <button onClick={() => setFilter('past')}>Past</button>
      </div>
      
      <div className="grid md:grid-cols-3 gap-6">
        {filteredEvents.map(event => (
          <EventCard key={event.$id} event={event} />
        ))}
      </div>
    </div>
  );
}

Hybrid Pattern

Combine Server and Client Components for optimal performance:

// app/(public)/events/page.tsx
import { getEvents } from 'app/actions/events';
import { EventsHero } from '@/components/events/events-hero'; // Server
import { EventsListClient } from '@/components/events/events-list-client'; // Client

export default async function EventsPage() {
  const events = await getEvents(); // Fetch on server
  
  return (
    <div>
      <EventsHero /> {/* Server Component */}
      <EventsListClient initialEvents={events} /> {/* Client Component */}
    </div>
  );
}

Layout Components

File: components/layout/nav.tsx

The main navigation component with responsive design:

// components/layout/nav.tsx
import { Link } from 'next/navigation';
import { LocaleSwitcher } from '@/components/locale-switcher';

export function Nav() {
  return (
    <nav className="border-b">
      <div className="container flex items-center justify-between h-16">
        <Link href="/" className="font-bold text-xl">
          BISO Sites
        </Link>
        
        <div className="hidden md:flex gap-6">
          <Link href="/about">About</Link>
          <Link href="/events">Events</Link>
          <Link href="/shop">Shop</Link>
          <Link href="/units">Units</Link>
          <Link href="/contact">Contact</Link>
        </div>
        
        <div className="flex items-center gap-4">
          <LocaleSwitcher />
          <Link href="/auth/login">Login</Link>
        </div>
      </div>
    </nav>
  );
}

File: components/layout/footer.tsx

Site footer with links and information.

Homepage Components

Hero Section

File: components/home/hero-section.tsx

Dynamic hero with carousel:

// components/home/hero-section.tsx
import { HeroCarousel } from './hero-carousel';
import { Button } from '@repo/ui/components/ui/button';

export function HeroSection() {
  return (
    <section className="relative h-[600px]">
      <HeroCarousel />
      <div className="absolute inset-0 flex items-center justify-center">
        <div className="text-center text-white">
          <h1 className="text-6xl font-bold mb-4">
            Welcome to BISO Sites
          </h1>
          <p className="text-xl mb-8">
            Your student organization at BI Norwegian Business School
          </p>
          <Button size="lg">Become a Member</Button>
        </div>
      </div>
    </section>
  );
}

Events Section

File: components/home/events-section.tsx

Featured events on homepage with client-side interactions.

News Section

File: components/home/news-section.tsx

Latest news articles display.

E-commerce Components

Product Card

File: components/shop/product-card.tsx

// components/shop/product-card.tsx
import { Link } from 'next/navigation';
import { Image } from '@repo/ui/components/image';
import { Product } from '@/types';

interface ProductCardProps {
  product: Product;
}

export function ProductCard({ product }: ProductCardProps) {
  return (
    <Link href={`/shop/${product.slug}`} className="group">
      <div className="rounded-lg overflow-hidden">
        <Image 
          src={product.imageUrl} 
          alt={product.name}
          className="group-hover:scale-105 transition"
        />
        <div className="p-4">
          <h3 className="font-semibold">{product.name}</h3>
          <p className="text-muted-foreground">{product.department}</p>
          <p className="text-lg font-bold mt-2">{product.price} NOK</p>
        </div>
      </div>
    </Link>
  );
}

Cart Page Client

File: components/shop/cart-page-client.tsx

Shopping cart with real-time updates and reservation management.

Product Details Client

File: components/shop/product-details-client.tsx

Product detail page with variant selection and add-to-cart functionality.

Event Components

Event Card

File: components/events/event-card.tsx

Event preview card for listings.

Event Details Client

File: components/events/event-details-client.tsx

Full event details with registration form:

// components/events/event-details-client.tsx
'use client';

import { useState } from 'react';
import { Button } from '@repo/ui/components/ui/button';
import { registerForEvent } from 'app/actions/events';

export function EventDetailsClient({ event }) {
  const [loading, setLoading] = useState(false);
  
  async function handleRegister() {
    setLoading(true);
    await registerForEvent(event.$id);
    setLoading(false);
  }
  
  return (
    <div>
      <h1>{event.title}</h1>
      <p>{event.description}</p>
      <Button onClick={handleRegister} disabled={loading}>
        {loading ? 'Registering...' : 'Register for Event'}
      </Button>
    </div>
  );
}

Job Board Components

Job Card

File: components/jobs/job-card.tsx

Job posting preview card.

Job Details Client

File: components/jobs/job-details-client.tsx

Job details with application form.

Profile Components

Profile Tabs

File: components/profile/profile-tabs.tsx

Tabbed interface for user profile sections.

Membership Status Card

File: components/profile/membership-status-card.tsx

Displays current membership status and benefits.

Expense System Components

Expense Wizard

File: components/expense/expense-wizard.tsx

Multi-step expense creation wizard:

// components/expense/expense-wizard.tsx
'use client';

import { useState } from 'react';
import { ProfileStep } from './profile-step';
import { CampusStep } from './campus-step';
import { UploadStep } from './upload-step';

export function ExpenseWizard() {
  const [step, setStep] = useState(1);
  const [data, setData] = useState({});
  
  return (
    <div>
      {step === 1 && <ProfileStep onNext={(data) => {
        setData(prev => ({...prev, ...data}));
        setStep(2);
      }} />}
      
      {step === 2 && <CampusStep onNext={(data) => {
        setData(prev => ({...prev, ...data}));
        setStep(3);
      }} />}
      
      {step === 3 && <UploadStep 
        data={data} 
        onComplete={() => {/* redirect */}} 
      />}
    </div>
  );
}

AI Assistant Components

Public Thread

File: components/ai/public-thread.tsx

AI chat interface for public assistance.

Markdown Text

File: components/ai/markdown-text.tsx

Renders markdown responses from AI with syntax highlighting.

Shared Components from @repo/ui

The web app uses shared components from the @repo/ui package:

import { Button } from '@repo/ui/components/ui/button';
import { Card } from '@repo/ui/components/ui/card';
import { Input } from '@repo/ui/components/ui/input';
import { Dialog } from '@repo/ui/components/ui/dialog';
// ... and many more

See @repo/ui documentation for the complete component catalog.

Component Best Practices

✅
Component Best Practices
  1. Default to Server Components - Use Client Components only when needed
  2. Pass data down - Fetch in Server Components, pass to Client Components
  3. Colocate by feature - Keep related components together
  4. Use TypeScript - Define interfaces for all props
  5. Consistent naming - Use PascalCase, add Client suffix when needed
  6. Import from @repo/ui - Reuse shared components
  7. Handle loading states - Use Suspense and skeleton components

When to Use Client Components

Use "use client" when you need:

  • State: useState, useReducer
  • Effects: useEffect, useLayoutEffect
  • Event handlers: onClick, onChange, etc.
  • Browser APIs: window, document, localStorage
  • Custom hooks: useQuery, useForm, etc.

When to Use Server Components

Use Server Components (default) for:

  • Data fetching: Direct database access
  • Sensitive operations: API keys, secrets
  • SEO-critical content: Pre-rendered HTML
  • Static content: Non-interactive UI

Styling

Components use Tailwind CSS for styling:

export function MyComponent() {
  return (
    <div className="container mx-auto px-4 py-8">
      <h1 className="text-3xl font-bold text-primary">
        Title
      </h1>
      <p className="text-muted-foreground mt-2">
        Description
      </p>
    </div>
  );
}

See Styling Guide for more details.

Testing Components

Components should be testable and follow single responsibility principle:

// Good: Pure, testable component
export function EventCard({ event }: { event: Event }) {
  return (
    <div>
      <h3>{event.title}</h3>
      <p>{event.description}</p>
    </div>
  );
}

// Test
expect(screen.getByText('Event Title')).toBeInTheDocument();

Next Steps