frontend-component
Next.js 16+ uses App Router with Server Components by default. Client Components are only used when interactivity is needed (hooks, event handlers, browser APIs).
下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o frontend-component.zip https://jpskill.com/download/17303.zip && unzip -o frontend-component.zip && rm frontend-component.zip
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/17303.zip -OutFile "$d\frontend-component.zip"; Expand-Archive "$d\frontend-component.zip" -DestinationPath $d -Force; ri "$d\frontend-component.zip"
完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。
💾 手動でダウンロードしたい(コマンドが難しい人向け)
- 1. 下の青いボタンを押して
frontend-component.zipをダウンロード - 2. ZIPファイルをダブルクリックで解凍 →
frontend-componentフォルダができる - 3. そのフォルダを
C:\Users\あなたの名前\.claude\skills\(Win)または~/.claude/skills/(Mac)へ移動 - 4. Claude Code を再起動
⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。
🎯 このSkillでできること
下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。
📦 インストール方法 (3ステップ)
- 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
- 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
- 3. 展開してできたフォルダを、ホームフォルダの
.claude/skills/に置く- · macOS / Linux:
~/.claude/skills/ - · Windows:
%USERPROFILE%\.claude\skills\
- · macOS / Linux:
Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。
詳しい使い方ガイドを見る →- 最終更新
- 2026-05-18
- 取得日時
- 2026-05-18
- 同梱ファイル
- 1
📖 Skill本文(日本語訳)
※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。
Frontend Component Skill
目的: サーバー/クライアントパターンと既存のコンポーネント構造に従った Next.js コンポーネントを作成するためのガイダンス。
概要
Next.js 16+ は、デフォルトで App Router と Server Components を使用します。Client Components は、インタラクティビティが必要な場合にのみ使用されます(hooks、イベントハンドラー、ブラウザ API)。
Server vs Client Components
Server Components (デフォルト)
いつ使うか:
- ページとレイアウト
- 静的なコンテンツ
- API からのデータフェッチ(可能な場合)
- SEO 最適化されたコンテンツ
パターン:
// "use client" ディレクティブは不要
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Page Title",
};
export default function PageComponent() {
return <div>Static content</div>;
}
例: frontend/app/layout.tsx, frontend/app/page.tsx
Client Components (必要な場合)
いつ使うか:
- インタラクティブな要素(ボタン、フォーム、入力)
- イベントハンドラー (onClick, onChange, etc.)
- React hooks (useState, useEffect, useRouter, etc.)
- ブラウザ API (localStorage, window, document, etc.)
- リアルタイム更新
- ドラッグアンドドロップ機能
パターン:
"use client"; // 最初の行に必須
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
interface ComponentProps {
prop1: string;
prop2?: number;
}
export default function ComponentName({ prop1, prop2 }: ComponentProps) {
const router = useRouter();
const [state, setState] = useState("");
useEffect(() => {
// Side effects
}, []);
return <div>{/* Component JSX */}</div>;
}
例: frontend/components/ProtectedRoute.tsx, frontend/app/signup/page.tsx
Component Structure Template
"use client"; // クライアントコンポーネントの場合のみ
/**
* Component Name
*
* このコンポーネントの機能の簡単な説明
*/
import { useState, useEffect } from "react";
import { ComponentType } from "@/types";
import { cn } from "@/lib/utils";
interface ComponentProps {
prop1: string;
prop2?: number;
className?: string;
}
export default function ComponentName({ prop1, prop2, className }: ComponentProps) {
// State
const [state, setState] = useState("");
// Effects
useEffect(() => {
// Side effects
}, []);
// Handlers
const handleClick = () => {
// Handler logic
};
// Render
return (
<div className={cn("base-classes", className)}>
{/* Component content */}
</div>
);
}
Specific Component Patterns
1. ProtectedRoute Pattern
From: frontend/components/ProtectedRoute.tsx
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { isAuthenticated } from "@/lib/auth";
import LoadingSpinner from "./LoadingSpinner";
interface ProtectedRouteProps {
children: React.ReactNode;
}
export default function ProtectedRoute({ children }: ProtectedRouteProps) {
const router = useRouter();
const [isAuthorized, setIsAuthorized] = useState(false);
const [isChecking, setIsChecking] = useState(true);
useEffect(() => {
async function checkAuth() {
try {
const authenticated = await isAuthenticated();
if (!authenticated) {
const currentPath = window.location.pathname;
if (currentPath !== "/signin") {
sessionStorage.setItem("redirectAfterLogin", currentPath);
}
router.push("/signin");
} else {
setIsAuthorized(true);
}
} catch (error) {
console.error("Auth check failed:", error);
router.push("/signin");
} finally {
setIsChecking(false);
}
}
checkAuth();
}, [router]);
if (isChecking) {
return (
<div className="min-h-screen flex items-center justify-center">
<LoadingSpinner size="large" />
</div>
);
}
if (!isAuthorized) {
return null;
}
return <>{children}</>;
}
パターン:
- マウント時に認証をチェック
- チェック中にローディングスピナーを表示
- 意図された宛先を
sessionStorageに保存 - 認証されていない場合は
/signinにリダイレクト - 認証されている場合にのみ子要素をレンダリング
2. LoadingSpinner Pattern
From: frontend/components/LoadingSpinner.tsx
interface LoadingSpinnerProps {
size?: "small" | "medium" | "large";
color?: string;
label?: string;
}
export default function LoadingSpinner({
size = "medium",
color = "blue",
label = "Loading...",
}: LoadingSpinnerProps) {
const sizeClasses = {
small: "w-4 h-4 border-2",
medium: "w-8 h-8 border-3",
large: "w-12 h-12 border-4",
};
const colorClasses = {
blue: "border-blue-600 border-t-transparent",
gray: "border-gray-600 border-t-transparent",
white: "border-white border-t-transparent",
};
const spinnerClass = `${sizeClasses[size]} ${
colorClasses[color as keyof typeof colorClasses] || colorClasses.blue
} rounded-full animate-spin`;
return (
<div
className="flex items-center justify-center"
role="status"
aria-label={label}
aria-live="polite"
>
<div className={spinnerClass}></div>
<span className="sr-only">{label}</span>
</div>
);
}
パターン:
- 複数のサイズ (small, medium, large)
- 複数の色 (blue, gray, white)
- アクセシビリティラベル (
role="status",aria-label,aria-live) sr-onlyクラスによるスクリーンリーダーテキスト
3. Form Handling Pattern
From: frontend/app/signup/page.tsx
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { api } from "@/lib/api";
import { isValidEmail, getPasswordStrength } from "@/lib/utils";
export default function SignupPage() {
const router = useRouter();
const [formData, setFormData] = useState({
name: "",
email: "",
password: "",
});
const [errors, setErrors, useState<Record<string, string>>({});
const [isLoading, setIsLoading] = useState(false);
const [apiError, s 📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開
Frontend Component Skill
Purpose: Guidance for creating Next.js components following server/client patterns and existing component structures.
Overview
Next.js 16+ uses App Router with Server Components by default. Client Components are only used when interactivity is needed (hooks, event handlers, browser APIs).
Server vs Client Components
Server Components (Default)
When to Use:
- Pages and layouts
- Static content
- Data fetching from API (when possible)
- SEO-optimized content
Pattern:
// No "use client" directive
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Page Title",
};
export default function PageComponent() {
return <div>Static content</div>;
}
Example: frontend/app/layout.tsx, frontend/app/page.tsx
Client Components (When Needed)
When to Use:
- Interactive elements (buttons, forms, inputs)
- Event handlers (onClick, onChange, etc.)
- React hooks (useState, useEffect, useRouter, etc.)
- Browser APIs (localStorage, window, document, etc.)
- Real-time updates
- Drag and drop functionality
Pattern:
"use client"; // MUST be first line
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
interface ComponentProps {
prop1: string;
prop2?: number;
}
export default function ComponentName({ prop1, prop2 }: ComponentProps) {
const router = useRouter();
const [state, setState] = useState("");
useEffect(() => {
// Side effects
}, []);
return <div>{/* Component JSX */}</div>;
}
Example: frontend/components/ProtectedRoute.tsx, frontend/app/signup/page.tsx
Component Structure Template
"use client"; // Only if client component
/**
* Component Name
*
* Brief description of what this component does
*/
import { useState, useEffect } from "react";
import { ComponentType } from "@/types";
import { cn } from "@/lib/utils";
interface ComponentProps {
prop1: string;
prop2?: number;
className?: string;
}
export default function ComponentName({ prop1, prop2, className }: ComponentProps) {
// State
const [state, setState] = useState("");
// Effects
useEffect(() => {
// Side effects
}, []);
// Handlers
const handleClick = () => {
// Handler logic
};
// Render
return (
<div className={cn("base-classes", className)}>
{/* Component content */}
</div>
);
}
Specific Component Patterns
1. ProtectedRoute Pattern
From: frontend/components/ProtectedRoute.tsx
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { isAuthenticated } from "@/lib/auth";
import LoadingSpinner from "./LoadingSpinner";
interface ProtectedRouteProps {
children: React.ReactNode;
}
export default function ProtectedRoute({ children }: ProtectedRouteProps) {
const router = useRouter();
const [isAuthorized, setIsAuthorized] = useState(false);
const [isChecking, setIsChecking] = useState(true);
useEffect(() => {
async function checkAuth() {
try {
const authenticated = await isAuthenticated();
if (!authenticated) {
const currentPath = window.location.pathname;
if (currentPath !== "/signin") {
sessionStorage.setItem("redirectAfterLogin", currentPath);
}
router.push("/signin");
} else {
setIsAuthorized(true);
}
} catch (error) {
console.error("Auth check failed:", error);
router.push("/signin");
} finally {
setIsChecking(false);
}
}
checkAuth();
}, [router]);
if (isChecking) {
return (
<div className="min-h-screen flex items-center justify-center">
<LoadingSpinner size="large" />
</div>
);
}
if (!isAuthorized) {
return null;
}
return <>{children}</>;
}
Pattern:
- Check authentication on mount
- Show loading spinner during check
- Store intended destination in sessionStorage
- Redirect to
/signinif not authenticated - Only render children if authorized
2. LoadingSpinner Pattern
From: frontend/components/LoadingSpinner.tsx
interface LoadingSpinnerProps {
size?: "small" | "medium" | "large";
color?: string;
label?: string;
}
export default function LoadingSpinner({
size = "medium",
color = "blue",
label = "Loading...",
}: LoadingSpinnerProps) {
const sizeClasses = {
small: "w-4 h-4 border-2",
medium: "w-8 h-8 border-3",
large: "w-12 h-12 border-4",
};
const colorClasses = {
blue: "border-blue-600 border-t-transparent",
gray: "border-gray-600 border-t-transparent",
white: "border-white border-t-transparent",
};
const spinnerClass = `${sizeClasses[size]} ${
colorClasses[color as keyof typeof colorClasses] || colorClasses.blue
} rounded-full animate-spin`;
return (
<div
className="flex items-center justify-center"
role="status"
aria-label={label}
aria-live="polite"
>
<div className={spinnerClass}></div>
<span className="sr-only">{label}</span>
</div>
);
}
Pattern:
- Multiple sizes (small, medium, large)
- Multiple colors (blue, gray, white)
- Accessibility labels (
role="status",aria-label,aria-live) - Screen reader text with
sr-onlyclass
3. Form Handling Pattern
From: frontend/app/signup/page.tsx
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { api } from "@/lib/api";
import { isValidEmail, getPasswordStrength } from "@/lib/utils";
export default function SignupPage() {
const router = useRouter();
const [formData, setFormData] = useState({
name: "",
email: "",
password: "",
});
const [errors, setErrors] = useState<Record<string, string>>({});
const [isLoading, setIsLoading] = useState(false);
const [apiError, setApiError] = useState("");
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
if (!formData.name.trim()) {
newErrors.name = "Name is required";
}
if (!formData.email.trim()) {
newErrors.email = "Email is required";
} else if (!isValidEmail(formData.email)) {
newErrors.email = "Please enter a valid email address";
}
// More validation...
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setApiError("");
if (!validateForm()) {
return;
}
setIsLoading(true);
try {
const response = await api.signup(formData);
if (response.success) {
router.push("/dashboard");
} else {
setApiError(response.message || "Signup failed");
}
} catch (error: any) {
setApiError(error.message || "An error occurred");
} finally {
setIsLoading(false);
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
// Clear error for this field when user starts typing
if (errors[name]) {
setErrors((prev) => ({ ...prev, [name]: "" }));
}
};
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
</form>
);
}
Pattern:
- Separate state for form data, errors, loading, API errors
- Validation function that returns boolean
- Clear errors on input change
- Loading state during submission
- Try-catch for error handling
- Redirect on success
4. ToastNotification Pattern
From: frontend/components/ToastNotification.tsx
"use client";
import { useEffect, useState } from "react";
import { ToastMessage, ToastType } from "@/types";
export function useToast() {
const [toasts, setToasts] = useState<ToastMessage[]>([]);
const showToast = (type: ToastType, message: string, duration?: number) => {
const id = `toast-${Date.now()}-${Math.random()}`;
const newToast: ToastMessage = {
id,
type,
message,
duration,
};
setToasts((prev) => [...prev, newToast]);
};
const dismissToast = (id: string) => {
setToasts((prev) => prev.filter((toast) => toast.id !== id));
};
return {
toasts,
showToast,
dismissToast,
success: (message: string, duration?: number) => showToast("success", message, duration),
error: (message: string, duration?: number) => showToast("error", message, duration),
// ...
};
}
Pattern:
- Custom hook for toast management
- Auto-dismiss with duration
- Stack multiple toasts
- Helper methods (success, error, warning, info)
Tailwind CSS Patterns
1. Utility Classes Only
<div className="flex items-center justify-center gap-4 p-6 bg-white rounded-lg shadow-md">
Pattern: Use Tailwind utility classes, no inline styles
2. Conditional Classes with cn() Utility
import { cn } from "@/lib/utils";
<div className={cn(
"base-classes",
condition && "conditional-classes",
className // Allow prop override
)}>
Pattern: Use cn() from @/lib/utils for conditional classes
3. Dark Mode Support
<div className="bg-white dark:bg-gray-800 text-gray-900 dark:text-white">
Pattern: Use dark: prefix for dark mode styles
4. Responsive Design
<div className="w-full sm:w-1/2 md:w-1/3 lg:w-1/4">
Pattern: Use breakpoint prefixes (sm:, md:, lg:, xl:)
Accessibility Patterns (WCAG 2.1 AA)
1. ARIA Labels
<button aria-label="Close dialog">×</button>
<div role="status" aria-live="polite" aria-label="Loading...">
Pattern: Always provide aria-label for icon-only buttons
2. Semantic HTML
<nav>
<ul>
<li><a href="/">Home</a></li>
</ul>
</nav>
Pattern: Use semantic HTML elements (nav, main, section, article, etc.)
3. Keyboard Navigation
<button
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
handleClick();
}
}}
>
Pattern: Ensure keyboard accessibility for all interactive elements
4. Screen Reader Text
<span className="sr-only">Loading content</span>
Pattern: Use sr-only class for screen reader-only text
5. Focus Management
<input
autoFocus
className="focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
Pattern: Always provide visible focus indicators
File Naming Conventions
- Pages: kebab-case (e.g.,
signup.tsx,signin.tsx) - Components: PascalCase (e.g.,
TaskList.tsx,TaskItem.tsx) - Layouts:
layout.tsx - Error Pages:
error.tsx,not-found.tsx
Constitution Requirements
- FR-033: Next.js 16+ App Router structure ✅
- FR-034: Server components default, client when needed ✅
- FR-035: Error boundaries ✅
- FR-036: WCAG 2.1 AA compliance ✅
- FR-037: TypeScript strict mode ✅
- FR-038: Prettier formatting ✅
- FR-039: ESLint rules ✅
References
- Specification:
specs/002-frontend-todo-app/spec.md- Component specifications - Existing Components:
frontend/components/*.tsx- Component examples - Existing Pages:
frontend/app/*.tsx- Page examples
Advanced Component Patterns
1. Drag and Drop Pattern (Phase 7 - T065)
Library: @dnd-kit/core
"use client";
import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
export default function SortableTaskList({ tasks, onReorder }: Props) {
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (over && active.id !== over.id) {
onReorder(active.id, over.id);
}
};
return (
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={tasks.map(t => t.id)} strategy={verticalListSortingStrategy}>
{tasks.map(task => (
<SortableTaskItem key={task.id} task={task} />
))}
</SortableContext>
</DndContext>
);
}
Pattern: Use @dnd-kit/core for drag and drop, handle reorder on drag end
2. Undo/Redo Pattern (Phase 7 - T066)
"use client";
import { useReducer } from "react";
interface HistoryState<T> {
past: T[];
present: T;
future: T[];
}
function historyReducer<T>(state: HistoryState<T>, action: { type: string; newPresent?: T }): HistoryState<T> {
const { past, present, future } = state;
switch (action.type) {
case "UNDO":
if (past.length === 0) return state;
return {
past: past.slice(0, past.length - 1),
present: past[past.length - 1],
future: [present, ...future],
};
case "REDO":
if (future.length === 0) return state;
return {
past: [...past, present],
present: future[0],
future: future.slice(1),
};
case "SET":
if (action.newPresent === present) return state;
return {
past: [...past, present],
present: action.newPresent!,
future: [],
};
default:
return state;
}
}
export function useHistory<T>(initialPresent: T) {
const [state, dispatch] = useReducer(historyReducer, {
past: [],
present: initialPresent,
future: [],
});
const undo = () => dispatch({ type: "UNDO" });
const redo = () => dispatch({ type: "REDO" });
const set = (newPresent: T) => dispatch({ type: "SET", newPresent });
return { state: state.present, set, undo, redo, canUndo: state.past.length > 0, canRedo: state.future.length > 0 };
}
Pattern: Use useReducer with history pattern for undo/redo functionality
3. Real-time Updates with Polling (Phase 7 - T067)
"use client";
import { useEffect, useRef } from "react";
export function usePolling(callback: () => Promise<void>, interval: number = 5000) {
const intervalRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
const poll = async () => {
try {
await callback();
} catch (error) {
console.error("Polling error:", error);
}
};
// Initial call
poll();
// Set up polling interval
intervalRef.current = setInterval(poll, interval);
// Cleanup
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [callback, interval]);
}
Pattern: Use setInterval for polling, cleanup on unmount, handle errors gracefully
4. Inline Editing Pattern (Phase 7 - T068)
"use client";
import { useState } from "react";
export default function InlineEditable({ value, onSave }: Props) {
const [isEditing, setIsEditing] = useState(false);
const [editValue, setEditValue] = useState(value);
const handleSave = () => {
onSave(editValue);
setIsEditing(false);
};
const handleCancel = () => {
setEditValue(value);
setIsEditing(false);
};
if (isEditing) {
return (
<input
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onBlur={handleSave}
onKeyDown={(e) => {
if (e.key === "Enter") handleSave();
if (e.key === "Escape") handleCancel();
}}
autoFocus
/>
);
}
return (
<span onClick={() => setIsEditing(true)} className="cursor-pointer">
{value}
</span>
);
}
Pattern: Toggle edit mode, save on blur/Enter, cancel on Escape
5. Performance Optimization Patterns (Phase 8 - T072)
Code Splitting with next/dynamic
import dynamic from "next/dynamic";
// Lazy load heavy components
const TaskStatistics = dynamic(() => import("@/components/TaskStatistics"), {
loading: () => <LoadingSpinner />,
ssr: false, // Disable SSR if not needed
});
const TaskDetailModal = dynamic(() => import("@/components/TaskDetailModal"), {
loading: () => <LoadingSpinner />,
});
Pattern: Use next/dynamic for code splitting, provide loading fallback
Image Optimization
import Image from "next/image";
<Image
src="/image.jpg"
alt="Description"
width={500}
height={300}
loading="lazy"
placeholder="blur"
/>
Pattern: Use Next.js Image component for automatic optimization
6. Error Boundary Pattern (Phase 8 - T074)
"use client";
import React, { Component, ErrorInfo, ReactNode } from "react";
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export default class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): Partial<State> {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
console.error("ErrorBoundary caught an error:", error, errorInfo);
// Log to error tracking service
}
render() {
if (this.state.hasError) {
return (
this.props.fallback || (
<div className="p-4 bg-red-50 border border-red-200 rounded">
<h2 className="text-red-800 font-bold">Something went wrong</h2>
<p className="text-red-600">{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false })}>Try again</button>
</div>
)
);
}
return this.props.children;
}
}
Pattern: Class component, catch errors, provide fallback UI, log errors
7. PWA Patterns (Phase 8 - T069, T070, T071)
Service Worker Setup
// public/sw.js or use next-pwa
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker
.register("/sw.js")
.then((registration) => {
console.log("SW registered:", registration);
})
.catch((error) => {
console.error("SW registration failed:", error);
});
});
}
Pattern: Register service worker on page load, handle registration errors
IndexedDB for Offline Storage
import { openDB, DBSchema, IDBPDatabase } from "idb";
interface TaskDB extends DBSchema {
tasks: {
key: number;
value: Task;
indexes: { "by-user-id": string };
};
}
export async function getDB(): Promise<IDBPDatabase<TaskDB>> {
return openDB<TaskDB>("todo-db", 1, {
upgrade(db) {
const taskStore = db.createObjectStore("tasks", { keyPath: "id" });
taskStore.createIndex("by-user-id", "user_id");
},
});
}
export async function saveTaskOffline(task: Task) {
const db = await getDB();
await db.put("tasks", task);
}
export async function getTasksOffline(userId: string): Promise<Task[]> {
const db = await getDB();
return db.getAllFromIndex("tasks", "by-user-id", userId);
}
Pattern: Use idb library for IndexedDB, create stores and indexes, handle offline data
Offline Sync Mechanism
export async function syncOfflineChanges(userId: string) {
const db = await getDB();
const offlineTasks = await db.getAllFromIndex("tasks", "by-user-id", userId);
for (const task of offlineTasks) {
if (task.syncStatus === "pending") {
try {
await api.createTask(userId, task);
await db.put("tasks", { ...task, syncStatus: "synced" });
} catch (error) {
console.error("Sync failed for task:", task.id, error);
}
}
}
}
// Call on connection restore
window.addEventListener("online", () => {
syncOfflineChanges(currentUserId);
});
Pattern: Track sync status, sync on connection restore, handle sync errors
8. Caching Strategies (Phase 8 - T073)
// Cache API responses
const cache = new Map<string, { data: any; timestamp: number }>();
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
export async function getCachedData<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
return cached.data;
}
const data = await fetcher();
cache.set(key, { data, timestamp: Date.now() });
return data;
}
Pattern: Use Map for in-memory cache, check expiration, update cache on fetch
9. Error Logging and Tracking (Phase 8 - T075)
export function logError(error: Error, context?: Record<string, any>) {
console.error("Error:", error, context);
// Send to error tracking service (e.g., Sentry)
if (typeof window !== "undefined" && (window as any).Sentry) {
(window as any).Sentry.captureException(error, {
extra: context,
});
}
// Or send to custom endpoint
fetch("/api/log-error", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: error.message,
stack: error.stack,
context,
timestamp: new Date().toISOString(),
}),
}).catch((err) => console.error("Failed to log error:", err));
}
Pattern: Log to console, send to error tracking service, include context
Common Patterns Summary
- ✅ Use Server Components by default
- ✅ Add
"use client"only when needed - ✅ Use TypeScript interfaces for props
- ✅ Use
cn()utility for conditional classes - ✅ Always include accessibility attributes
- ✅ Use semantic HTML elements
- ✅ Provide loading states
- ✅ Handle errors gracefully
- ✅ Use Tailwind utility classes
- ✅ Support dark mode with
dark:prefix - ✅ Use
@dnd-kit/corefor drag and drop - ✅ Use
useReducerfor undo/redo - ✅ Use
setIntervalfor polling with cleanup - ✅ Use
next/dynamicfor code splitting - ✅ Use ErrorBoundary for error handling
- ✅ Use IndexedDB for offline storage
- ✅ Sync offline changes on connection restore
- ✅ Cache API responses with expiration
- ✅ Log errors to tracking service