

Also from Kynth Studios


Also from Kynth Studios


Also from Kynth Studios
1# GenSlides Frontend - Development Guidelines23IMPORTANT: always use latest dependencies. follow design tokens and global.css in ./src/styles/design-tokens.css and ./src/styles/global.css45## Tech Stack67- **Language**: TypeScript8- **Framework**: React 19+9- **Build Tool**: Vite10- **Styling**: Tailwind CSS11- **State Management**: Zustand12- **Drag & Drop**: @dnd-kit1314## Architecture Principles1516### SOLID Principles (Adapted for React)1718- **S - Single Responsibility**: One component, one purpose19 - `SlideList.tsx` - Renders list only, delegates item rendering to `SlideItem`20 - `useSlides.ts` - Data fetching only, no UI logic21 - `slideStore.ts` - State management only, no API calls2223- **O - Open/Closed**: Components open for extension via props2425```tsx26 // Good: extensible via props27 <Button variant="primary" size="lg" onClick={handleClick} />2829 // Avoid: modifying component internals for each use case30```3132- **L - Liskov Substitution**: Components with same interface are interchangeable3334```tsx35 // Both can be used wherever a clickable element is expected36 <Button onClick={...}>Click</Button>37 <IconButton onClick={...} icon={<PlayIcon />} />38```3940- **I - Interface Segregation**: Small, focused prop interfaces4142```tsx43 // Good: focused interface44 interface SlideItemProps {45 slide: Slide;46 isSelected: boolean;47 onSelect: (sid: string) => void;48 }4950 // Avoid: bloated interface with optional props51```5253- **D - Dependency Inversion**: Components depend on abstractions5455```tsx56 // Good: component receives data via props/hooks57 function SlideList({ slides, onSelect }: Props) { ... }5859 // Avoid: component directly imports global state60```6162### YAGNI (You Aren't Gonna Need It)6364- Don't add props "just in case"65- Start with inline styles, extract to components when reused66- Avoid premature optimization (memo, useMemo, useCallback)6768### KISS (Keep It Simple, Stupid)6970- Prefer composition over configuration71- Use native HTML elements when possible72- Minimize state - derive values when possible7374### DRY (Don't Repeat Yourself)7576- Avoid duplicating code77- Use functions, classes, and modules to DRY code78- Use patterns and templates to DRY code7980## Code Organization8182```83src/84├── main.tsx # App entry point85├── App.tsx # Root component, routing setup86├── api/ # API layer - HTTP requests only87│ ├── index.ts # Axios/fetch instance, interceptors88│ ├── slides.ts # Slides API functions89│ ├── images.ts # Images API functions90│ └── style.ts # Style API functions91├── stores/ # Zustand stores - global state only92│ ├── index.ts # Store exports93│ ├── slideStore.ts # Slides & project state94│ └── playerStore.ts # Playback state95├── components/ # UI components96│ ├── layout/ # Layout components (Header, Sidebar, etc.)97│ ├── slides/ # Slide-related components98│ ├── preview/ # Image preview components99│ ├── player/ # Fullscreen player100│ ├── style/ # Style picker modal101│ └── common/ # Reusable UI primitives102├── hooks/ # Custom React hooks103├── types/ # TypeScript type definitions104└── styles/ # Global styles105```106107### Component Structure108109```tsx110// components/slides/SlideItem.tsx111112// 1. Imports (React, libraries, local)113import { useState, useCallback } from 'react';114import { useSortable } from '@dnd-kit/sortable';115import { CSS } from '@dnd-kit/utilities';116import type { Slide } from '@/types';117118// 2. Types119interface SlideItemProps {120 slide: Slide;121 isSelected: boolean;122 onSelect: (sid: string) => void;123 onEdit: (sid: string, content: string) => void;124}125126// 3. Component127export function SlideItem({ slide, isSelected, onSelect, onEdit }: SlideItemProps) {128 // 3a. Hooks129 const [isEditing, setIsEditing] = useState(false);130 const { attributes, listeners, setNodeRef, transform, transition } = useSortable({131 id: slide.sid,132 });133134 // 3b. Derived state135 const style = {136 transform: CSS.Transform.toString(transform),137 transition,138 };139140 // 3c. Handlers141 const handleDoubleClick = useCallback(() => {142 setIsEditing(true);143 }, []);144145 // 3d. Render146 return (147 <div148 ref={setNodeRef}149 style={style}150 {...attributes}151 {...listeners}152 onClick={() => onSelect(slide.sid)}153 onDoubleClick={handleDoubleClick}154 className={`p-4 rounded-lg ${isSelected ? 'ring-2 ring-blue-500' : ''}`}155 >156 {isEditing ? (157 <SlideEditor ... />158 ) : (159 <p>{slide.content}</p>160 )}161 </div>162 );163}164```165166## State Management (Zustand)167168### Store Structure169170```typescript171// stores/slideStore.ts172import { create } from 'zustand';173import { slidesApi } from '@/api/slides';174import type { Slide, Style, ProjectResponse } from '@/types';175176interface SlideState {177 // Data178 slug: string | null;179 title: string;180 style: Style | null;181 slides: Slide[];182 selectedSlideId: string | null;183184 // Loading states185 isLoading: boolean;186 isGenerating: boolean;187 error: string | null;188189 // Actions190 loadProject: (slug: string) => Promise<void>;191 selectSlide: (sid: string) => void;192 createSlide: (content: string, position?: number) => Promise<void>;193 updateSlide: (sid: string, content: string) => Promise<void>;194 deleteSlide: (sid: string) => Promise<void>;195 reorderSlides: (slideIds: string[]) => Promise<void>;196 clearError: () => void;197}198199export const useSlideStore = create<SlideState>((set, get) => ({200 // Initial state201 slug: null,202 title: '',203 style: null,204 slides: [],205 selectedSlideId: null,206 isLoading: false,207 isGenerating: false,208 error: null,209210 // Actions211 loadProject: async (slug) => {212 set({ isLoading: true, error: null });213 try {214 const data = await slidesApi.getProject(slug);215 set({216 slug,217 title: data.title,218 style: data.style,219 slides: data.slides,220 selectedSlideId: data.slides[0]?.sid ?? null,221 isLoading: false,222 });223 } catch (e) {224 set({ error: (e as Error).message, isLoading: false });225 }226 },227228 selectSlide: (sid) => set({ selectedSlideId: sid }),229230 // ... other actions231}));232```233234### Store Best Practices235236```typescript237// DO: Keep stores focused238const useSlideStore = create<SlideState>(...); // Slide data239const usePlayerStore = create<PlayerState>(...); // Playback state240241// DON'T: Create god stores with everything242const useAppStore = create<EverythingState>(...);243244// DO: Use selectors for derived state245const selectedSlide = useSlideStore(246 (state) => state.slides.find(s => s.sid === state.selectedSlideId)247);248249// DON'T: Store derived state250const useSlideStore = create((set) => ({251 slides: [],252 selectedSlideId: null,253 selectedSlide: null, // This is derived, don't store it!254}));255```256257## Concurrency & Async258259### API Request Patterns260261```typescript262// api/index.ts263const BASE_URL = '/api';264265export async function request<T>(266 endpoint: string,267 options: RequestInit = {}268): Promise<T> {269 const response = await fetch(`${BASE_URL}${endpoint}`, {270 headers: {271 'Content-Type': 'application/json',272 ...options.headers,273 },274 ...options,275 });276277 if (!response.ok) {278 const error = await response.json().catch(() => ({}));279 throw new Error(error.detail || `Request failed: ${response.status}`);280 }281282 return response.json();283}284285// api/slides.ts286export const slidesApi = {287 getProject: (slug: string) =>288 request<ProjectResponse>(`/slides/${slug}`),289290 createSlide: (slug: string, data: CreateSlideRequest) =>291 request<SlideResponse>(`/slides/${slug}`, {292 method: 'POST',293 body: JSON.stringify(data),294 }),295};296```297298### Loading States299300```tsx301function SlideList() {302 const { slides, isLoading, error } = useSlideStore();303304 if (isLoading) {305 return <LoadingSpinner />;306 }307308 if (error) {309 return <ErrorMessage message={error} />;310 }311312 if (slides.length === 0) {313 return <EmptyState message="No slides yet" />;314 }315316 return (317 <div>318 {slides.map(slide => <SlideItem key={slide.sid} slide={slide} />)}319 </div>320 );321}322```323324### Optimistic Updates325326```typescript327// For better UX, update UI before server confirms328reorderSlides: async (slideIds) => {329 const prevSlides = get().slides;330331 // Optimistic update332 const reorderedSlides = slideIds.map(333 id => prevSlides.find(s => s.sid === id)!334 );335 set({ slides: reorderedSlides });336337 try {338 await slidesApi.reorderSlides(get().slug!, slideIds);339 } catch (e) {340 // Rollback on error341 set({ slides: prevSlides, error: (e as Error).message });342 }343},344```345346### Debouncing347348```typescript349// hooks/useDebounce.ts350import { useState, useEffect } from 'react';351352export function useDebounce<T>(value: T, delay: number): T {353 const [debouncedValue, setDebouncedValue] = useState(value);354355 useEffect(() => {356 const timer = setTimeout(() => setDebouncedValue(value), delay);357 return () => clearTimeout(timer);358 }, [value, delay]);359360 return debouncedValue;361}362363// Usage in SlideEditor364function SlideEditor({ slide, onSave }: Props) {365 const [content, setContent] = useState(slide.content);366 const debouncedContent = useDebounce(content, 500);367368 useEffect(() => {369 if (debouncedContent !== slide.content) {370 onSave(slide.sid, debouncedContent);371 }372 }, [debouncedContent, slide.sid, slide.content, onSave]);373374 return <textarea value={content} onChange={e => setContent(e.target.value)} />;375}376```377378## Error Handling379380### Error Boundary381382```tsx383// components/common/ErrorBoundary.tsx384import { Component, ReactNode } from 'react';385386interface Props {387 children: ReactNode;388 fallback?: ReactNode;389}390391interface State {392 hasError: boolean;393 error: Error | null;394}395396export class ErrorBoundary extends Component<Props, State> {397 state: State = { hasError: false, error: null };398399 static getDerivedStateFromError(error: Error): State {400 return { hasError: true, error };401 }402403 componentDidCatch(error: Error, info: React.ErrorInfo) {404 console.error('ErrorBoundary caught:', error, info);405 }406407 render() {408 if (this.state.hasError) {409 return this.props.fallback ?? (410 <div className="p-4 text-red-500">411 Something went wrong: {this.state.error?.message}412 </div>413 );414 }415 return this.props.children;416 }417}418```419420### API Error Handling421422```typescript423// types/index.ts424export interface ApiError {425 detail: string;426 error_code?: string;427}428429// api/index.ts430export class ApiRequestError extends Error {431 constructor(432 message: string,433 public statusCode: number,434 public errorCode?: string435 ) {436 super(message);437 this.name = 'ApiRequestError';438 }439}440441// Handle in components442async function handleGenerateImage() {443 try {444 await generateImage(selectedSlideId);445 } catch (e) {446 if (e instanceof ApiRequestError) {447 if (e.statusCode === 502) {448 toast.error('Image generation failed. Please try again.');449 } else if (e.statusCode === 429) {450 toast.error('Rate limited. Please wait a moment.');451 }452 } else {453 toast.error('An unexpected error occurred.');454 }455 }456}457```458459### Form Validation460461```tsx462function StylePickerForm({ onSubmit }: Props) {463 const [prompt, setPrompt] = useState('');464 const [error, setError] = useState<string | null>(null);465466 const handleSubmit = (e: React.FormEvent) => {467 e.preventDefault();468 setError(null);469470 if (!prompt.trim()) {471 setError('Please enter a style description');472 return;473 }474475 if (prompt.length < 5) {476 setError('Description must be at least 5 characters');477 return;478 }479480 onSubmit(prompt);481 };482483 return (484 <form onSubmit={handleSubmit}>485 <input486 value={prompt}487 onChange={e => setPrompt(e.target.value)}488 className={error ? 'border-red-500' : ''}489 />490 {error && <p className="text-red-500 text-sm">{error}</p>}491 <button type="submit">Generate</button>492 </form>493 );494}495```496497## Logging & Debugging498499### Console Logging (Development Only)500501```typescript502// utils/logger.ts503const isDev = import.meta.env.DEV;504505export const logger = {506 debug: (...args: unknown[]) => {507 if (isDev) console.debug('[DEBUG]', ...args);508 },509 info: (...args: unknown[]) => {510 if (isDev) console.info('[INFO]', ...args);511 },512 warn: (...args: unknown[]) => {513 console.warn('[WARN]', ...args);514 },515 error: (...args: unknown[]) => {516 console.error('[ERROR]', ...args);517 },518};519520// Usage521logger.debug('Slide selected:', sid);522logger.info('Project loaded:', { slug, slideCount: slides.length });523logger.error('API request failed:', error);524```525526### React DevTools Integration527528```typescript529// stores/slideStore.ts530import { devtools } from 'zustand/middleware';531532export const useSlideStore = create<SlideState>()(533 devtools(534 (set, get) => ({535 // ... store implementation536 }),537 { name: 'SlideStore' } // Shows in Redux DevTools538 )539);540```541542## Testing543544### Test Structure545546```547src/548├── __tests__/549│ ├── components/550│ │ └── SlideItem.test.tsx551│ ├── stores/552│ │ └── slideStore.test.ts553│ └── hooks/554│ └── useKeyboard.test.ts555```556557### Component Testing558559```tsx560// __tests__/components/SlideItem.test.tsx561import { render, screen, fireEvent } from '@testing-library/react';562import { SlideItem } from '@/components/slides/SlideItem';563564describe('SlideItem', () => {565 const mockSlide = {566 sid: 'slide_001',567 content: 'Test slide content',568 content_hash: 'abc123',569 created_at: '2024-01-01T00:00:00Z',570 updated_at: '2024-01-01T00:00:00Z',571 };572573 it('renders slide content', () => {574 render(575 <SlideItem576 slide={mockSlide}577 isSelected={false}578 onSelect={vi.fn()}579 onEdit={vi.fn()}580 />581 );582 expect(screen.getByText('Test slide content')).toBeInTheDocument();583 });584585 it('calls onSelect when clicked', () => {586 const onSelect = vi.fn();587 render(588 <SlideItem589 slide={mockSlide}590 isSelected={false}591 onSelect={onSelect}592 onEdit={vi.fn()}593 />594 );595 fireEvent.click(screen.getByText('Test slide content'));596 expect(onSelect).toHaveBeenCalledWith('slide_001');597 });598});599```600601### Running Tests602603```bash604# Run all tests605npm test606607# Run in watch mode608npm test -- --watch609610# Run with coverage611npm test -- --coverage612```613614## Code Style615616### TypeScript Strict Mode617618```json619// tsconfig.json620{621 "compilerOptions": {622 "strict": true,623 "noUncheckedIndexedAccess": true,624 "noImplicitReturns": true625 }626}627```628629### Naming Conventions630631| Type | Convention | Example |632|------------------|-------------------------------|---------------------------|633| Components | PascalCase | `SlideItem.tsx` |634| Hooks | camelCase with `use` prefix | `useSlides.ts` |635| Stores | camelCase with `Store` suffix | `slideStore.ts` |636| Types/Interfaces | PascalCase | `Slide`, `SlideItemProps` |637| Constants | SCREAMING_SNAKE_CASE | `API_BASE_URL` |638| CSS Classes | kebab-case (Tailwind) | `bg-blue-500` |639640### Import Order641642```tsx643// 1. React644import { useState, useEffect } from 'react';645646// 2. Third-party libraries647import { useSortable } from '@dnd-kit/sortable';648649// 3. Internal modules (absolute imports)650import { useSlideStore } from '@/stores';651import type { Slide } from '@/types';652653// 4. Relative imports654import { SlideEditor } from './SlideEditor';655656// 5. Styles657import './SlideItem.css';658```659660### Tailwind Best Practices661662```tsx663// DO: Use Tailwind utilities directly664<button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">665 Click me666</button>667668// DO: Extract repeated patterns to components669function Button({ children, variant = 'primary' }: ButtonProps) {670 const variants = {671 primary: 'bg-blue-500 text-white hover:bg-blue-600',672 secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',673 };674 return (675 <button className={`px-4 py-2 rounded ${variants[variant]}`}>676 {children}677 </button>678 );679}680681// AVOID: Using @apply excessively in CSS682```683
One repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| tyrchen/geektime-bootcamp-ai.cursor/rules/python-fastapi-backend.mdc · 230 | Cursor rules | setuptestlint-formatstyle+7 | 84/100 | 9 days ago | |
| tyrchen/geektime-bootcamp-ai.cursor/rules/rust-best-practices.mdc · 230 | Cursor rules | teststylearchdependencies+1 | 81/100 | 9 days ago | |
| tyrchen/geektime-bootcamp-ai.cursor/rules/specify-rules.mdc · 230 | Cursor rules | stylearch | 52/100 | 9 days ago | |
| tyrchen/geektime-bootcamp-aisite/CLAUDE.md · 230 | CLAUDE.md | agent-behaviour | 25/100 | 9 days ago | |
| tyrchen/geektime-bootcamp-aiw3/raflow/CLAUDE.md · 230 | CLAUDE.md | agent-behaviour | 25/100 | 9 days ago | |
| tyrchen/geektime-bootcamp-aiw6/codereview-agent/CLAUDE.md · 230 | CLAUDE.md | setupbuildlint-formatarch+4 | 90/100 | 9 days ago | |
| tyrchen/geektime-bootcamp-aiw6/opencode-introspection/CLAUDE.md · 230 | CLAUDE.md | setupbuildtestlint-format+10 | 84/100 | 9 days ago | |
| tyrchen/geektime-bootcamp-aiw6/opencode-introspection/visualizer/CLAUDE.md · 230 | CLAUDE.md | setupbuildtestlint-format+9 | 78/100 | 9 days ago | |
| tyrchen/geektime-bootcamp-aiw6/simple-agent/CLAUDE.md · 230 | CLAUDE.md | setupbuildtestlint-format+10 | 84/100 | 9 days ago | |
| tyrchen/geektime-bootcamp-aiw7/genslides/backend/CLAUDE.md · 230 | CLAUDE.md | testlint-formatstylearch+6 | 100/100 | 9 days ago | |
| tyrchen/geektime-bootcamp-aiw5/pg-mcp/CLAUDE.md · 230 | CLAUDE.md | setuptestlint-formatstyle+5 | 86/100 | 9 days ago |
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| dotCMS/corecore-web/CLAUDE.md · 949 | CLAUDE.md | teststylearchtesting-strategy+3 | 100/100 | 14 days ago | |
| tphakala/birdnet-goCLAUDE.md · 1.6k | CLAUDE.md | buildtestlint-formatstyle+8 | 100/100 | today | |
| tyrchen/geektime-bootcamp-aiw7/genslides/backend/CLAUDE.md · 230 | CLAUDE.md | testlint-formatstylearch+6 | 100/100 | 9 days ago | |
| nimbalyst/nimbalystpackages/android/CLAUDE.md · 1.5k | CLAUDE.md | setupbuildstylearch+2 | 100/100 | 14 days ago | |
| microsoft/playwrightCLAUDE.md · 95k | CLAUDE.md | buildtestlint-formatstyle+7 | 100/100 | 7 days ago | |
| Adit-Jain-srm/NightmareNetCLAUDE.md · 46 | CLAUDE.md | buildtestlint-formatstyle+6 | 100/100 | 14 days ago | |
| bagisto/bagistoCLAUDE.md · 28k | CLAUDE.md | setupbuildteststyle+5 | 100/100 | 7 days ago | |
| dotCMS/coreCLAUDE.md · 949 | CLAUDE.md | setupbuildteststyle+7 | 99/100 | today |
A badge carrying the measured quality of the strongest agent config file in this repository, out of 100. It reads from this index every time somebody loads your page, so it changes when the measurement changes and there is nothing to keep up to date. Free, no account, and the value is not something you or we can set by hand.
[](https://rulestack.kynth.studio/configs/tyrchen-geektime-bootcamp-ai-w7-genslides-frontend-claude)Would rather not hotlink us? Every badge is also served in shields.io’s endpoint schema, so shields renders the image and your readers never talk to our domain:
Published by Toolproof, the masthead over this index and eight others. The method behind the number is at toolproof.kynth.studio/methodology, and the whole thing is readable as JSON with no key at /api.