A comprehensive technical breakdown of the Pulse social media frontend application built with React 18, Vite, and Tailwind CSS.
- Architecture Overview
- Technology Stack
- Project Structure
- Folder Breakdown
- Core Components
- Page Components
- Services Layer
- State Management
- Authentication Flow
- Data Flow Patterns
- Styling System
- API Integration
- Known Challenges and Solutions
- Development Setup
The Pulse frontend follows a component-based architecture with clear separation of concerns:
+---------------------------------------------------------------------+
| App.jsx (Root) |
| +---------------------------------------------------------------+ |
| | Authentication Layer | |
| | (Token validation, user state management) | |
| +---------------------------------------------------------------+ |
| | |
| +---------------+---------------+ |
| v v v |
| +-----------------+ +-------------+ +-----------------+ |
| | Navbar | | Sidebar | | Main Content | |
| | (Navigation) | | (User Card)| | (Routes) | |
| +-----------------+ +-------------+ +-----------------+ |
| | |
| +---------------+---------------+---------------+ |
| v v v v |
| +---------+ +---------+ +---------+ +---------+ +---------+ |
| | Home | | Profile | |Messages | | Explore | |Settings | |
| +---------+ +---------+ +---------+ +---------+ +---------+ |
| | |
| +---------------+---------------+ |
| v v v |
| +-----------------+ +-----------------+ +-----------------+ |
| | PostCard | | UserCard | | CommentSection | |
| | (Reusable) | | (Reusable) | | (Reusable) | |
| +-----------------+ +-----------------+ +-----------------+ |
+---------------------------------------------------------------------+
|
v
+---------------------------------------------------------------------+
| Services Layer (api.js) |
| +---------------------------------------------------------------+ |
| | Axios Instance -> Request Interceptor -> Response Handler | |
| +---------------------------------------------------------------+ |
| | |
| v |
| +---------------------------------------------------------------+ |
| | FastAPI Backend (localhost:8000) | |
| +---------------------------------------------------------------+ |
+---------------------------------------------------------------------+
- Component Composition: Small, reusable components composed into larger page components
- Prop Drilling with Callbacks: Parent components pass callbacks to children for state updates
- Optimistic Updates: UI updates immediately, then syncs with server
- Service Layer Abstraction: All API calls abstracted into a single service file
- Token-based Authentication: JWT tokens stored in localStorage, attached via interceptors
| Technology | Version | Purpose |
|---|---|---|
| React | 18.2.0 | UI component library |
| Vite | 5.0.0 | Build tool and dev server |
| React Router DOM | 6.30.2 | Client-side routing |
| Axios | 1.13.2 | HTTP client |
| Tailwind CSS | 3.4.19 | Utility-first styling |
| Lucide React | 0.263.1 | Icon library |
| PostCSS | 8.5.4 | CSS processing |
| Autoprefixer | 10.4.21 | CSS vendor prefixing |
frontend/
├── public/ # Static assets
├── src/
│ ├── components/ # Reusable UI components
│ │ ├── CommentSection.jsx # Comment display and input
│ │ ├── CreatePost.jsx # Post creation modal
│ │ ├── Navbar.jsx # Top navigation bar
│ │ ├── PostCard.jsx # Individual post display
│ │ ├── Sidebar.jsx # Left sidebar with user info
│ │ ├── Toast.jsx # Toast notification component
│ │ └── UserCard.jsx # User profile card (compact)
│ │
│ ├── pages/ # Page-level components
│ │ ├── Explore.jsx # User/content discovery
│ │ ├── Home.jsx # Main feed page
│ │ ├── Login.jsx # Authentication page
│ │ ├── Messages.jsx # Direct messaging
│ │ ├── Notifications.jsx # Activity notifications
│ │ ├── Profile.jsx # User profile page
│ │ ├── Saved.jsx # Saved posts collection
│ │ ├── Settings.jsx # User settings
│ │ └── Trending.jsx # Trending hashtags
│ │
│ ├── services/
│ │ └── api.js # API service layer (all HTTP calls)
│ │
│ ├── App.jsx # Root component with routing
│ ├── index.css # Global styles and Tailwind directives
│ └── main.jsx # Application entry point
│
├── index.html # HTML template
├── package.json # Dependencies and scripts
├── tailwind.config.js # Tailwind CSS configuration
├── postcss.config.js # PostCSS configuration
└── vite.config.js # Vite build configuration
This folder contains UI components that are used across multiple pages. Each component is self-contained with its own state management and event handlers.
Page components represent full routes in the application. They typically:
- Fetch data on mount using
useEffect - Manage page-level state
- Compose multiple child components
- Handle user interactions and API calls
Contains the API service module that abstracts all HTTP communication with the backend. This provides a clean interface for components to interact with the server.
Purpose: Application root that handles authentication state and routing logic.
Location: src/App.jsx
Major Imports:
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { getCurrentUser } from './services/api';Key State:
const [currentUser, setCurrentUser] = useState(null); // Authenticated user data
const [loading, setLoading] = useState(true); // Auth check loading stateImportant Functions:
| Function | Purpose |
|---|---|
checkAuth() |
Validates token on app load, fetches fresh user data |
handleLogin(user) |
Sets user state after successful login |
refreshUserStats() |
Re-fetches user data to update sidebar stats |
handleLogout() |
Clears auth data and resets state |
handleUserUpdate(user) |
Updates user state after profile changes |
Data Flow:
- On mount,
checkAuth()checks for token in localStorage - If token exists, fetches fresh user data from
/users/me - User data includes stats (posts_count, followers_count, following_count)
currentUseris passed down to all child components as proprefreshUserStatscallback is passed to pages that modify follow relationships
Routing Structure:
// Authenticated routes
<Route path="/home" element={<Home currentUser={currentUser} onRefreshStats={refreshUserStats} />} />
<Route path="/profile/:userId" element={<Profile currentUser={currentUser} onRefreshStats={refreshUserStats} />} />
<Route path="/notifications" element={<Notifications />} />
<Route path="/messages" element={<Messages currentUser={currentUser} />} />
<Route path="/explore" element={<Explore currentUser={currentUser} onRefreshStats={refreshUserStats} />} />
<Route path="/trending" element={<Trending currentUser={currentUser} />} />
<Route path="/saved" element={<Saved currentUser={currentUser} />} />
<Route path="/settings" element={<Settings currentUser={currentUser} onLogout={handleLogout} onUserUpdate={handleUserUpdate} />} />
// Unauthenticated route
<Route path="/login" element={<Login onLogin={handleLogin} />} />Purpose: Fixed top navigation with search, notifications, messages, and user actions.
Location: src/components/Navbar.jsx
Props:
{ user, notificationCount, messageCount, onLogout }Key State:
const [searchQuery, setSearchQuery] = useState('');Features:
- Brand logo linking to home
- Search input (form submission ready)
- Notification bell with badge count
- Message icon with badge count
- User avatar linking to profile
- Logout button
Data Flow:
- Receives
userprop from App.jsx - Receives badge counts as props (currently hardcoded in App.jsx)
- Calls
onLogoutcallback when logout clicked
Purpose: Displays user profile card and navigation links.
Location: src/components/Sidebar.jsx
Props:
{ user }Navigation Items:
const navItems = [
{ path: '/home', icon: Home, label: 'Home' },
{ path: `/profile/${user?.user_id}`, icon: User, label: 'Profile' },
{ path: '/notifications', icon: Bell, label: 'Notifications' },
{ path: '/messages', icon: MessageCircle, label: 'Messages' },
{ path: '/explore', icon: Search, label: 'Explore' },
{ path: '/trending', icon: Hash, label: 'Trending' },
{ path: '/saved', icon: Bookmark, label: 'Saved' },
{ path: '/settings', icon: Settings, label: 'Settings' }
];User Stats Displayed:
- Posts count (
user.posts_count) - Followers count (
user.followers_count) - Following count (
user.following_count)
Styling Notes:
- Fixed position, 250px width
- Uses
useLocation()for active link highlighting - Custom scrollbar via Tailwind class
Purpose: Renders individual posts with like, comment, save, and delete functionality.
Location: src/components/PostCard.jsx
Props:
{ post, onLike, onComment, onDelete, onSave, currentUser }Key State:
const [showComments, setShowComments] = useState(false); // Toggle comments visibility
const [liked, setLiked] = useState(post.is_liked || false); // Like state
const [saved, setSaved] = useState(post.is_saved || false); // Save state
const [likesCount, setLikesCount] = useState(post.likes_count || 0);
const [commentsCount, setCommentsCount] = useState(post.comments_count || 0);
const [showMenu, setShowMenu] = useState(false); // Delete menu visibilityImportant Functions:
| Function | Purpose |
|---|---|
handleLike() |
Optimistic update, calls onLike callback |
handleCommentSubmit(text) |
Calls onComment, increments count |
handleDelete() |
Confirmation dialog, calls onDelete |
handleSave() |
Optimistic update, calls onSave callback |
Optimistic Update Pattern:
const handleLike = async () => {
const prevLiked = liked;
const prevCount = likesCount;
// Optimistic update
setLiked(!liked);
setLikesCount(liked ? likesCount - 1 : likesCount + 1);
try {
const result = await onLike(post.post_id);
if (result && result.success !== undefined) {
setLiked(result.is_liked);
}
} catch (error) {
// Rollback on error
setLiked(prevLiked);
setLikesCount(prevCount);
}
};Media Handling:
// Handles both absolute URLs and relative paths
src={post.media.startsWith('http') ? post.media : `${API_BASE_URL}${post.media}`}Purpose: Modal dialog for creating new posts with text, hashtags, and image upload.
Location: src/components/CreatePost.jsx
Props:
{ isOpen, onClose, onSubmit, currentUser }Key State:
const [postText, setPostText] = useState('');
const [hashtags, setHashtags] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [selectedImage, setSelectedImage] = useState(null);
const [imagePreview, setImagePreview] = useState(null);
const [isUploading, setIsUploading] = useState(false);
const fileInputRef = useRef(null);Image Upload Flow:
- User clicks "Photo" button, triggering hidden file input
handleImageSelectvalidates file type and size (max 5MB)- FileReader creates preview via
readAsDataURL - On submit,
uploadImageAPI is called first - If upload succeeds, post is created with media URL
Supported Image Types:
- JPEG/JPG
- PNG
- GIF
- WebP
Hashtag Parsing:
const hashtagsArray = hashtags
.split(',')
.map(tag => tag.trim().replace('#', ''))
.filter(tag => tag.length > 0);Purpose: Displays comments for a post and provides comment input.
Location: src/components/CommentSection.jsx
Props:
{ postId, currentUser, onCommentSubmit }Key State:
const [comments, setComments] = useState([]);
const [newComment, setNewComment] = useState('');
const [loading, setLoading] = useState(true);API Calls:
getComments(postId)- Fetches comments on mount and after submission
Data Flow:
- On mount, fetches comments via
loadComments() - User types comment, stored in
newCommentstate - On submit, calls
onCommentSubmitcallback - Refreshes comment list via
loadComments() - Clears input field
Purpose: Displays user info with follow/unfollow button for suggested users.
Location: src/components/UserCard.jsx
Props:
{ user, onFollow }Key State:
const [isFollowing, setIsFollowing] = useState(user.is_following || false);
const [isLoading, setIsLoading] = useState(false);Follow Toggle Pattern:
const handleFollow = async () => {
setIsLoading(true);
try {
if (onFollow) {
const result = await onFollow(user.user_id);
if (result && result.success) {
setIsFollowing(result.is_following);
}
}
} catch (error) {
console.error('Error following user:', error);
} finally {
setIsLoading(false);
}
};Purpose: Displays temporary notification messages.
Location: src/components/Toast.jsx
Props:
{ message, type, onClose }Types: 'success' | 'error'
Behavior:
- Auto-dismisses after 3 seconds
- Manual close via X button
- Different styling based on type
Purpose: Handles user login and registration.
Location: src/pages/Login.jsx
Key State:
const [isLogin, setIsLogin] = useState(true); // Toggle login/signup mode
const [showPassword, setShowPassword] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [formData, setFormData] = useState({
username: '',
email: '',
password: ''
});Authentication Flow:
- On page load, clears old tokens from localStorage
- User submits form
- Calls
login()orregister()API - On success:
- Stores token in localStorage
- Stores user data in localStorage
- Calls
debugToken()to verify token - Calls
onLogincallback - Navigates to
/home
- On failure, displays error message
Props:
{ onLogin } // Callback to App.jsx to set authenticated stateAPI Calls:
login(username, password)register(username, email, password)debugToken()- Verification utility
Purpose: Displays post feed, suggested users, and trending hashtags.
Location: src/pages/Home.jsx
Props:
{ currentUser, onRefreshStats }Key State:
const [posts, setPosts] = useState([]);
const [suggestedUsers, setSuggestedUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [showCreatePost, setShowCreatePost] = useState(false);
const [toast, setToast] = useState(null);Data Loading:
const loadHomeData = async () => {
const [postsData, usersData] = await Promise.all([
getPosts(),
getSuggestedUsers()
]);
setPosts(postsData);
setSuggestedUsers(usersData);
};Handler Functions:
| Function | Purpose | API Call |
|---|---|---|
handleCreatePost(data) |
Creates new post, refreshes feed | createPost(), getPosts() |
handleLikePost(postId) |
Toggles like on post | likePost() |
handleAddComment(postId, text) |
Adds comment to post | addComment() |
handleDeletePost(postId) |
Removes post from feed | deletePost() |
handleFollowUser(userId) |
Follows user, removes from suggested | followUser() |
handleSavePost(postId) |
Toggles save status | savePost() |
Layout Structure:
- Left: Main feed column with create post trigger and posts
- Right: Sidebar with suggested users and trending hashtags (desktop only)
Purpose: Displays user profile with posts, stats, and follow functionality.
Location: src/pages/Profile.jsx
Props:
{ currentUser, onRefreshStats }URL Params:
const { userId } = useParams();Key State:
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState('posts'); // posts | media | liked
const [isFollowing, setIsFollowing] = useState(false);Own Profile Detection:
const isOwnProfile = currentUser?.user_id === parseInt(userId);Data Loading:
const loadProfileData = async () => {
const [userData, postsData] = await Promise.all([
getUserProfile(parseInt(userId)),
getUserPosts(parseInt(userId))
]);
setUser(userData);
setPosts(postsData);
setIsFollowing(userData?.is_following || userData?.isFollowing || false);
};Follow Handler with Optimistic Update:
const handleFollow = async () => {
const prevFollowing = isFollowing;
setIsFollowing(!isFollowing); // Optimistic
const result = await followUser(parseInt(userId));
if (result && result.success) {
const updatedUser = await getUserProfile(parseInt(userId));
setUser(updatedUser);
setIsFollowing(result.is_following);
onRefreshStats?.(); // Update sidebar
} else {
setIsFollowing(prevFollowing); // Rollback
}
};Tabs:
- Posts: User's posts
- Media: Posts with images (placeholder)
- Liked: Liked posts (placeholder)
Purpose: Conversation list and chat interface.
Location: src/pages/Messages.jsx
Key State:
const [conversations, setConversations] = useState([]);
const [selectedConversation, setSelectedConversation] = useState(null);
const [messages, setMessages] = useState([]);
const [newMessage, setNewMessage] = useState('');API Calls:
getMessages()- Fetches conversation listgetConversation(userId)- Fetches messages with specific usersendMessage(receiverId, content)- Sends new message
Purpose: Account settings with profile editing.
Location: src/pages/Settings.jsx
Props:
{ currentUser, onLogout, onUserUpdate }Sections:
- Account (username, email, bio editing)
- Notifications (toggle settings)
- Help & Support (info)
Profile Update Flow:
- User modifies form fields
- On save, calls
updateProfile() - Updates localStorage with new user data
- Calls
onUserUpdatecallback to update App state
Purpose: Centralizes all HTTP communication with the backend.
Location: src/services/api.js
Base Configuration:
const API_BASE_URL = 'http://localhost:8000';
const api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});Request Interceptor (Token Attachment):
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});Response Interceptor (Error Handling):
api.interceptors.response.use(
(response) => response,
(error) => {
// Logs error but does NOT redirect
// Allows components to handle errors gracefully
return Promise.reject(error);
}
);| Function | Endpoint | Method | Returns |
|---|---|---|---|
login(username, password) |
/auth/login |
POST | { success, user, token } |
register(username, email, password) |
/auth/register |
POST | { success, user, token } |
logout() |
- | - | Clears localStorage, redirects |
debugToken() |
/auth/debug-token |
POST | Token validation result |
| Function | Endpoint | Method | Returns |
|---|---|---|---|
getCurrentUser() |
/users/me + /users/{id}/stats |
GET | User with stats |
getUserProfile(userId) |
/users/{id} + /users/{id}/stats |
GET | User with stats |
updateProfile(data) |
/users/me |
PUT | { success, user } |
getSuggestedUsers() |
/users/?limit=5 |
GET | User array |
searchUsers(query) |
/users/?query={q}&limit=20 |
GET | User array |
| Function | Endpoint | Method | Returns |
|---|---|---|---|
getPosts() |
/posts/?limit=20 |
GET | Post array (formatted) |
getUserPosts(userId) |
/posts/user/{id}?limit=20 |
GET | Post array (formatted) |
createPost(data) |
/posts/ |
POST | { success, post } |
deletePost(postId) |
/posts/{id} |
DELETE | { success } |
| Function | Endpoint | Method | Returns |
|---|---|---|---|
likePost(postId) |
/likes/post/{id}/check, /likes/, /likes/{id} |
GET/POST/DELETE | { success, is_liked } |
| Function | Endpoint | Method | Returns |
|---|---|---|---|
getComments(postId) |
/comments/post/{id} |
GET | Comment array (formatted) |
addComment(postId, text) |
/comments/ |
POST | { success, comment } |
| Function | Endpoint | Method | Returns |
|---|---|---|---|
followUser(userId) |
/follows/check/{id}, /follows/, /follows/{id} |
GET/POST/DELETE | { success, is_following } |
checkFollowStatus(userId) |
/follows/check/{id} |
GET | Boolean |
getFollowers(userId) |
/follows/followers/{id} |
GET | User array |
getFollowing(userId) |
/follows/following/{id} |
GET | User array |
| Function | Endpoint | Method | Returns |
|---|---|---|---|
getMessages() |
/messages/conversations |
GET | Conversation array |
getConversation(userId) |
/messages/conversation/{id} |
GET | Message array |
sendMessage(receiverId, content) |
/messages/ |
POST | { success, message } |
| Function | Endpoint | Method | Returns |
|---|---|---|---|
getSavedPosts() |
/saved/ |
GET | Post array |
savePost(postId) |
/saved/toggle/{id} |
POST | { success, is_saved } |
checkSavedStatus(postId) |
/saved/check/{id} |
GET | Boolean |
| Function | Endpoint | Method | Returns |
|---|---|---|---|
uploadImage(file) |
/upload/image |
POST | { success, url, filename } |
Time Formatting:
function formatTimeAgo(dateString) {
const seconds = Math.floor((now - date) / 1000);
if (seconds < 60) return 'Just now';
if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes ago`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours ago`;
if (seconds < 604800) return `${Math.floor(seconds / 86400)} days ago`;
return date.toLocaleDateString();
}Pulse uses React's built-in state management without external libraries:
-
Global State (App.jsx)
currentUser- Authenticated user data- Passed down via props to all pages
-
Page State (Page components)
- Data fetched on mount
- Loading states
- UI state (modals, tabs)
-
Component State (Reusable components)
- Local UI state
- Optimistic update states
Pattern 1: Callback Prop
// Parent passes callback
<Home currentUser={currentUser} onRefreshStats={refreshUserStats} />
// Child calls after action
const handleFollowUser = async (userId) => {
const result = await followUser(userId);
if (result.success && onRefreshStats) {
onRefreshStats(); // Triggers parent state refresh
}
};Pattern 2: Re-fetch on Action
const handleCreatePost = async (postData) => {
await createPost(postData);
const newPosts = await getPosts(); // Re-fetch all posts
setPosts(newPosts);
};Pattern 3: Optimistic Update
const handleLike = async () => {
const prev = liked;
setLiked(!liked); // Optimistic
try {
const result = await likePost(postId);
setLiked(result.is_liked); // Server truth
} catch {
setLiked(prev); // Rollback
}
};+-------------+ +-------------+ +-------------+ +-------------+
| Login.jsx |---->| api.js |---->| Backend |---->| localStorage|
| | | login() | | /auth/login | | |
+-------------+ +-------------+ +-------------+ +-------------+
| | |
| | |
|<------------ { token, user } ----------| |
| |
|-------------- store token -------------------------------->|
| |
|-------------- store user --------------------------------->|
| |
v |
+-------------+ |
| App.jsx |<------------- onLogin(user) -----------------------|
| setCurrentUser |
+-------------+
|
v
+-------------+
| /home |
| Navigate |
+-------------+
// On login success
localStorage.setItem('token', response.token);
localStorage.setItem('user', JSON.stringify(response.user));
// On app load (App.jsx)
const token = localStorage.getItem('token');
const userData = localStorage.getItem('user');
if (token && userData) {
const freshUserData = await getCurrentUser(); // Verify token still valid
setCurrentUser(freshUserData);
}
// On logout
localStorage.removeItem('token');
localStorage.removeItem('user');Every API request automatically includes the token:
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});User Action Component API Backend
| | | |
| Click Create | | |
|------------------>| | |
| | | |
| | [If has image] | |
| |--uploadImage()---->|--/upload/image------>|
| |<---------------------{ url }--------------|
| | | |
| |--createPost()----->|--/posts/------------>|
| |<---------------------{ post }-------------|
| | | |
| |--getPosts()------->|--/posts/------------>|
| |<---------------------[posts]--------------|
| | | |
| | setPosts(posts) | |
| | onRefreshStats() | |
|<--UI Updated------| | |
User Action Component API Backend
| | | |
| Click Follow | | |
|------------------>| | |
| | | |
| | setIsFollowing | |
| | (optimistic) | |
| | | |
| |--checkFollowStatus>|--/follows/check/{id}>|
| |<----------------------{ is_following }----|
| | | |
| | [If following] | |
| |--DELETE /follows/-->|---------------------->|
| | [If not following]| |
| |--POST /follows/--->|---------------------->|
| | | |
| | setIsFollowing | |
| | (server value) | |
| | | |
| | onRefreshStats() | (Update sidebar) |
|<--UI Updated------| | |
Custom Colors (tailwind.config.js):
colors: {
dark: {
bg: '#0a0a0a', // Main background
card: '#141414', // Card backgrounds
border: '#262626', // Border color
hover: '#1a1a1a', // Hover states
}
}Font Family:
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
}| Use Case | Color Class |
|---|---|
| Page background | bg-dark-bg |
| Card background | bg-dark-card |
| Borders | border-dark-border |
| Hover states | hover:bg-dark-hover |
| Primary text | text-neutral-50 |
| Secondary text | text-neutral-400 |
| Muted text | text-neutral-500 |
| Disabled text | text-neutral-600 |
| Active button | bg-neutral-50 text-neutral-900 |
.scrollbar-custom::-webkit-scrollbar {
width: 6px;
}
.scrollbar-custom::-webkit-scrollbar-track {
background: #1a1a1a;
}
.scrollbar-custom::-webkit-scrollbar-thumb {
background: #404040;
border-radius: 3px;
}The frontend uses a "graceful degradation" approach:
- API calls never force redirects - The response interceptor logs errors but doesn't redirect
- Components handle their own errors - Each component decides how to respond to failures
- Fallback data - Components often fall back to cached/default data on failure
// Example: User data with fallback
const freshUserData = await getCurrentUser();
if (freshUserData) {
setCurrentUser(freshUserData);
} else {
// Fallback to localStorage
const user = JSON.parse(localStorage.getItem('user'));
setCurrentUser(user);
}The API service normalizes backend responses:
// Login response normalization
return {
success: true,
user: response.data.user,
token: response.data.access_token, // Backend uses 'access_token'
};
// Follow status handling (supports both formats)
setIsFollowing(userData?.is_following || userData?.isFollowing || false);Problem: Sidebar follower/following counts didn't update after follow actions.
Root Cause: App.jsx user state wasn't refreshed after follow actions in child components.
Solution:
- Created
refreshUserStats()function in App.jsx - Passed as
onRefreshStatsprop to relevant pages - Child components call
onRefreshStats()after successful follow actions
// App.jsx
const refreshUserStats = async () => {
const freshUserData = await getCurrentUser();
if (freshUserData) {
setCurrentUser(freshUserData);
localStorage.setItem('user', JSON.stringify(freshUserData));
}
};
// Profile.jsx
const handleFollow = async () => {
const result = await followUser(userId);
if (result.success && onRefreshStats) {
onRefreshStats(); // Refresh sidebar
}
};Problem: API calls failed with 401 after page refresh despite having a token.
Root Cause: Token wasn't being attached to requests properly.
Solution:
- Added request interceptor to automatically attach tokens
- Added console logging for debugging token flow
- Ensured token is read fresh from localStorage on each request
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});Problem: Frontend crashes when backend uses different field names.
Root Cause: Backend uses snake_case (is_following), some code expected camelCase.
Solution: Added fallback handling for both formats:
setIsFollowing(userData?.is_following || userData?.isFollowing || false);Problem: Post images couldn't be uploaded.
Solution:
- Created dedicated
uploadImage()function using FormData - Set proper
Content-Type: multipart/form-dataheader - Backend created
/upload/imageendpoint with static file serving
export const uploadImage = async (file) => {
const formData = new FormData();
formData.append('file', file);
const response = await axios.post(`${API_BASE_URL}/upload/image`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
'Authorization': `Bearer ${token}`
}
});
return { success: true, url: response.data.url };
};Problem: UI showed incorrect state when API call failed after optimistic update.
Solution: Implemented rollback pattern:
const handleLike = async () => {
const prevLiked = liked;
const prevCount = likesCount;
setLiked(!liked);
setLikesCount(liked ? likesCount - 1 : likesCount + 1);
try {
const result = await likePost(postId);
setLiked(result.is_liked);
} catch (error) {
setLiked(prevLiked);
setLikesCount(prevCount);
}
};- Node.js 18.x or higher
- npm or yarn
cd frontend
npm installnpm run devThe application will be available at http://localhost:5173
npm run build
npm run preview # Preview production buildThe API base URL is hardcoded in api.js:
const API_BASE_URL = 'http://localhost:8000';For production, this should be configured via environment variables.
| Script | Command | Purpose |
|---|---|---|
| dev | vite |
Start development server |
| build | vite build |
Create production build |
| preview | vite preview |
Preview production build |
| lint | eslint . |
Run ESLint |
App.jsx
├── Navbar.jsx
├── Sidebar.jsx
└── Routes
├── Login.jsx
├── Home.jsx
│ ├── PostCard.jsx
│ │ └── CommentSection.jsx
│ ├── CreatePost.jsx
│ ├── UserCard.jsx
│ └── Toast.jsx
├── Profile.jsx
│ └── PostCard.jsx
│ └── CommentSection.jsx
├── Messages.jsx
├── Notifications.jsx
├── Explore.jsx
│ └── UserCard.jsx
├── Trending.jsx
├── Saved.jsx
│ └── PostCard.jsx
└── Settings.jsx
| File | Lines | Purpose |
|---|---|---|
| api.js | ~615 | Complete API service layer |
| App.jsx | ~150 | Root component with routing |
| Home.jsx | ~265 | Main feed page |
| Profile.jsx | ~260 | User profile page |
| PostCard.jsx | ~200 | Post display component |
| CreatePost.jsx | ~215 | Post creation modal |
| Login.jsx | ~250 | Authentication page |
| Settings.jsx | ~400 | User settings page |
| Sidebar.jsx | ~95 | Navigation sidebar |
| Navbar.jsx | ~95 | Top navigation |
This frontend is part of the Pulse social media platform project.