Feature development workflow with QA testing for the KATH competition platform. Use when: (1) Implementing new features or pages, (2) Fixing bugs in existing components, (3) Refactoring code, (4) Adding new routes or components. Always follow the implement -> test -> verify cycle. Integrates with supabase-dev skill for database-related features.
src/types/index.ts and src/lib/supabase.ts)src/services/supabase.service.ts)src/hooks/)src/pages/, src/sections/, src/components/)src/App.tsx)After each feature implementation, run these verification steps:
npx tsc --noEmit 2>&1 | head -50
MUST pass with 0 errors before proceeding.
npm run build 2>&1 | tail -20
MUST succeed before proceeding.
npx vitest run 2>&1 | tail -20
All tests MUST pass.
import { useEffect, useState } from 'react';
import { supabaseXxxService } from '@/services/supabase.service';
import type { Xxxx } from '@/types';
export const MyComponent = () => {
const [data, setData] = useState<Xxxx[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const result = await supabaseXxxService.getAll();
if (result.data) setData(result.data);
else setError(result.error?.message || 'Failed to load');
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
fetchData();
}, []);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage message={error} />;
return <div>{/* render */}</div>;
};
const [errors, setErrors] = useState<Record<string, string>>({});
const validate = (): boolean => {
const newErrors: Record<string, string> = {};
if (!field.trim()) newErrors.field = 'Field is required';
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async () => {
if (!validate()) return;
// submit logic
};
const handleFileUpload = async (file: File) => {
// Validate
const maxSize = 10 * 1024 * 1024; // 10MB
const allowedTypes = ['application/pdf', 'image/png', 'image/jpeg'];
if (file.size > maxSize) { setError('File too large'); return; }
if (!allowedTypes.includes(file.type)) { setError('Invalid file type'); return; }
// Upload
const fileName = `${Date.now()}-${file.name}`;
const { data, error } = await supabase.storage
.from('bucket')
.upload(`submissions/${fileName}`, file);
if (error) { setError(error.message); return; }
// Get public URL
const { data: { publicUrl } } = supabase.storage
.from('bucket')
.getPublicUrl(`submissions/${fileName}`);
};
When adding new pages, register in src/App.tsx:
// For admin pages (inside AdminLayout):
<Route path="new-page" element={<AdminNewPage />} />
// For public pages:
<Route path="/new-page" element={<NewPage />} />
// For protected pages:
<Route element={<ParticipantRoute />}>
<Route path="/new-page" element={<NewPage />} />
</Route>
Icons are re-exported from src/icons/index.tsx (wraps lucide-react).
Import from @/icons for consistency:
import { Trophy, Users, ArrowRight } from '@/icons';
getByCompetition(competitionId) needs the ID@/types and @/lib/supabase for the same type - pick oneany type - use proper types or unknown