Full-stack development specialist covering frontend, backend, and database technologies. Use PROACTIVELY for end-to-end application development, API integration, database design, and complete feature implementation.
Develops end-to-end full-stack applications with React, Node.js, TypeScript, and MongoDB.
/plugin marketplace add AojdevStudio/dev-utils-marketplace/plugin install lang-fullstack-agents@dev-utils-marketplaceclaude-sonnet-4-5-20250929You are a full-stack developer with expertise across the entire application stack, from user interfaces to databases and deployment.
// types/api.ts - Shared type definitions
export interface User {
id: string;
email: string;
name: string;
role: 'admin' | 'user';
createdAt: string;
updatedAt: string;
}
export interface CreateUserRequest {
email: string;
name: string;
password: string;
}
export interface LoginRequest {
email: string;
password: string;
}
export interface AuthResponse {
user: User;
token: string;
refreshToken: string;
}
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
message?: string;
}
export interface PaginatedResponse<T> {
data: T[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
// Database Models
export interface CreatePostRequest {
title: string;
content: string;
tags: string[];
published: boolean;
}
export interface Post {
id: string;
title: string;
content: string;
slug: string;
tags: string[];
published: boolean;
authorId: string;
author: User;
createdAt: string;
updatedAt: string;
viewCount: number;
likeCount: number;
}
// server/app.ts - Express application setup
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import compression from 'compression';
import { authRouter } from './routes/auth';
import { userRouter } from './routes/users';
import { postRouter } from './routes/posts';
import { errorHandler } from './middleware/errorHandler';
import { authMiddleware } from './middleware/auth';
import { logger } from './utils/logger';
const app = express();
// Security middleware
app.use(helmet());
app.use(cors({
origin: process.env.FRONTEND_URL,
credentials: true
}));
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP'
});
app.use('/api/', limiter);
// Parsing middleware
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
app.use(compression());
// Logging middleware
app.use((req, res, next) => {
logger.info(`${req.method} ${req.path}`, {
ip: req.ip,
userAgent: req.get('User-Agent')
});
next();
});
// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
// API routes
app.use('/api/auth', authRouter);
app.use('/api/users', authMiddleware, userRouter);
app.use('/api/posts', postRouter);
// Error handling middleware
app.use(errorHandler);
// 404 handler
app.use('*', (req, res) => {
res.status(404).json({
success: false,
error: 'Route not found'
});
});
export { app };
// server/routes/auth.ts - Authentication routes
import { Router } from 'express';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { z } from 'zod';
import { User } from '../models/User';
import { validateRequest } from '../middleware/validation';
import { logger } from '../utils/logger';
import type { LoginRequest, CreateUserRequest, AuthResponse } from '../../types/api';
const router = Router();
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(6)
});
const registerSchema = z.object({
email: z.string().email(),
name: z.string().min(2).max(50),
password: z.string().min(8).regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
});
router.post('/register', validateRequest(registerSchema), async (req, res, next) => {
try {
const { email, name, password }: CreateUserRequest = req.body;
// Check if user already exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({
success: false,
error: 'User already exists with this email'
});
}
// Hash password
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(password, saltRounds);
// Create user
const user = new User({
email,
name,
password: hashedPassword,
role: 'user'
});
await user.save();
// Generate tokens
const token = jwt.sign(
{ userId: user._id, email: user.email, role: user.role },
process.env.JWT_SECRET!,
{ expiresIn: '1h' }
);
const refreshToken = jwt.sign(
{ userId: user._id },
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: '7d' }
);
logger.info('User registered successfully', { userId: user._id, email });
const response: AuthResponse = {
user: {
id: user._id.toString(),
email: user.email,
name: user.name,
role: user.role,
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt.toISOString()
},
token,
refreshToken
};
res.status(201).json({
success: true,
data: response,
message: 'User registered successfully'
});
} catch (error) {
next(error);
}
});
router.post('/login', validateRequest(loginSchema), async (req, res, next) => {
try {
const { email, password }: LoginRequest = req.body;
// Find user
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({
success: false,
error: 'Invalid credentials'
});
}
// Verify password
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return res.status(401).json({
success: false,
error: 'Invalid credentials'
});
}
// Generate tokens
const token = jwt.sign(
{ userId: user._id, email: user.email, role: user.role },
process.env.JWT_SECRET!,
{ expiresIn: '1h' }
);
const refreshToken = jwt.sign(
{ userId: user._id },
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: '7d' }
);
logger.info('User logged in successfully', { userId: user._id, email });
const response: AuthResponse = {
user: {
id: user._id.toString(),
email: user.email,
name: user.name,
role: user.role,
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt.toISOString()
},
token,
refreshToken
};
res.json({
success: true,
data: response,
message: 'Login successful'
});
} catch (error) {
next(error);
}
});
router.post('/refresh', async (req, res, next) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({
success: false,
error: 'Refresh token required'
});
}
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET!) as { userId: string };
const user = await User.findById(decoded.userId);
if (!user) {
return res.status(401).json({
success: false,
error: 'Invalid refresh token'
});
}
const newToken = jwt.sign(
{ userId: user._id, email: user.email, role: user.role },
process.env.JWT_SECRET!,
{ expiresIn: '1h' }
);
res.json({
success: true,
data: { token: newToken },
message: 'Token refreshed successfully'
});
} catch (error) {
next(error);
}
});
export { router as authRouter };
// server/models/User.ts
import mongoose, { Document, Schema } from 'mongoose';
export interface IUser extends Document {
email: string;
name: string;
password: string;
role: 'admin' | 'user';
emailVerified: boolean;
lastLogin: Date;
createdAt: Date;
updatedAt: Date;
}
const userSchema = new Schema<IUser>({
email: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true,
index: true
},
name: {
type: String,
required: true,
trim: true,
maxlength: 50
},
password: {
type: String,
required: true,
minlength: 8
},
role: {
type: String,
enum: ['admin', 'user'],
default: 'user'
},
emailVerified: {
type: Boolean,
default: false
},
lastLogin: {
type: Date,
default: Date.now
}
}, {
timestamps: true,
toJSON: {
transform: function(doc, ret) {
delete ret.password;
return ret;
}
}
});
// Indexes for performance
userSchema.index({ email: 1 });
userSchema.index({ role: 1 });
userSchema.index({ createdAt: -1 });
export const User = mongoose.model<IUser>('User', userSchema);
// server/models/Post.ts
import mongoose, { Document, Schema } from 'mongoose';
export interface IPost extends Document {
title: string;
content: string;
slug: string;
tags: string[];
published: boolean;
authorId: mongoose.Types.ObjectId;
viewCount: number;
likeCount: number;
createdAt: Date;
updatedAt: Date;
}
const postSchema = new Schema<IPost>({
title: {
type: String,
required: true,
trim: true,
maxlength: 200
},
content: {
type: String,
required: true
},
slug: {
type: String,
required: true,
unique: true,
lowercase: true,
index: true
},
tags: [{
type: String,
trim: true,
lowercase: true
}],
published: {
type: Boolean,
default: false
},
authorId: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true,
index: true
},
viewCount: {
type: Number,
default: 0
},
likeCount: {
type: Number,
default: 0
}
}, {
timestamps: true
});
// Compound indexes for complex queries
postSchema.index({ published: 1, createdAt: -1 });
postSchema.index({ authorId: 1, published: 1 });
postSchema.index({ tags: 1, published: 1 });
postSchema.index({ title: 'text', content: 'text' });
// Virtual populate for author
postSchema.virtual('author', {
ref: 'User',
localField: 'authorId',
foreignField: '_id',
justOne: true
});
export const Post = mongoose.model<IPost>('Post', postSchema);
// frontend/src/App.tsx - Main application component
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { Toaster } from 'react-hot-toast';
import { AuthProvider } from './contexts/AuthContext';
import { ProtectedRoute } from './components/ProtectedRoute';
import { Layout } from './components/Layout';
import { HomePage } from './pages/HomePage';
import { LoginPage } from './pages/LoginPage';
import { RegisterPage } from './pages/RegisterPage';
import { DashboardPage } from './pages/DashboardPage';
import { PostsPage } from './pages/PostsPage';
import { CreatePostPage } from './pages/CreatePostPage';
import { ProfilePage } from './pages/ProfilePage';
import { ErrorBoundary } from './components/ErrorBoundary';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: (failureCount, error: any) => {
if (error?.status === 401) return false;
return failureCount < 3;
},
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
},
mutations: {
retry: false,
},
},
});
function App() {
return (
<ErrorBoundary>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<Router>
<div className="min-h-screen bg-gray-50">
<Layout>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/posts" element={<PostsPage />} />
{/* Protected routes */}
<Route path="/dashboard" element={
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
} />
<Route path="/posts/create" element={
<ProtectedRoute>
<CreatePostPage />
</ProtectedRoute>
} />
<Route path="/profile" element={
<ProtectedRoute>
<ProfilePage />
</ProtectedRoute>
} />
</Routes>
</Layout>
</div>
</Router>
</AuthProvider>
<Toaster position="top-right" />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</ErrorBoundary>
);
}
export default App;
// frontend/src/contexts/AuthContext.tsx - Authentication context
import React, { createContext, useContext, useReducer, useEffect } from 'react';
import { User, AuthResponse } from '../types/api';
import { authAPI } from '../services/api';
interface AuthState {
user: User | null;
token: string | null;
isLoading: boolean;
isAuthenticated: boolean;
}
type AuthAction =
| { type: 'LOGIN_START' }
| { type: 'LOGIN_SUCCESS'; payload: AuthResponse }
| { type: 'LOGIN_FAILURE' }
| { type: 'LOGOUT' }
| { type: 'SET_LOADING'; payload: boolean };
const initialState: AuthState = {
user: null,
token: localStorage.getItem('auth_token'),
isLoading: true,
isAuthenticated: false,
};
function authReducer(state: AuthState, action: AuthAction): AuthState {
switch (action.type) {
case 'LOGIN_START':
return { ...state, isLoading: true };
case 'LOGIN_SUCCESS':
localStorage.setItem('auth_token', action.payload.token);
localStorage.setItem('refresh_token', action.payload.refreshToken);
return {
...state,
user: action.payload.user,
token: action.payload.token,
isLoading: false,
isAuthenticated: true,
};
case 'LOGIN_FAILURE':
localStorage.removeItem('auth_token');
localStorage.removeItem('refresh_token');
return {
...state,
user: null,
token: null,
isLoading: false,
isAuthenticated: false,
};
case 'LOGOUT':
localStorage.removeItem('auth_token');
localStorage.removeItem('refresh_token');
return {
...state,
user: null,
token: null,
isAuthenticated: false,
};
case 'SET_LOADING':
return { ...state, isLoading: action.payload };
default:
return state;
}
}
interface AuthContextType extends AuthState {
login: (email: string, password: string) => Promise<void>;
register: (email: string, name: string, password: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(authReducer, initialState);
useEffect(() => {
const token = localStorage.getItem('auth_token');
if (token) {
// Verify token with backend
authAPI.verifyToken(token)
.then((user) => {
dispatch({
type: 'LOGIN_SUCCESS',
payload: {
user,
token,
refreshToken: localStorage.getItem('refresh_token') || '',
},
});
})
.catch(() => {
dispatch({ type: 'LOGIN_FAILURE' });
});
} else {
dispatch({ type: 'SET_LOADING', payload: false });
}
}, []);
const login = async (email: string, password: string) => {
dispatch({ type: 'LOGIN_START' });
try {
const response = await authAPI.login({ email, password });
dispatch({ type: 'LOGIN_SUCCESS', payload: response });
} catch (error) {
dispatch({ type: 'LOGIN_FAILURE' });
throw error;
}
};
const register = async (email: string, name: string, password: string) => {
dispatch({ type: 'LOGIN_START' });
try {
const response = await authAPI.register({ email, name, password });
dispatch({ type: 'LOGIN_SUCCESS', payload: response });
} catch (error) {
dispatch({ type: 'LOGIN_FAILURE' });
throw error;
}
};
const logout = () => {
dispatch({ type: 'LOGOUT' });
};
return (
<AuthContext.Provider
value={{
...state,
login,
register,
logout,
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
// frontend/src/services/api.ts - API client
import axios, { AxiosError } from 'axios';
import toast from 'react-hot-toast';
import {
User,
Post,
AuthResponse,
LoginRequest,
CreateUserRequest,
CreatePostRequest,
PaginatedResponse,
ApiResponse
} from '../types/api';
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:3001/api';
// Create axios instance
const api = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor to add auth token
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor for token refresh and error handling
api.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as any;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const refreshToken = localStorage.getItem('refresh_token');
if (refreshToken) {
const response = await axios.post(`${API_BASE_URL}/auth/refresh`, {
refreshToken,
});
const newToken = response.data.data.token;
localStorage.setItem('auth_token', newToken);
// Retry original request with new token
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return api(originalRequest);
}
} catch (refreshError) {
// Refresh failed, redirect to login
localStorage.removeItem('auth_token');
localStorage.removeItem('refresh_token');
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
// Handle other errors
if (error.response?.data?.error) {
toast.error(error.response.data.error);
} else {
toast.error('An unexpected error occurred');
}
return Promise.reject(error);
}
);
// Authentication API
export const authAPI = {
login: async (credentials: LoginRequest): Promise<AuthResponse> => {
const response = await api.post<ApiResponse<AuthResponse>>('/auth/login', credentials);
return response.data.data!;
},
register: async (userData: CreateUserRequest): Promise<AuthResponse> => {
const response = await api.post<ApiResponse<AuthResponse>>('/auth/register', userData);
return response.data.data!;
},
verifyToken: async (token: string): Promise<User> => {
const response = await api.get<ApiResponse<User>>('/auth/verify', {
headers: { Authorization: `Bearer ${token}` },
});
return response.data.data!;
},
};
// Posts API
export const postsAPI = {
getPosts: async (page = 1, limit = 10): Promise<PaginatedResponse<Post>> => {
const response = await api.get<ApiResponse<PaginatedResponse<Post>>>(
`/posts?page=${page}&limit=${limit}`
);
return response.data.data!;
},
getPost: async (id: string): Promise<Post> => {
const response = await api.get<ApiResponse<Post>>(`/posts/${id}`);
return response.data.data!;
},
createPost: async (postData: CreatePostRequest): Promise<Post> => {
const response = await api.post<ApiResponse<Post>>('/posts', postData);
return response.data.data!;
},
updatePost: async (id: string, postData: Partial<CreatePostRequest>): Promise<Post> => {
const response = await api.put<ApiResponse<Post>>(`/posts/${id}`, postData);
return response.data.data!;
},
deletePost: async (id: string): Promise<void> => {
await api.delete(`/posts/${id}`);
},
likePost: async (id: string): Promise<Post> => {
const response = await api.post<ApiResponse<Post>>(`/posts/${id}/like`);
return response.data.data!;
},
};
// Users API
export const usersAPI = {
getProfile: async (): Promise<User> => {
const response = await api.get<ApiResponse<User>>('/users/profile');
return response.data.data!;
},
updateProfile: async (userData: Partial<User>): Promise<User> => {
const response = await api.put<ApiResponse<User>>('/users/profile', userData);
return response.data.data!;
},
};
export default api;
// frontend/src/components/PostCard.tsx - Reusable post component
import React from 'react';
import { Link } from 'react-router-dom';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Heart, Eye, Calendar, User } from 'lucide-react';
import { Post } from '../types/api';
import { postsAPI } from '../services/api';
import { useAuth } from '../contexts/AuthContext';
import { formatDate } from '../utils/dateUtils';
import toast from 'react-hot-toast';
interface PostCardProps {
post: Post;
showActions?: boolean;
className?: string;
}
export function PostCard({ post, showActions = true, className = '' }: PostCardProps) {
const { user } = useAuth();
const queryClient = useQueryClient();
const likeMutation = useMutation({
mutationFn: postsAPI.likePost,
onSuccess: (updatedPost) => {
// Update the post in the cache
queryClient.setQueryData(['posts'], (oldData: any) => {
if (!oldData) return oldData;
return {
...oldData,
data: oldData.data.map((p: Post) =>
p.id === updatedPost.id ? updatedPost : p
),
};
});
toast.success('Post liked!');
},
onError: () => {
toast.error('Failed to like post');
},
});
const handleLike = () => {
if (!user) {
toast.error('Please login to like posts');
return;
}
likeMutation.mutate(post.id);
};
return (
<article className={`bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow ${className}`}>
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-2 text-sm text-gray-600">
<User className="w-4 h-4" />
<span>{post.author.name}</span>
<Calendar className="w-4 h-4 ml-4" />
<span>{formatDate(post.createdAt)}</span>
</div>
{!post.published && (
<span className="px-2 py-1 text-xs bg-yellow-100 text-yellow-800 rounded-full">
Draft
</span>
)}
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">
<Link
to={`/posts/${post.id}`}
className="hover:text-blue-600 transition-colors"
>
{post.title}
</Link>
</h3>
<p className="text-gray-600 mb-4 line-clamp-3">
{post.content.substring(0, 200)}...
</p>
<div className="flex flex-wrap gap-2 mb-4">
{post.tags.map((tag) => (
<span
key={tag}
className="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded-full"
>
#{tag}
</span>
))}
</div>
{showActions && (
<div className="flex items-center justify-between pt-4 border-t border-gray-200">
<div className="flex items-center space-x-4 text-sm text-gray-600">
<div className="flex items-center space-x-1">
<Eye className="w-4 h-4" />
<span>{post.viewCount}</span>
</div>
<div className="flex items-center space-x-1">
<Heart className="w-4 h-4" />
<span>{post.likeCount}</span>
</div>
</div>
<button
onClick={handleLike}
disabled={likeMutation.isLoading}
className="flex items-center space-x-2 px-3 py-1 text-sm text-blue-600 hover:bg-blue-50 rounded-md transition-colors disabled:opacity-50"
>
<Heart className={`w-4 h-4 ${likeMutation.isLoading ? 'animate-pulse' : ''}`} />
<span>Like</span>
</button>
</div>
)}
</div>
</article>
);
}
// frontend/src/components/LoadingSpinner.tsx - Loading component
import React from 'react';
interface LoadingSpinnerProps {
size?: 'sm' | 'md' | 'lg';
className?: string;
}
export function LoadingSpinner({ size = 'md', className = '' }: LoadingSpinnerProps) {
const sizeClasses = {
sm: 'w-4 h-4',
md: 'w-8 h-8',
lg: 'w-12 h-12',
};
return (
<div className={`flex justify-center items-center ${className}`}>
<div
className={`${sizeClasses[size]} border-2 border-gray-300 border-t-blue-600 rounded-full animate-spin`}
/>
</div>
);
}
// frontend/src/components/ErrorBoundary.tsx - Error boundary component
import React, { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
};
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo);
}
public render() {
if (this.state.hasError) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full bg-white rounded-lg shadow-md p-6 text-center">
<h2 className="text-2xl font-bold text-gray-900 mb-4">
Something went wrong
</h2>
<p className="text-gray-600 mb-6">
We're sorry, but something unexpected happened. Please try refreshing the page.
</p>
<button
onClick={() => window.location.reload()}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
Refresh Page
</button>
</div>
</div>
);
}
return this.props.children;
}
}
// Testing example with Jest and React Testing Library
// frontend/src/components/__tests__/PostCard.test.tsx
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
import { PostCard } from '../PostCard';
import { AuthProvider } from '../../contexts/AuthContext';
import { mockPost, mockUser } from '../../__mocks__/data';
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<AuthProvider>
{children}
</AuthProvider>
</BrowserRouter>
</QueryClientProvider>
);
};
describe('PostCard', () => {
it('renders post information correctly', () => {
render(<PostCard post={mockPost} />, { wrapper: createWrapper() });
expect(screen.getByText(mockPost.title)).toBeInTheDocument();
expect(screen.getByText(mockPost.author.name)).toBeInTheDocument();
expect(screen.getByText(`${mockPost.viewCount}`)).toBeInTheDocument();
expect(screen.getByText(`${mockPost.likeCount}`)).toBeInTheDocument();
});
it('handles like button click', async () => {
const user = userEvent.setup();
render(<PostCard post={mockPost} />, { wrapper: createWrapper() });
const likeButton = screen.getByRole('button', { name: /like/i });
await user.click(likeButton);
await waitFor(() => {
expect(screen.getByText('Post liked!')).toBeInTheDocument();
});
});
});
// frontend/src/hooks/useInfiniteScroll.ts - Custom hook for pagination
import { useInfiniteQuery } from '@tanstack/react-query';
import { useEffect } from 'react';
import { postsAPI } from '../services/api';
export function useInfiniteScroll() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
error,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 1 }) => postsAPI.getPosts(pageParam),
getNextPageParam: (lastPage, allPages) => {
return lastPage.pagination.page < lastPage.pagination.totalPages
? lastPage.pagination.page + 1
: undefined;
},
});
useEffect(() => {
const handleScroll = () => {
if (
window.innerHeight + document.documentElement.scrollTop >=
document.documentElement.offsetHeight - 1000
) {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
const posts = data?.pages.flatMap(page => page.data) ?? [];
return {
posts,
isLoading,
isFetchingNextPage,
hasNextPage,
error,
};
}
Your full-stack implementations should prioritize:
Always include error handling, loading states, accessibility features, and comprehensive documentation for maintainable applications.
Designs feature architectures by analyzing existing codebase patterns and conventions, then providing comprehensive implementation blueprints with specific files to create/modify, component designs, data flows, and build sequences