diff --git a/.cursor/rules/functional-programming.mdc b/.cursor/rules/functional-programming.mdc new file mode 100644 index 000000000..4fdb29ef8 --- /dev/null +++ b/.cursor/rules/functional-programming.mdc @@ -0,0 +1,478 @@ +--- +description: +globs: +alwaysApply: false +--- +# Frontend Functional Programming Cursor Rules + +## Core Principles +- Prioritize pure functions and immutable data structures +- Favor function composition over inheritance/ +- Use declarative programming patterns over imperative ones +- Minimize side effects and make them explicit when necessary +- Follow the principle of least surprise in API design + +## TypeScript/JavaScript Guidelines + +### Function Design +```typescript +// ✅ Pure functions with clear input/output +const calculateTotalPrice = (items: Item[], taxRate: number): number => + items.reduce((total, item) => total + item.price, 0) * (1 + taxRate); + +// ✅ Higher-order functions for reusability +const withLogging = ( + fn: (...args: T) => R +) => (...args: T): R => { + console.log(`Calling ${fn.name} with:`, args); + const result = fn(...args); + console.log(`Result:`, result); + return result; +}; + +// ❌ Avoid functions with side effects +const addItemToCart = (item: Item) => { + cart.push(item); // Mutating global state + updateUI(); // Side effect +}; +``` + +### Data Transformation +```typescript +// ✅ Use immutable operations +const addItem = (items: Item[], newItem: Item): Item[] => [...items, newItem]; +const updateItem = (items: Item[], id: string, updates: Partial): Item[] => + items.map(item => item.id === id ? { ...item, ...updates } : item); + +// ✅ Prefer map/filter/reduce over loops +const getActiveUsers = (users: User[]): User[] => + users.filter(user => user.isActive); + +const getUserNames = (users: User[]): string[] => + users.map(user => user.name); +``` + +### Error Handling +```typescript +// ✅ Use Result/Either types for error handling +type Result = + | { success: true; data: T } + | { success: false; error: E }; + +const parseUser = (json: string): Result => { + try { + const data = JSON.parse(json); + return { success: true, data }; + } catch (error) { + return { success: false, error: 'Invalid JSON' }; + } +}; + +// ✅ Chain operations with proper error handling +const processUserData = (json: string): Result => + parseUser(json) + .flatMap(validateUser) + .flatMap(transformUser); +``` + +## React Guidelines + +### Component Design +```tsx +// ✅ Pure functional components +interface UserCardProps { + user: User; + onEdit: (user: User) => void; +} + +const UserCard: React.FC = ({ user, onEdit }) => ( +
+

{user.name}

+

{user.email}

+ +
+); + +// ✅ Custom hooks for stateful logic +const useUserData = (userId: string) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + fetchUser(userId) + .then(setUser) + .catch(err => setError(err.message)) + .finally(() => setLoading(false)); + }, [userId]); + + return { user, loading, error }; +}; +``` + +### State Management +```tsx +// ✅ Immutable state updates +const todoReducer = (state: TodoState, action: TodoAction): TodoState => { + switch (action.type) { + case 'ADD_TODO': + return { + ...state, + todos: [...state.todos, action.payload] + }; + case 'TOGGLE_TODO': + return { + ...state, + todos: state.todos.map(todo => + todo.id === action.payload.id + ? { ...todo, completed: !todo.completed } + : todo + ) + }; + default: + return state; + } +}; + +// ✅ Use React.useMemo for expensive computations +const ExpensiveComponent: React.FC<{ items: Item[] }> = ({ items }) => { + const expensiveValue = useMemo( + () => items.reduce((acc, item) => acc + item.complexCalculation(), 0), + [items] + ); + + return
{expensiveValue}
; +}; +``` + +### Event Handling +```tsx +// ✅ Curried event handlers for reusability +const createHandler = (action: string) => (id: string) => (event: React.MouseEvent) => { + event.preventDefault(); + dispatch({ type: action, payload: { id } }); +}; + +const handleEdit = createHandler('EDIT_ITEM'); +const handleDelete = createHandler('DELETE_ITEM'); + +// ✅ Compose event handlers +const withPreventDefault = (handler: () => void) => (event: React.MouseEvent) => { + event.preventDefault(); + handler(); +}; + +const handleSubmit = withPreventDefault(() => { + // Submit logic +}); +``` + +## Utility Functions + +### Common Patterns +```typescript +// ✅ Pipe function for composition +const pipe = (...fns: Array<(arg: T) => T>) => (value: T): T => + fns.reduce((acc, fn) => fn(acc), value); + +// ✅ Compose function (right-to-left) +const compose = (...fns: Array<(arg: T) => T>) => (value: T): T => + fns.reduceRight((acc, fn) => fn(acc), value); + +// ✅ Curry utility +const curry = (fn: (a: A, b: B) => C) => (a: A) => (b: B) => fn(a, b); + +// ✅ Maybe/Option type for null handling +class Maybe { + constructor(private value: T | null | undefined) {} + + static of(value: T | null | undefined): Maybe { + return new Maybe(value); + } + + map(fn: (value: T) => U): Maybe { + return this.value != null ? Maybe.of(fn(this.value)) : Maybe.of(null); + } + + flatMap(fn: (value: T) => Maybe): Maybe { + return this.value != null ? fn(this.value) : Maybe.of(null); + } + + getOrElse(defaultValue: T): T { + return this.value != null ? this.value : defaultValue; + } +} +``` + +### Array Utilities +```typescript +// ✅ Functional array utilities +const groupBy = ( + array: T[], + keyFn: (item: T) => K +): Record => + array.reduce((groups, item) => { + const key = keyFn(item); + return { ...groups, [key]: [...(groups[key] || []), item] }; + }, {} as Record); + +const unique = (array: T[]): T[] => [...new Set(array)]; + +const chunk = (array: T[], size: number): T[][] => + array.reduce((chunks, item, index) => { + const chunkIndex = Math.floor(index / size); + chunks[chunkIndex] = chunks[chunkIndex] || []; + chunks[chunkIndex].push(item); + return chunks; + }, [] as T[][]); +``` + + + +## Performance Considerations + +```typescript +// ✅ Memoize expensive computations +const expensiveCalculation = useMemo(() => { + return items.reduce((sum, item) => sum + item.value * item.multiplier, 0); +}, [items]); + +// ✅ Use React.memo for component memoization +const MemoizedUserCard = React.memo(({ user, onEdit }) => { + // Component logic +}, (prevProps, nextProps) => { + return prevProps.user.id === nextProps.user.id && + prevProps.user.updatedAt === nextProps.user.updatedAt; +}); + +// ✅ Optimize with useCallback for stable references +const handleUserEdit = useCallback((user: User) => { + dispatch({ type: 'EDIT_USER', payload: user }); +}, [dispatch]); +``` + +## Anti-Patterns to Avoid + +```typescript +// ❌ Mutating props or state directly +const Component = ({ items }) => { + items.push(newItem); // Don't mutate props + return
{items.length}
; +}; + +// ❌ Side effects in render +const Component = () => { + localStorage.setItem('key', 'value'); // Side effect in render + return
Component
; +}; + +// ❌ Mixing concerns in components +const UserComponent = () => { + // Don't mix data fetching, state management, and UI in one place + const [users, setUsers] = useState([]); + + useEffect(() => { + fetch('/api/users') // API call + .then(res => res.json()) + .then(data => { + const processedData = data.map(user => ({ // Data processing + ...user, + displayName: `${user.firstName} ${user.lastName}` + })); + setUsers(processedData); + }); + }, []); + + return ( +
+ {users.map(user => ( +
{user.displayName}
+ ))} +
+ ); +}; +``` + +## Additional Rules + +### Documentation +```typescript +/** + * Calculates the total price including tax for a list of items + * @param items - Array of items with price property + * @param taxRate - Tax rate as decimal (e.g., 0.1 for 10%) + * @returns Total price including tax + * @example + * const total = calculateTotalPrice([{price: 100}, {price: 200}], 0.1); + * // Returns 330 + */ +const calculateTotalPrice = (items: Item[], taxRate: number): number => + items.reduce((total, item) => total + item.price, 0) * (1 + taxRate); + +/** + * Custom hook for managing user data with loading and error states + * @param userId - The ID of the user to fetch + * @returns Object containing user data, loading state, and error state + */ +const useUserData = (userId: string) => { + // Hook implementation +}; +``` + +### Complex Logic Decomposition +```typescript +// ✅ Break complex logic into small, composable pure functions +const validateEmail = (email: string): boolean => + /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + +const validatePassword = (password: string): boolean => + password.length >= 8 && /[A-Z]/.test(password) && /[0-9]/.test(password); + +const validateRequired = (value: string): boolean => + value.trim().length > 0; + +// Compose validation functions +const validateUserInput = (user: UserInput): ValidationResult => { + const errors: string[] = []; + + if (!validateRequired(user.name)) errors.push('Name is required'); + if (!validateEmail(user.email)) errors.push('Invalid email format'); + if (!validatePassword(user.password)) errors.push('Password must be at least 8 characters with uppercase and number'); + + return { + isValid: errors.length === 0, + errors + }; +}; +``` + +### Immutable State Updates +```typescript +// ✅ Always return new objects/arrays for state changes +const updateUserProfile = (user: User, updates: Partial): User => ({ + ...user, + ...updates, + updatedAt: new Date().toISOString() +}); + +const addItemToList = (list: T[], item: T): T[] => [...list, item]; + +const updateItemInList = ( + list: T[], + predicate: (item: T) => boolean, + updates: Partial +): T[] => + list.map(item => predicate(item) ? { ...item, ...updates } : item); + +const removeItemFromList = ( + list: T[], + predicate: (item: T) => boolean +): T[] => + list.filter(item => !predicate(item)); +``` + +### Single Responsibility Components +```typescript +// ✅ Each component has a single, clear responsibility +const UserAvatar: React.FC<{ user: User; size: 'small' | 'medium' | 'large' }> = + ({ user, size }) => ( + {`${user.name}'s + ); + +const UserName: React.FC<{ user: User; showEmail?: boolean }> = + ({ user, showEmail = false }) => ( +
+

{user.name}

+ {showEmail &&

{user.email}

} +
+ ); + +const UserActions: React.FC<{ user: User; onEdit: (user: User) => void }> = + ({ user, onEdit }) => ( +
+ +
+ ); + +// Compose components together +const UserCard: React.FC<{ user: User; onEdit: (user: User) => void }> = + ({ user, onEdit }) => ( +
+ + + +
+ ); +``` + +### Side Effects Management +```typescript +// ✅ Isolate side effects in useEffect or service layers +const UserProfile: React.FC<{ userId: string }> = ({ userId }) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + // Side effects in useEffect + useEffect(() => { + const fetchUser = async () => { + try { + setLoading(true); + const userData = await userService.getUser(userId); + setUser(userData); + } catch (error) { + console.error('Failed to fetch user:', error); + } finally { + setLoading(false); + } + }; + + fetchUser(); + }, [userId]); + + if (loading) return ; + if (!user) return ; + + return ; +}; + +// ✅ Service layer for API calls and external interactions +class UserService { + /** + * Fetches user data from the API + * @param userId - The ID of the user to fetch + * @returns Promise resolving to user data + */ + async getUser(userId: string): Promise { + const response = await fetch(`/api/users/${userId}`); + if (!response.ok) { + throw new Error(`Failed to fetch user: ${response.statusText}`); + } + return response.json(); + } + + /** + * Updates user data via API + * @param userId - The ID of the user to update + * @param updates - Partial user data to update + * @returns Promise resolving to updated user data + */ + async updateUser(userId: string, updates: Partial): Promise { + const response = await fetch(`/api/users/${userId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updates) + }); + if (!response.ok) { + throw new Error(`Failed to update user: ${response.statusText}`); + } + return response.json(); + } +} + +const userService = new UserService(); +export { userService }; +``` \ No newline at end of file diff --git a/.cursor/rules/toss.mdc b/.cursor/rules/toss.mdc new file mode 100644 index 000000000..7208ceeee --- /dev/null +++ b/.cursor/rules/toss.mdc @@ -0,0 +1,693 @@ +--- +description: +globs: +alwaysApply: false +--- +# Frontend Design Guideline + +This document summarizes key frontend design principles and rules, showcasing +recommended patterns. Follow these guidelines when writing frontend code. + +# Readability + +Improving the clarity and ease of understanding code. + +## Naming Magic Numbers + +**Rule:** Replace magic numbers with named constants for clarity. + +**Reasoning:** + +- Improves clarity by giving semantic meaning to unexplained values. +- Enhances maintainability. + +#### Recommended Pattern: + +```typescript +const ANIMATION_DELAY_MS = 300; + +async function onLikeClick() { + await postLike(url); + await delay(ANIMATION_DELAY_MS); // Clearly indicates waiting for animation + await refetchPostLike(); +} +``` + +## Abstracting Implementation Details + +**Rule:** Abstract complex logic/interactions into dedicated components/HOCs. + +**Reasoning:** + +- Reduces cognitive load by separating concerns. +- Improves readability, testability, and maintainability of components. + +#### Recommended Pattern 1: Auth Guard + +(Login check abstracted to a wrapper/guard component) + +```tsx +// App structure +function App() { + return ( + + {" "} + {/* Wrapper handles auth check */} + + + ); +} + +// AuthGuard component encapsulates the check/redirect logic +function AuthGuard({ children }) { + const status = useCheckLoginStatus(); + useEffect(() => { + if (status === "LOGGED_IN") { + location.href = "/home"; + } + }, [status]); + + // Render children only if not logged in, otherwise render null (or loading) + return status !== "LOGGED_IN" ? children : null; +} + +// LoginStartPage is now simpler, focused only on login UI/logic +function LoginStartPage() { + // ... login related logic ONLY ... + return <>{/* ... login related components ... */}; +} +``` + +#### Recommended Pattern 2: Dedicated Interaction Component + +(Dialog logic abstracted into a dedicated `InviteButton` component) + +```tsx +export function FriendInvitation() { + const { data } = useQuery(/* ... */); + + return ( + <> + {/* Use the dedicated button component */} + + {/* ... other UI ... */} + + ); +} + +// InviteButton handles the confirmation flow internally +function InviteButton({ name }) { + const handleClick = async () => { + const canInvite = await overlay.openAsync(({ isOpen, close }) => ( + + )); + + if (canInvite) { + await sendPush(); + } + }; + + return ; +} +``` + +## Separating Code Paths for Conditional Rendering + +**Rule:** Separate significantly different conditional UI/logic into distinct +components. + +**Reasoning:** + +- Improves readability by avoiding complex conditionals within one component. +- Ensures each specialized component has a clear, single responsibility. + +#### Recommended Pattern: + +(Separate components for each role) + +```tsx +function SubmitButton() { + const isViewer = useRole() === "viewer"; + + // Delegate rendering to specialized components + return isViewer ? : ; +} + +// Component specifically for the 'viewer' role +function ViewerSubmitButton() { + return Submit; +} + +// Component specifically for the 'admin' (or non-viewer) role +function AdminSubmitButton() { + useEffect(() => { + showAnimation(); // Animation logic isolated here + }, []); + + return ; +} +``` + +## Simplifying Complex Ternary Operators + +**Rule:** Replace complex/nested ternaries with `if`/`else` or IIFEs for +readability. + +**Reasoning:** + +- Makes conditional logic easier to follow quickly. +- Improves overall code maintainability. + +#### Recommended Pattern: + +(Using an IIFE with `if` statements) + +```typescript +const status = (() => { + if (ACondition && BCondition) return "BOTH"; + if (ACondition) return "A"; + if (BCondition) return "B"; + return "NONE"; +})(); +``` + +## Reducing Eye Movement (Colocating Simple Logic) + +**Rule:** Colocate simple, localized logic or use inline definitions to reduce +context switching. + +**Reasoning:** + +- Allows top-to-bottom reading and faster comprehension. +- Reduces cognitive load from context switching (eye movement). + +#### Recommended Pattern A: Inline `switch` + +```tsx +function Page() { + const user = useUser(); + + // Logic is directly visible here + switch (user.role) { + case "admin": + return ( +
+ + +
+ ); + case "viewer": + return ( +
+ {/* Example for viewer */} + +
+ ); + default: + return null; + } +} +``` + +#### Recommended Pattern B: Colocated simple policy object + +```tsx +function Page() { + const user = useUser(); + // Simple policy defined right here, easy to see + const policy = { + admin: { canInvite: true, canView: true }, + viewer: { canInvite: false, canView: true }, + }[user.role]; + + // Ensure policy exists before accessing properties if role might not match + if (!policy) return null; + + return ( +
+ + +
+ ); +} +``` + +## Naming Complex Conditions + +**Rule:** Assign complex boolean conditions to named variables. + +**Reasoning:** + +- Makes the _meaning_ of the condition explicit. +- Improves readability and self-documentation by reducing cognitive load. + +#### Recommended Pattern: + +(Conditions assigned to named variables) + +```typescript +const matchedProducts = products.filter((product) => { + // Check if product belongs to the target category + const isSameCategory = product.categories.some( + (category) => category.id === targetCategory.id + ); + + // Check if any product price falls within the desired range + const isPriceInRange = product.prices.some( + (price) => price >= minPrice && price <= maxPrice + ); + + // The overall condition is now much clearer + return isSameCategory && isPriceInRange; +}); +``` + +**Guidance:** Name conditions when the logic is complex, reused, or needs unit +testing. Avoid naming very simple, single-use conditions. + +# Predictability + +Ensuring code behaves as expected based on its name, parameters, and context. + +## Standardizing Return Types + +**Rule:** Use consistent return types for similar functions/hooks. + +**Reasoning:** + +- Improves code predictability; developers can anticipate return value shapes. +- Reduces confusion and potential errors from inconsistent types. + +#### Recommended Pattern 1: API Hooks (React Query) + +```typescript +// Always return the Query object +import { useQuery, UseQueryResult } from "@tanstack/react-query"; + +// Assuming fetchUser returns Promise +function useUser(): UseQueryResult { + const query = useQuery({ queryKey: ["user"], queryFn: fetchUser }); + return query; +} + +// Assuming fetchServerTime returns Promise +function useServerTime(): UseQueryResult { + const query = useQuery({ + queryKey: ["serverTime"], + queryFn: fetchServerTime, + }); + return query; +} +``` + +#### Recommended Pattern 2: Validation Functions + +(Using a consistent type, ideally a Discriminated Union) + +```typescript +type ValidationResult = { ok: true } | { ok: false; reason: string }; + +function checkIsNameValid(name: string): ValidationResult { + if (name.length === 0) return { ok: false, reason: "Name cannot be empty." }; + if (name.length >= 20) + return { ok: false, reason: "Name cannot be longer than 20 characters." }; + return { ok: true }; +} + +function checkIsAgeValid(age: number): ValidationResult { + if (!Number.isInteger(age)) + return { ok: false, reason: "Age must be an integer." }; + if (age < 18) return { ok: false, reason: "Age must be 18 or older." }; + if (age > 99) return { ok: false, reason: "Age must be 99 or younger." }; + return { ok: true }; +} + +// Usage allows safe access to 'reason' only when ok is false +const nameValidation = checkIsNameValid(name); +if (!nameValidation.ok) { + console.error(nameValidation.reason); +} +``` + +## Revealing Hidden Logic (Single Responsibility) + +**Rule:** Avoid hidden side effects; functions should only perform actions +implied by their signature (SRP). + +**Reasoning:** + +- Leads to predictable behavior without unintended side effects. +- Creates more robust, testable code through separation of concerns (SRP). + +#### Recommended Pattern: + +```typescript +// Function *only* fetches balance +async function fetchBalance(): Promise { + const balance = await http.get("..."); + return balance; +} + +// Caller explicitly performs logging where needed +async function handleUpdateClick() { + const balance = await fetchBalance(); // Fetch + logging.log("balance_fetched"); // Log (explicit action) + await syncBalance(balance); // Another action +} +``` + +## Using Unique and Descriptive Names (Avoiding Ambiguity) + +**Rule:** Use unique, descriptive names for custom wrappers/functions to avoid +ambiguity. + +**Reasoning:** + +- Avoids ambiguity and enhances predictability. +- Allows developers to understand specific actions (e.g., adding auth) directly + from the name. + +#### Recommended Pattern: + +```typescript +// In httpService.ts - Clearer module name +import { http as httpLibrary } from "@some-library/http"; + +export const httpService = { + // Unique module name + async getWithAuth(url: string) { + // Descriptive function name + const token = await fetchToken(); + return httpLibrary.get(url, { + headers: { Authorization: `Bearer ${token}` }, + }); + }, +}; + +// In fetchUser.ts - Usage clearly indicates auth +import { httpService } from "./httpService"; +export async function fetchUser() { + // Name 'getWithAuth' makes the behavior explicit + return await httpService.getWithAuth("..."); +} +``` + +# Cohesion + +Keeping related code together and ensuring modules have a well-defined, single +purpose. + +## Considering Form Cohesion + +**Rule:** Choose field-level or form-level cohesion based on form requirements. + +**Reasoning:** + +- Balances field independence (field-level) vs. form unity (form-level). +- Ensures related form logic is appropriately grouped based on requirements. + +#### Recommended Pattern (Field-Level Example): + +```tsx +// Each field uses its own `validate` function +import { useForm } from "react-hook-form"; + +export function Form() { + const { + register, + formState: { errors }, + handleSubmit, + } = useForm({ + /* defaultValues etc. */ + }); + + const onSubmit = handleSubmit((formData) => { + console.log("Form submitted:", formData); + }); + + return ( +
+
+ + value.trim() === "" ? "Please enter your name." : true, // Example validation + })} + placeholder="Name" + /> + {errors.name &&

{errors.name.message}

} +
+
+ + /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value) + ? true + : "Invalid email address.", // Example validation + })} + placeholder="Email" + /> + {errors.email &&

{errors.email.message}

} +
+ +
+ ); +} +``` + +#### Recommended Pattern (Form-Level Example): + +```tsx +// A single schema defines validation for the whole form +import * as z from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; + +const schema = z.object({ + name: z.string().min(1, "Please enter your name."), + email: z.string().min(1, "Please enter your email.").email("Invalid email."), +}); + +export function Form() { + const { + register, + formState: { errors }, + handleSubmit, + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { name: "", email: "" }, + }); + + const onSubmit = handleSubmit((formData) => { + console.log("Form submitted:", formData); + }); + + return ( +
+
+ + {errors.name &&

{errors.name.message}

} +
+
+ + {errors.email &&

{errors.email.message}

} +
+ +
+ ); +} +``` + +**Guidance:** Choose **field-level** for independent validation, async checks, +or reusable fields. Choose **form-level** for related fields, wizard forms, or +interdependent validation. + +## Organizing Code by Feature/Domain + +**Rule:** Organize directories by feature/domain, not just by code type. + +**Reasoning:** + +- Increases cohesion by keeping related files together. +- Simplifies feature understanding, development, maintenance, and deletion. + +#### Recommended Pattern: + +(Organized by feature/domain) + +``` +src/ +├── components/ # Shared/common components +├── hooks/ # Shared/common hooks +├── utils/ # Shared/common utils +├── domains/ +│ ├── user/ +│ │ ├── components/ +│ │ │ └── UserProfileCard.tsx +│ │ ├── hooks/ +│ │ │ └── useUser.ts +│ │ └── index.ts # Optional barrel file +│ ├── product/ +│ │ ├── components/ +│ │ │ └── ProductList.tsx +│ │ ├── hooks/ +│ │ │ └── useProducts.ts +│ │ └── ... +│ └── order/ +│ ├── components/ +│ │ └── OrderSummary.tsx +│ ├── hooks/ +│ │ └── useOrder.ts +│ └── ... +└── App.tsx +``` + +## Relating Magic Numbers to Logic + +**Rule:** Define constants near related logic or ensure names link them clearly. + +**Reasoning:** + +- Improves cohesion by linking constants to the logic they represent. +- Prevents silent failures caused by updating logic without updating related + constants. + +#### Recommended Pattern: + +```typescript +// Constant clearly named and potentially defined near animation logic +const ANIMATION_DELAY_MS = 300; + +async function onLikeClick() { + await postLike(url); + // Delay uses the constant, maintaining the link to the animation + await delay(ANIMATION_DELAY_MS); + await refetchPostLike(); +} +``` + +_Ensure constants are maintained alongside the logic they depend on or clearly +named to show the relationship._ + +# Coupling + +Minimizing dependencies between different parts of the codebase. + +## Balancing Abstraction and Coupling (Avoiding Premature Abstraction) + +**Rule:** Avoid premature abstraction of duplicates if use cases might diverge; +prefer lower coupling. + +**Reasoning:** + +- Avoids tight coupling from forcing potentially diverging logic into one + abstraction. +- Allowing some duplication can improve decoupling and maintainability when + future needs are uncertain. + +#### Guidance: + +Before abstracting, consider if the logic is truly identical and likely to +_stay_ identical across all use cases. If divergence is possible (e.g., +different pages needing slightly different behavior from a shared hook like +`useOpenMaintenanceBottomSheet`), keeping the logic separate initially (allowing +duplication) can lead to more maintainable, decoupled code. Discuss trade-offs +with the team. _[No specific 'good' code example here, as the recommendation is +situational awareness rather than a single pattern]._ + +## Scoping State Management (Avoiding Overly Broad Hooks) + +**Rule:** Break down broad state management into smaller, focused +hooks/contexts. + +**Reasoning:** + +- Reduces coupling by ensuring components only depend on necessary state slices. +- Improves performance by preventing unnecessary re-renders from unrelated state + changes. + +#### Recommended Pattern: + +(Focused hooks, low coupling) + +```typescript +// Hook specifically for cardId query param +import { useQueryParam, NumberParam } from "use-query-params"; +import { useCallback } from "react"; + +export function useCardIdQueryParam() { + // Assuming 'query' provides the raw param value + const [cardIdParam, setCardIdParam] = useQueryParam("cardId", NumberParam); + + const setCardId = useCallback( + (newCardId: number | undefined) => { + setCardIdParam(newCardId, "replaceIn"); // Or 'push' depending on desired history behavior + }, + [setCardIdParam] + ); + + // Provide a stable return tuple + return [cardIdParam ?? undefined, setCardId] as const; +} + +// Separate hook for date range, etc. +// export function useDateRangeQueryParam() { /* ... */ } +``` + +Components now only import and use `useCardIdQueryParam` if they need `cardId`, +decoupling them from date range state, etc. + +## Eliminating Props Drilling with Composition + +**Rule:** Use Component Composition instead of Props Drilling. + +**Reasoning:** + +- Significantly reduces coupling by eliminating unnecessary intermediate + dependencies. +- Makes refactoring easier and clarifies data flow in flatter component trees. + +#### Recommended Pattern: + +```tsx +import React, { useState } from "react"; + +// Assume Modal, Input, Button, ItemEditList components exist + +function ItemEditModal({ open, items, recommendedItems, onConfirm, onClose }) { + const [keyword, setKeyword] = useState(""); + + // Render children directly within Modal, passing props only where needed + return ( + + {/* Input and Button rendered directly */} +
+ setKeyword(e.target.value)} // State managed here + placeholder="Search items..." + /> + +
+ {/* ItemEditList rendered directly, gets props it needs */} + +
+ ); +} + +// The intermediate ItemEditBody component is eliminated, reducing coupling. +``` \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..bf3f19c1f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +node_modules +dist +build +.next +coverage +*.min.js +pnpm-lock.yaml +package-lock.json diff --git a/.prettierrc b/.prettierrc index d9ae6b1fb..eda681137 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,8 +2,7 @@ "semi": false, "printWidth": 120, "tabWidth": 2, - "singleQuote": false, + "singleQuote": true, "quoteProps": "consistent", - "trailingComma": "all", - "singleAttributePerLine": false -} \ No newline at end of file + "trailingComma": "all" +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..d7df89c9c --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"] +} diff --git a/eslint.config.js b/eslint.config.js index 092408a9f..51a8c25a7 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,6 +3,8 @@ import globals from 'globals' import reactHooks from 'eslint-plugin-react-hooks' import reactRefresh from 'eslint-plugin-react-refresh' import tseslint from 'typescript-eslint' +import eslintConfigPrettier from 'eslint-config-prettier' +import fsdPlugin from 'eslint-plugin-fsd-lint' export default tseslint.config( { ignores: ['dist'] }, @@ -16,13 +18,16 @@ export default tseslint.config( plugins: { 'react-hooks': reactHooks, 'react-refresh': reactRefresh, + 'fsd': fsdPlugin, }, rules: { ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + 'fsd/forbidden-imports': 'error', + 'fsd/no-cross-slice-dependency': 'error', + 'fsd/no-ui-in-business-logic': 'error', + 'fsd/ordered-imports': 'warn', }, }, + eslintConfigPrettier, ) diff --git a/package.json b/package.json index e014c5272..cb74dc029 100644 --- a/package.json +++ b/package.json @@ -6,28 +6,38 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", + "postbuild": "cp dist/index.html dist/404.html", "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,md}\"", + "format:check": "prettier --check \"src/**/*.{js,jsx,ts,tsx,json,css,md}\"", "preview": "vite preview", - "test": "vitest", - "coverage": "vitest run --coverage" + "predeploy": "pnpm run build", + "deploy": "npx gh-pages -d dist" }, "dependencies": { + "@tanstack/react-query": "^5.85.0", "react": "^19.1.1", - "react-dom": "^19.1.1" + "react-dom": "^19.1.1", + "zustand": "^5.0.7" }, "devDependencies": { "@eslint/js": "^9.33.0", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-select": "^2.2.5", + "@tanstack/react-query-devtools": "^5.85.1", "@testing-library/jest-dom": "^6.6.4", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", + "@types/node": "^24.2.1", "@types/react": "^19.1.9", "@types/react-dom": "^19.1.7", "@vitejs/plugin-react": "^5.0.0", "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "eslint": "^9.33.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-fsd-lint": "^1.0.9", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6b2a40d18..78a6e7666 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,18 @@ importers: .: dependencies: + '@tanstack/react-query': + specifier: ^5.85.0 + version: 5.85.0(react@19.1.1) react: specifier: ^19.1.1 version: 19.1.1 react-dom: specifier: ^19.1.1 version: 19.1.1(react@19.1.1) + zustand: + specifier: ^5.0.7 + version: 5.0.7(@types/react@19.1.9)(react@19.1.1) devDependencies: '@eslint/js': specifier: ^9.33.0 @@ -24,6 +30,9 @@ importers: '@radix-ui/react-select': specifier: ^2.2.5 version: 2.2.5(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@tanstack/react-query-devtools': + specifier: ^5.85.1 + version: 5.85.1(@tanstack/react-query@5.85.0(react@19.1.1))(react@19.1.1) '@testing-library/jest-dom': specifier: ^6.6.4 version: 6.6.4 @@ -33,6 +42,9 @@ importers: '@testing-library/user-event': specifier: ^14.6.1 version: 14.6.1(@testing-library/dom@10.4.0) + '@types/node': + specifier: ^24.2.1 + version: 24.2.1 '@types/react': specifier: ^19.1.9 version: 19.1.9 @@ -41,7 +53,7 @@ importers: version: 19.1.7(@types/react@19.1.9) '@vitejs/plugin-react': specifier: ^5.0.0 - version: 5.0.0(vite@7.1.1(@types/node@22.8.1)) + version: 5.0.0(vite@7.1.1(@types/node@24.2.1)) axios: specifier: ^1.11.0 version: 1.11.0 @@ -51,6 +63,12 @@ importers: eslint: specifier: ^9.33.0 version: 9.33.0 + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@9.33.0) + eslint-plugin-fsd-lint: + specifier: ^1.0.9 + version: 1.0.9(eslint@9.33.0) eslint-plugin-react-hooks: specifier: ^5.2.0 version: 5.2.0(eslint@9.33.0) @@ -68,7 +86,7 @@ importers: version: 0.539.0(react@19.1.1) msw: specifier: ^2.10.4 - version: 2.10.4(@types/node@22.8.1)(typescript@5.9.2) + version: 2.10.4(@types/node@24.2.1)(typescript@5.9.2) prettier: specifier: ^3.6.2 version: 3.6.2 @@ -83,10 +101,10 @@ importers: version: 8.39.0(eslint@9.33.0)(typescript@5.9.2) vite: specifier: ^7.1.1 - version: 7.1.1(@types/node@22.8.1) + version: 7.1.1(@types/node@24.2.1) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.8.1)(@vitest/browser@2.1.3)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.8.1)(typescript@5.9.2)) + version: 3.2.4(@types/node@24.2.1)(@vitest/browser@2.1.3)(jsdom@26.1.0)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2)) vitest-browser-react: specifier: ^1.0.1 version: 1.0.1(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(@vitest/browser@2.1.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(vitest@3.2.4) @@ -103,10 +121,6 @@ packages: '@asamuzakjp/css-color@2.8.3': resolution: {integrity: sha512-GIc76d9UI1hCvOATjZPyHFmE5qhRccp3/zGfMPapK3jBi+yocEzp6BBB0UnfRYP9NP4FANqUZYb0hnfs3TM3hw==} - '@babel/code-frame@7.26.2': - resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} - engines: {node: '>=6.9.0'} - '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -930,6 +944,23 @@ packages: cpu: [x64] os: [win32] + '@tanstack/query-core@5.83.1': + resolution: {integrity: sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q==} + + '@tanstack/query-devtools@5.84.0': + resolution: {integrity: sha512-fbF3n+z1rqhvd9EoGp5knHkv3p5B2Zml1yNRjh7sNXklngYI5RVIWUrUjZ1RIcEoscarUb0+bOvIs5x9dwzOXQ==} + + '@tanstack/react-query-devtools@5.85.1': + resolution: {integrity: sha512-sn1l10BTvXeu93pUi3bWv298AvU+yTmhi6LtXF5XyvVtIP420FqsO5sn5sQvRnJCwHEpMQrE23sQuvDN185qeQ==} + peerDependencies: + '@tanstack/react-query': ^5.85.1 + react: ^18 || ^19 + + '@tanstack/react-query@5.85.0': + resolution: {integrity: sha512-t1HMfToVMGfwEJRya6GG7gbK0luZJd+9IySFNePL1BforU1F3LqQ3tBC2Rpvr88bOrlU6PXyMLgJD0Yzn4ztUw==} + peerDependencies: + react: ^18 || ^19 + '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} @@ -992,8 +1023,8 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@22.8.1': - resolution: {integrity: sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg==} + '@types/node@24.2.1': + resolution: {integrity: sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==} '@types/react-dom@19.1.7': resolution: {integrity: sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==} @@ -1383,6 +1414,17 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-fsd-lint@1.0.9: + resolution: {integrity: sha512-D5Rh40tX9oqD63uD4RfYK6ZfbsMOGEstTC/nbRWmE5S4AK+WB2dibISyw9LQ5BIoP77yew/SmJ7fZ1iIH5IneA==} + peerDependencies: + eslint: '>=9.0.0' + eslint-plugin-react-hooks@5.2.0: resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} engines: {node: '>=10'} @@ -2133,8 +2175,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici-types@7.10.0: + resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} universalify@0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} @@ -2345,6 +2387,24 @@ packages: resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} engines: {node: '>=18'} + zustand@5.0.7: + resolution: {integrity: sha512-Ot6uqHDW/O2VdYsKLLU8GQu8sCOM1LcoE8RwvLv9uuRT9s6SOHCKs0ZEOhxg+I1Ld+A1Q5lwx+UlKXXUoCZITg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@adobe/css-tools@4.4.0': {} @@ -2362,12 +2422,6 @@ snapshots: '@csstools/css-tokenizer': 3.0.3 lru-cache: 10.4.3 - '@babel/code-frame@7.26.2': - dependencies: - '@babel/helper-validator-identifier': 7.25.9 - js-tokens: 4.0.0 - picocolors: 1.1.1 - '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -2684,16 +2738,16 @@ snapshots: '@humanwhocodes/retry@0.4.2': {} - '@inquirer/confirm@5.0.1(@types/node@22.8.1)': + '@inquirer/confirm@5.0.1(@types/node@24.2.1)': dependencies: - '@inquirer/core': 10.0.1(@types/node@22.8.1) - '@inquirer/type': 3.0.0(@types/node@22.8.1) - '@types/node': 22.8.1 + '@inquirer/core': 10.0.1(@types/node@24.2.1) + '@inquirer/type': 3.0.0(@types/node@24.2.1) + '@types/node': 24.2.1 - '@inquirer/core@10.0.1(@types/node@22.8.1)': + '@inquirer/core@10.0.1(@types/node@24.2.1)': dependencies: '@inquirer/figures': 1.0.7 - '@inquirer/type': 3.0.0(@types/node@22.8.1) + '@inquirer/type': 3.0.0(@types/node@24.2.1) ansi-escapes: 4.3.2 cli-width: 4.1.0 mute-stream: 2.0.0 @@ -2706,9 +2760,9 @@ snapshots: '@inquirer/figures@1.0.7': {} - '@inquirer/type@3.0.0(@types/node@22.8.1)': + '@inquirer/type@3.0.0(@types/node@24.2.1)': dependencies: - '@types/node': 22.8.1 + '@types/node': 24.2.1 '@jridgewell/gen-mapping@0.3.12': dependencies: @@ -3081,9 +3135,24 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.46.2': optional: true + '@tanstack/query-core@5.83.1': {} + + '@tanstack/query-devtools@5.84.0': {} + + '@tanstack/react-query-devtools@5.85.1(@tanstack/react-query@5.85.0(react@19.1.1))(react@19.1.1)': + dependencies: + '@tanstack/query-devtools': 5.84.0 + '@tanstack/react-query': 5.85.0(react@19.1.1) + react: 19.1.1 + + '@tanstack/react-query@5.85.0(react@19.1.1)': + dependencies: + '@tanstack/query-core': 5.83.1 + react: 19.1.1 + '@testing-library/dom@10.4.0': dependencies: - '@babel/code-frame': 7.26.2 + '@babel/code-frame': 7.27.1 '@babel/runtime': 7.26.0 '@types/aria-query': 5.0.4 aria-query: 5.3.0 @@ -3153,9 +3222,9 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/node@22.8.1': + '@types/node@24.2.1': dependencies: - undici-types: 6.19.8 + undici-types: 7.10.0 '@types/react-dom@19.1.7(@types/react@19.1.9)': dependencies: @@ -3262,7 +3331,7 @@ snapshots: '@typescript-eslint/types': 8.39.0 eslint-visitor-keys: 4.2.1 - '@vitejs/plugin-react@5.0.0(vite@7.1.1(@types/node@22.8.1))': + '@vitejs/plugin-react@5.0.0(vite@7.1.1(@types/node@24.2.1))': dependencies: '@babel/core': 7.28.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) @@ -3270,21 +3339,21 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.30 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.1.1(@types/node@22.8.1) + vite: 7.1.1(@types/node@24.2.1) transitivePeerDependencies: - supports-color - '@vitest/browser@2.1.3(@types/node@22.8.1)(@vitest/spy@3.2.4)(typescript@5.9.2)(vite@7.1.1(@types/node@22.8.1))(vitest@3.2.4)': + '@vitest/browser@2.1.3(@types/node@24.2.1)(@vitest/spy@3.2.4)(typescript@5.9.2)(vite@7.1.1(@types/node@24.2.1))(vitest@3.2.4)': dependencies: '@testing-library/dom': 10.4.0 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0) - '@vitest/mocker': 2.1.3(@vitest/spy@3.2.4)(msw@2.10.4(@types/node@22.8.1)(typescript@5.9.2))(vite@7.1.1(@types/node@22.8.1)) + '@vitest/mocker': 2.1.3(@vitest/spy@3.2.4)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(vite@7.1.1(@types/node@24.2.1)) '@vitest/utils': 2.1.3 magic-string: 0.30.17 - msw: 2.10.4(@types/node@22.8.1)(typescript@5.9.2) + msw: 2.10.4(@types/node@24.2.1)(typescript@5.9.2) sirv: 2.0.4 tinyrainbow: 1.2.0 - vitest: 3.2.4(@types/node@22.8.1)(@vitest/browser@2.1.3)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.8.1)(typescript@5.9.2)) + vitest: 3.2.4(@types/node@24.2.1)(@vitest/browser@2.1.3)(jsdom@26.1.0)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2)) ws: 8.18.0 transitivePeerDependencies: - '@types/node' @@ -3302,23 +3371,23 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@2.1.3(@vitest/spy@3.2.4)(msw@2.10.4(@types/node@22.8.1)(typescript@5.9.2))(vite@7.1.1(@types/node@22.8.1))': + '@vitest/mocker@2.1.3(@vitest/spy@3.2.4)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(vite@7.1.1(@types/node@24.2.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - msw: 2.10.4(@types/node@22.8.1)(typescript@5.9.2) - vite: 7.1.1(@types/node@22.8.1) + msw: 2.10.4(@types/node@24.2.1)(typescript@5.9.2) + vite: 7.1.1(@types/node@24.2.1) - '@vitest/mocker@3.2.4(msw@2.10.4(@types/node@22.8.1)(typescript@5.9.2))(vite@7.1.1(@types/node@22.8.1))': + '@vitest/mocker@3.2.4(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(vite@7.1.1(@types/node@24.2.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - msw: 2.10.4(@types/node@22.8.1)(typescript@5.9.2) - vite: 7.1.1(@types/node@22.8.1) + msw: 2.10.4(@types/node@24.2.1)(typescript@5.9.2) + vite: 7.1.1(@types/node@24.2.1) '@vitest/pretty-format@2.1.3': dependencies: @@ -3347,7 +3416,7 @@ snapshots: '@vitest/utils@2.1.3': dependencies: '@vitest/pretty-format': 2.1.3 - loupe: 3.1.3 + loupe: 3.2.0 tinyrainbow: 1.2.0 '@vitest/utils@3.2.4': @@ -3590,6 +3659,14 @@ snapshots: escape-string-regexp@4.0.0: {} + eslint-config-prettier@10.1.8(eslint@9.33.0): + dependencies: + eslint: 9.33.0 + + eslint-plugin-fsd-lint@1.0.9(eslint@9.33.0): + dependencies: + eslint: 9.33.0 + eslint-plugin-react-hooks@5.2.0(eslint@9.33.0): dependencies: eslint: 9.33.0 @@ -3946,12 +4023,12 @@ snapshots: ms@2.1.3: {} - msw@2.10.4(@types/node@22.8.1)(typescript@5.9.2): + msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2): dependencies: '@bundled-es-modules/cookie': 2.0.1 '@bundled-es-modules/statuses': 1.0.1 '@bundled-es-modules/tough-cookie': 0.1.6 - '@inquirer/confirm': 5.0.1(@types/node@22.8.1) + '@inquirer/confirm': 5.0.1(@types/node@24.2.1) '@mswjs/interceptors': 0.39.5 '@open-draft/deferred-promise': 2.2.0 '@open-draft/until': 2.1.0 @@ -4288,7 +4365,7 @@ snapshots: typescript@5.9.2: {} - undici-types@6.19.8: {} + undici-types@7.10.0: {} universalify@0.2.0: {} @@ -4322,13 +4399,13 @@ snapshots: optionalDependencies: '@types/react': 19.1.9 - vite-node@3.2.4(@types/node@22.8.1): + vite-node@3.2.4(@types/node@24.2.1): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.1.1(@types/node@22.8.1) + vite: 7.1.1(@types/node@24.2.1) transitivePeerDependencies: - '@types/node' - jiti @@ -4343,7 +4420,7 @@ snapshots: - tsx - yaml - vite@7.1.1(@types/node@22.8.1): + vite@7.1.1(@types/node@24.2.1): dependencies: esbuild: 0.25.3 fdir: 6.4.6(picomatch@4.0.3) @@ -4352,24 +4429,24 @@ snapshots: rollup: 4.46.2 tinyglobby: 0.2.14 optionalDependencies: - '@types/node': 22.8.1 + '@types/node': 24.2.1 fsevents: 2.3.3 vitest-browser-react@1.0.1(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(@vitest/browser@2.1.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(vitest@3.2.4): dependencies: - '@vitest/browser': 2.1.3(@types/node@22.8.1)(@vitest/spy@3.2.4)(typescript@5.9.2)(vite@7.1.1(@types/node@22.8.1))(vitest@3.2.4) + '@vitest/browser': 2.1.3(@types/node@24.2.1)(@vitest/spy@3.2.4)(typescript@5.9.2)(vite@7.1.1(@types/node@24.2.1))(vitest@3.2.4) react: 19.1.1 react-dom: 19.1.1(react@19.1.1) - vitest: 3.2.4(@types/node@22.8.1)(@vitest/browser@2.1.3)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.8.1)(typescript@5.9.2)) + vitest: 3.2.4(@types/node@24.2.1)(@vitest/browser@2.1.3)(jsdom@26.1.0)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2)) optionalDependencies: '@types/react': 19.1.9 '@types/react-dom': 19.1.7(@types/react@19.1.9) - vitest@3.2.4(@types/node@22.8.1)(@vitest/browser@2.1.3)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.8.1)(typescript@5.9.2)): + vitest@3.2.4(@types/node@24.2.1)(@vitest/browser@2.1.3)(jsdom@26.1.0)(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2)): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.10.4(@types/node@22.8.1)(typescript@5.9.2))(vite@7.1.1(@types/node@22.8.1)) + '@vitest/mocker': 3.2.4(msw@2.10.4(@types/node@24.2.1)(typescript@5.9.2))(vite@7.1.1(@types/node@24.2.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -4387,12 +4464,12 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.1(@types/node@22.8.1) - vite-node: 3.2.4(@types/node@22.8.1) + vite: 7.1.1(@types/node@24.2.1) + vite-node: 3.2.4(@types/node@24.2.1) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.8.1 - '@vitest/browser': 2.1.3(@types/node@22.8.1)(@vitest/spy@3.2.4)(typescript@5.9.2)(vite@7.1.1(@types/node@22.8.1))(vitest@3.2.4) + '@types/node': 24.2.1 + '@vitest/browser': 2.1.3(@types/node@24.2.1)(@vitest/spy@3.2.4)(typescript@5.9.2)(vite@7.1.1(@types/node@24.2.1))(vitest@3.2.4) jsdom: 26.1.0 transitivePeerDependencies: - jiti @@ -4473,3 +4550,8 @@ snapshots: yocto-queue@0.1.0: {} yoctocolors-cjs@2.1.2: {} + + zustand@5.0.7(@types/react@19.1.9)(react@19.1.1): + optionalDependencies: + '@types/react': 19.1.9 + react: 19.1.1 diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index 0c0032aab..000000000 --- a/src/App.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { BrowserRouter as Router } from "react-router-dom" -import Header from "./components/Header.tsx" -import Footer from "./components/Footer.tsx" -import PostsManagerPage from "./pages/PostsManagerPage.tsx" - -const App = () => { - return ( - -
-
-
- -
-
-
-
- ) -} - -export default App diff --git a/src/app/index.tsx b/src/app/index.tsx new file mode 100644 index 000000000..a51c6c7d8 --- /dev/null +++ b/src/app/index.tsx @@ -0,0 +1,12 @@ +import AppLayout from './layouts/AppLayout.tsx' +import { AppProviders } from './providers/index.tsx' + +export function App() { + return ( + + + + ) +} + +export default App diff --git a/src/app/layouts/AppLayout.tsx b/src/app/layouts/AppLayout.tsx new file mode 100644 index 000000000..82886b6f4 --- /dev/null +++ b/src/app/layouts/AppLayout.tsx @@ -0,0 +1,17 @@ +import Header from './Header.tsx' +import Footer from './Footer.tsx' +import PostsManagerPage from '@pages/PostsManagerPage' + +const App = () => { + return ( +
+
+
+ +
+
+
+ ) +} + +export default App diff --git a/src/app/layouts/Footer.tsx b/src/app/layouts/Footer.tsx new file mode 100644 index 000000000..27522c768 --- /dev/null +++ b/src/app/layouts/Footer.tsx @@ -0,0 +1,13 @@ +import React from 'react' + +const Footer: React.FC = () => { + return ( +
+
+

© 2023 Post Management System. All rights reserved.

+
+
+ ) +} + +export default Footer diff --git a/src/app/layouts/Header.tsx b/src/app/layouts/Header.tsx new file mode 100644 index 000000000..9c840ca4c --- /dev/null +++ b/src/app/layouts/Header.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import { MessageSquare } from 'lucide-react' + +const Header: React.FC = () => { + return ( +
+
+
+ +

게시물 관리 시스템

+
+ +
+
+ ) +} + +export default Header diff --git a/src/app/providers/index.tsx b/src/app/providers/index.tsx new file mode 100644 index 000000000..877510746 --- /dev/null +++ b/src/app/providers/index.tsx @@ -0,0 +1,33 @@ +import { BrowserRouter as Router } from 'react-router-dom' +import { QueryClient, QueryClientProvider, QueryCache, MutationCache } from '@tanstack/react-query' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { ReactNode } from 'react' +import { shouldRetry, handleQueryError, handleMutationError } from '@shared/lib/error-handler' + +const queryClient = new QueryClient({ + queryCache: new QueryCache({ + onError: handleQueryError, + }), + mutationCache: new MutationCache({ + onError: handleMutationError, + }), + defaultOptions: { + queries: { + retry: shouldRetry, + refetchOnWindowFocus: false, + }, + }, +}) + +interface AppProvidersProps { + children: ReactNode +} + +export const AppProviders = ({ children }: AppProvidersProps) => { + return ( + + {children} + + + ) +} diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9bb..000000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx deleted file mode 100644 index 91af02f8c..000000000 --- a/src/components/Footer.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; - -const Footer: React.FC = () => { - return ( -
-
-

© 2023 Post Management System. All rights reserved.

-
-
- ); -}; - -export default Footer; diff --git a/src/components/Header.tsx b/src/components/Header.tsx deleted file mode 100644 index 63ecec168..000000000 --- a/src/components/Header.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { MessageSquare } from 'lucide-react'; - -const Header: React.FC = () => { - return ( -
-
-
- -

게시물 관리 시스템

-
- -
-
- ); -}; - -export default Header; - diff --git a/src/components/index.tsx b/src/components/index.tsx deleted file mode 100644 index 8495817d3..000000000 --- a/src/components/index.tsx +++ /dev/null @@ -1,214 +0,0 @@ -import * as React from "react" -import { forwardRef } from "react" -import * as SelectPrimitive from "@radix-ui/react-select" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { Check, ChevronDown, X } from "lucide-react" -import { cva, VariantProps } from "class-variance-authority" - -const buttonVariants = cva( - "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background", - { - variants: { - variant: { - default: "bg-blue-500 text-white hover:bg-blue-600", - destructive: "bg-red-500 text-white hover:bg-red-600", - outline: "border border-gray-300 bg-transparent text-gray-700 hover:bg-gray-100", - secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300", - ghost: "bg-transparent text-gray-700 hover:bg-gray-100", - link: "underline-offset-4 hover:underline text-blue-500", - }, - size: { - default: "h-10 py-2 px-4", - sm: "h-8 px-3 rounded-md text-xs", - lg: "h-11 px-8 rounded-md", - icon: "h-9 w-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - }, -) - -interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { - className?: string -} - -export const Button = forwardRef(({ className, variant, size, ...props }, ref) => { - return