10 - Coding Standards
Overview
This document outlines the coding standards and best practices for the Wavic WebApp project.
TypeScript
Type Definitions
Use explicit types for all function parameters and return values:
typescript
// ✅ Good
function getArtist(id: string): Promise<TArtist> {
// ...
}
// ❌ Avoid
function getArtist(id) {
// ...
}Type Naming Conventions
| Pattern | Usage | Example |
|---|---|---|
T prefix | Type aliases | TArtist, TTrack |
I prefix | Interfaces | IUser, IProject |
| PascalCase | Enums | TrackStatus, LAYOUT_OPTIONS |
| No prefix | Props | ArtistCardProps |
Avoid any
typescript
// ✅ Good
const data: TArtist[] = await response.json();
// ❌ Avoid
const data: any = await response.json();Use Type Guards
typescript
function isTrack(item: TArtist | TTrack): item is TTrack {
return 'audio' in item;
}React Components
Component Structure
typescript
'use client'; // Only if needed
// 1. Imports
import { useState, useEffect } from 'react';
import cn from '@/utils/class-names';
import { TArtist } from 'types/artist';
// 2. Types/Interfaces
interface ArtistCardProps {
artist: TArtist;
onSelect?: (artist: TArtist) => void;
className?: string;
}
// 3. Component
export default function ArtistCard({
artist,
onSelect,
className,
}: ArtistCardProps) {
// 4. Hooks
const [isHovered, setIsHovered] = useState(false);
// 5. Effects
useEffect(() => {
// ...
}, []);
// 6. Handlers
const handleClick = () => {
onSelect?.(artist);
};
// 7. Render
return (
<div
className={cn('p-4 rounded-lg', className)}
onClick={handleClick}
>
{artist.name}
</div>
);
}Client vs Server Components
typescript
// Server Component (default) - No 'use client'
// Can: fetch data, access backend, use async/await
// Cannot: useState, useEffect, browser APIs
// Client Component
'use client';
// Can: useState, useEffect, event handlers, browser APIs
// Cannot: async component, direct server-side operationsNaming Conventions
| Type | Convention | Example |
|---|---|---|
| Components | PascalCase | ArtistCard.tsx |
| Hooks | camelCase with use | useWindowSize.ts |
| Utilities | camelCase | formatDate.ts |
| Constants | SCREAMING_SNAKE | MAX_FILE_SIZE |
| Files | kebab-case | artist-card.tsx |
Styling
Tailwind CSS
Use Tailwind utility classes:
typescript
// ✅ Good
<div className="flex items-center gap-4 p-4 bg-white rounded-lg" />
// ❌ Avoid inline styles
<div style={{ display: 'flex', alignItems: 'center' }} />Class Name Utility
Use cn() for conditional classes:
typescript
import cn from '@/utils/class-names';
<div className={cn(
'base-classes',
isActive && 'active-classes',
variant === 'primary' && 'primary-classes',
className // Allow override
)} />Responsive Design
Mobile-first approach:
typescript
<div className={cn(
'text-sm', // Mobile
'md:text-base', // Tablet
'lg:text-lg', // Desktop
)} />Dark Mode
Use Tailwind dark variant:
typescript
<div className="bg-white dark:bg-gray-900 text-black dark:text-white" />State Management
Local State
Use for component-specific UI state:
typescript
const [isOpen, setIsOpen] = useState(false);
const [inputValue, setInputValue] = useState('');Context
Use for feature-level shared state:
typescript
// Creating context
const WaveContext = createContext<WaveContextProps | null>(null);
// Using context
const { track, setTrack } = useWaveContext();Server State
Use Server Actions for data fetching:
typescript
'use server';
export async function getArtists(token: string) {
const response = await fetch(`${API_URL}/artist`, {
headers: { Authorization: `Bearer ${token}` },
});
return response.json();
}API Calls
Standard Pattern
typescript
export async function fetchData(token: string): Promise<DataType> {
try {
const response = await fetch(`${API_URL}/endpoint`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
}Error Handling
typescript
// In components
try {
const data = await fetchData(token);
setData(data);
} catch (error) {
toast.error('Failed to load data');
setError(error);
}File Organization
Imports Order
typescript
// 1. React/Next.js
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
// 2. Third-party libraries
import { toast } from 'react-hot-toast';
import { motion } from 'framer-motion';
// 3. Internal - absolute imports
import { routes } from '@/config/routes';
import cn from '@/utils/class-names';
import { useWaveContext } from '@/contexts/waveContext';
// 4. Internal - types
import { TArtist } from 'types/artist';
// 5. Internal - relative imports
import { ChildComponent } from './child-component';Export Patterns
typescript
// Named exports for utilities
export function formatDate(date: Date) {}
export const MAX_SIZE = 1024;
// Default export for components
export default function MyComponent() {}Error Handling
Try-Catch
typescript
async function handleSubmit() {
try {
setLoading(true);
await createArtist(formData, token);
toast.success('Artist created');
router.push('/');
} catch (error) {
toast.error('Failed to create artist');
console.error(error);
} finally {
setLoading(false);
}
}Error Boundaries
Wrap feature areas with error boundaries:
typescript
<ErrorBoundary fallback={<ErrorFallback />}>
<FeatureComponent />
</ErrorBoundary>Performance
Memoization
typescript
// Expensive computations
const sortedTracks = useMemo(
() => tracks.sort((a, b) => a.order - b.order),
[tracks]
);
// Stable callbacks
const handleClick = useCallback(() => {
setIsOpen(true);
}, []);Lazy Loading
typescript
import dynamic from 'next/dynamic';
const HeavyComponent = dynamic(
() => import('./heavy-component'),
{ loading: () => <Skeleton /> }
);Image Optimization
typescript
import Image from 'next/image';
<Image
src={artist.image}
alt={artist.name}
width={200}
height={200}
placeholder="blur"
blurDataURL={blurHash}
/>Testing (Future)
Component Testing
typescript
// __tests__/artist-card.test.tsx
import { render, screen } from '@testing-library/react';
import ArtistCard from '../artist-card';
describe('ArtistCard', () => {
it('renders artist name', () => {
render(<ArtistCard artist={mockArtist} />);
expect(screen.getByText(mockArtist.name)).toBeInTheDocument();
});
});Documentation
JSDoc Comments
typescript
/**
* Fetches artist data from the API
* @param token - JWT authentication token
* @param artistId - The artist's unique identifier
* @returns Promise resolving to artist data
* @throws Error if the request fails
*/
export async function getArtist(
token: string,
artistId: string
): Promise<TArtist> {
// ...
}Component Documentation
typescript
/**
* ArtistCard displays an artist's information in a card format.
*
* @example
* ```tsx
* <ArtistCard
* artist={artist}
* onSelect={handleSelect}
* className="mb-4"
* />
* ```
*/
export default function ArtistCard({ ... }) { }Git Practices
Commit Messages
<type>(<scope>): <description>
feat(artist): add artist card component
fix(player): resolve volume slider issue
docs(readme): update installation steps
style(ui): format button component
refactor(auth): simplify login flow
test(artist): add unit tests for artist service
chore(deps): update dependenciesBranch Naming
feature/add-artist-sharing
fix/player-volume-bug
docs/update-readme
refactor/auth-simplification