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 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
Navigation
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>
);
}Footer
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 moreSee @repo/ui documentation for the complete component catalog.
Component Best Practices
- Default to Server Components - Use Client Components only when needed
- Pass data down - Fetch in Server Components, pass to Client Components
- Colocate by feature - Keep related components together
- Use TypeScript - Define interfaces for all props
- Consistent naming - Use PascalCase, add
Clientsuffix when needed - Import from @repo/ui - Reuse shared components
- 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();Related Documentation
- Routing - Page structure
- Server Actions - Data mutations
- @repo/ui Package - Shared component library
- Styling Guide - Tailwind patterns
Next Steps
- Learn about server actions
- Explore feature implementations
- Review styling conventions
