Skip to content

Latest commit

 

History

History
271 lines (213 loc) · 9.22 KB

File metadata and controls

271 lines (213 loc) · 9.22 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Essential Commands

Development

npm run dev          # Start dev server with service worker generation
npm run build        # TypeScript check + production build with service worker
npm run lint         # Run ESLint
npm run preview      # Preview production build

Service Worker

The Firebase Cloud Messaging (FCM) service worker is auto-generated:

npm run generate:sw  # Generate firebase-messaging-sw.js from template

This runs automatically before dev and build commands.

Architecture Overview

Project Structure

src/
├── domain/          # Reusable business logic and types (cardsets, groups, users, etc.)
├── features/        # Feature modules with page-level components and logic
├── pages/           # Legacy route components (prefer routes/ for new pages)
├── routes/          # TanStack Router file-based routing
├── shared/          # Shared APIs, components, utilities, services
└── stores/          # Zustand global state (auth only)

Key Pattern: This codebase uses a domain-feature separation:

  • Domain layer (domain/): Contains reusable business logic, types, and React Query hooks that can be used across multiple features
  • Feature layer (features/): Contains feature-specific implementations, forms, schemas, and UI components
  • Shared layer (shared/): Infrastructure code (API clients, socket management, base components)

Routing (TanStack Router)

  • File-based routing: Routes are defined in src/routes/*.tsx
  • Auto-generated route tree: routeTree.gen.ts is generated by Vite plugin during build
  • Auth guards: Use authGuard() in route beforeLoad hooks

Example route structure:

// src/routes/some-route.tsx
export const Route = createFileRoute("/some-route")({
  component: RouteComponent,
  beforeLoad: ({ context }) => {
    authGuard({ auth: context.auth, mode: "protected" }); // or "non-protected"
  },
  validateSearch: (search) => ({ /* query params */ }),
  head: () => ({ meta: [/* SEO tags */] })
});

Auth guard modes:

  • "protected": Requires authentication (redirects to /auth/login if not authenticated)
  • "non-protected": Requires NO authentication (redirects to / if authenticated)
  • "bypass": No auth requirement

State Management

Zustand is used for global state (auth only):

  • Primary store: useAuthStore in src/stores/useAuthStore.ts
  • Manages: user profile, auth initialization, token refresh
  • Key methods:
    • initializeAuth(): Called on app mount to refresh token and sync user
    • syncUser(): Fetches current user from API
    • refreshToken(): Handles token refresh (called by axios interceptor)
    • clearUser(): Logout

API Layer

Multi-client setup:

  • Primary: Axios client (src/shared/apis/fetch.ts) with interceptors
  • Secondary: Custom FetchClient (src/shared/apis/fetchClient.ts)

Axios interceptor handles automatic token refresh:

  • On 401 response → attempts token refresh → retries original request
  • Skips refresh for auth endpoints (/auth/login, /auth/register, etc.)

API modules in src/shared/apis/:

  • Each domain has its own file: auth.ts, user.ts, card-set.ts, group.ts, etc.
  • Type-safe responses using generics: apiClient.get<ApiResponse<T>>()

React Query Integration

Domain hooks in domain/*/hooks/:

  • Use useSuspenseInfiniteQuery for paginated lists
  • Query keys include all parameters for proper cache isolation
  • select option transforms API responses into component-friendly shapes

Example pattern:

export const useCardSets = (params?: UseCardSetsParams) => {
  return useSuspenseInfiniteQuery({
    queryKey: ["cardsets", params],
    queryFn: async ({ pageParam }) => cardSetApi.getCardSets({ ...params, page: pageParam }),
    initialPageParam: 0,
    getNextPageParam: (lastPage) => lastPage.data.hasNext ? lastPage.data.nextPage : undefined,
    select: (data) => ({
      cardsets: data.pages.flatMap(page => page.data.cardsets),
      hasNext: data.pages[data.pages.length - 1].data.hasNext
    })
  });
};

Important: Query client is configured with refetchOnWindowFocus: false.

Real-Time Features (Socket.io + Yjs)

Socket.io (src/shared/socket/):

  • Singleton SocketManager ensures single WebSocket connection
  • useSocket hook provides: connect(), disconnect(), emit(), on(), off()
  • Socket URL from VITE_SOCKET_URL environment variable

Yjs collaborative editing:

  • YjsProvider class wraps Y.Doc and socket communication
  • useYjs hook manages connection, cards array, and awareness (cursor positions)
  • Shared structure: Y.Array<Y.Map> for cards

Socket event types in src/shared/socket/events.ts:

  • Server→Client: sync, awareness, expired, error
  • Client→Server: auth, update, awareness

Form Handling

React Hook Form + Zod pattern:

  1. Define Zod schema in features/*/schemas/form.schema.ts
  2. Use zodResolver in useForm hook
  3. Separate schemas for form validation vs API requests

Example:

// Schema
const schema = z.object({
  name: z.string().min(1, "Required"),
  category: z.enum(["IT", "LANGUAGE", "etc"]),
});

// Component
const { register, handleSubmit, formState: { errors } } = useForm({
  resolver: zodResolver(schema),
  defaultValues: { /* ... */ }
});

Feature Organization

Typical feature structure:

src/features/feature-name/
├── components/          # UI components
├── schemas/
│   ├── form.schema.ts   # Zod schema for form validation
│   └── request.schema.ts # API request validation
├── hooks/               # Feature-specific React Query hooks
├── model/               # Domain models (if needed)
└── ui/                  # UI-specific utilities

Domain Organization

Domain modules contain reusable business logic:

src/domain/entity-name/
├── components/          # Reusable components for this domain
├── hooks/              # React Query hooks
├── sub-domain/         # Nested domains (e.g., group/invitation/)
│   ├── hooks/
│   ├── types.ts
│   └── index.ts
└── types.ts            # Type definitions + converter functions

Type conversion pattern: Define toEntityName() converter functions that transform API responses into domain types.

Firebase Cloud Messaging (FCM)

Initialization flow (src/shared/libs/firebase.ts + src/shared/services/fcmService.ts):

  1. Firebase app initialized with env vars
  2. Service worker registered
  3. Notification permission requested
  4. FCM token obtained and registered with backend
  5. Foreground message listener set up

Called from useAuthStore.initializeAuth():

await registerFCMToken();
initializeForegroundMessageListener();

Shared Components

Component library (src/shared/components/):

  • Built with Radix UI + Tailwind CSS
  • Includes: form inputs, layout components (card, sheet), custom components (flip-card, infinite-scroll-list)
  • Skeleton components for Suspense fallbacks

Layouts (src/shared/layouts/):

  • BaseLayout: GNB + centered content container
  • SidebarTabLayout: Responsive sidebar + content (switches flex direction on mobile)

Image Upload Pattern

S3 presigned URL pattern (src/shared/lib/upload-image.ts):

  1. Calculate MD5 hash of file
  2. Request presigned URL from backend (imageApi.getPresignedUrl())
  3. PUT file directly to S3 using presigned URL
  4. Return imageRefId from backend response

Path Aliases

TypeScript path alias configured: @/*./src/*

Always use absolute imports:

import { Button } from "@/shared/components/ui/button"
import { useAuthStore } from "@/stores/useAuthStore"

Environment Variables

Required in .env.development and .env.production:

  • VITE_BASE_URL: API base URL
  • VITE_SOCKET_URL: WebSocket endpoint
  • VITE_FIREBASE_*: Firebase config (API key, auth domain, project ID, etc.)

Vite Configuration

  • TanStack Router plugin: Auto-generates route tree with code splitting
  • Proxy: /apihttps://api.flipnote.site/v1 (dev only)
  • Path alias: @./src
  • Tailwind CSS: Integrated via Vite plugin

Mobile-First Responsive Design

  • Use Tailwind breakpoints: md: for tablet+
  • SidebarTabLayout adapts from horizontal to vertical on mobile
  • GNB has separate authenticated/unauthenticated UI

Key Conventions

  1. Query keys: Include all parameters for cache isolation

    queryKey: ["entity", params]
  2. Error handling:

    • 401 errors trigger automatic token refresh via axios interceptor
    • Socket connection errors stored in hook state
    • Failed token refresh clears auth store (logout)
  3. Suspense integration:

    • Use useSuspenseInfiniteQuery for automatic Suspense
    • Provide skeleton components as Suspense fallbacks
    • Requires Suspense boundary in parent component
  4. Type safety:

    • Full TypeScript with strict mode
    • Zod for runtime validation
    • Type-safe API clients with generics
  5. Code splitting:

    • TanStack Router handles automatic route-based code splitting
    • No manual lazy loading needed for routes