React Frontend for Document Management
System
We build a React application that interfaces with the provided document-management backend. The project
uses JWT-based authentication, React Router for navigation, Tailwind CSS for styling, and supports file
upload/preview. We organize the src/ directory into logical folders (components, pages, services, etc.)
following React best practices 1 . Environment variables (e.g. REACT_APP_API_URL ) are used for API
endpoints 2 , and responsive design is achieved via Tailwind’s breakpoint prefixes (e.g. md: for medium
screens) as in the official docs 3 .
Project Structure:
my-app/
├── public/
│ └── index.html # Main HTML file
└── src/
├── components/ # Reusable UI components (Navbar, ProtectedRoute,
etc.)
├── pages/ # Route/page components (Login, Signup, Dashboard,
etc.)
├── services/ # API service modules (api.js, auth.js,
documents.js)
├── context/ # React Context for auth (AuthContext.js)
├── App.js # Main app with Routes
├── index.js # Entry point (renders App inside BrowserRouter)
└── index.css # Tailwind CSS imports and custom styles
JWT Authentication & Protected Routes
We implement JWT login/signup by sending credentials to the backend and storing the returned token in
local storage. A React Context ( AuthContext ) wraps the app to hold authentication state and provides
login / logout functions. Upon login, the token is stored and the user is considered authenticated. We
protect sensitive pages with a ProtectedRoute component: if the user is not logged in (no valid token),
we redirect them to the login page. This follows standard React Router v6 practices where protected routes
restrict access to authenticated users 4 . We decode the JWT (or fetch user info) to display the user profile
as needed.
// src/context/AuthContext.js
import React, { createContext, useContext, useState, useEffect } from 'react';
1
// Create AuthContext to hold auth state
const AuthContext = createContext();
// Provider component wraps the app and provides auth state
export const AuthProvider = ({ children }) => {
// Initialize token from localStorage (if any)
const [token, setToken] = useState(() => localStorage.getItem('token'));
// When token changes, update localStorage
useEffect(() => {
if (token) localStorage.setItem('token', token);
else localStorage.removeItem('token');
}, [token]);
// Login function (save token and redirect to dashboard)
const login = (newToken) => {
setToken(newToken);
};
// Logout function (clear token)
const logout = () => {
setToken(null);
};
return (
<AuthContext.Provider value={{ token, login, logout }}>
{children}
</AuthContext.Provider>
);
};
// Custom hook to use auth context
export const useAuth = () => {
return useContext(AuthContext);
};
// src/components/ProtectedRoute.js
import React from 'react';
import { useAuth } from '../context/AuthContext';
import { Navigate } from 'react-router-dom';
/**
* ProtectedRoute wraps a component and redirects to /login if not
authenticated.
* Only allows access if a token is present (i.e., user is logged in).
2
*/
export const ProtectedRoute = ({ children }) => {
const { token } = useAuth();
if (!token) {
// User not authenticated -> redirect to login
return <Navigate to="/login" replace />;
}
return children;
};
These components ensure that only logged-in users can access certain routes (as shown in the LogRocket
tutorial 4 ).
API Service Modules
We use Axios to interact with the backend. The api.js module sets up a base URL, and we add token
headers for protected requests. Separate service files encapsulate authentication and document APIs to
match the backend routes exactly (e.g., /auth/login , /auth/register , /documents , etc.).
// src/services/api.js
import axios from 'axios';
// Base URL from environment (adjust as needed)
const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:5000/api';
// Create an Axios instance
const api = axios.create({
baseURL: API_URL,
});
export default api;
// src/services/auth.js
import api from './api';
/**
* Send login request to backend.
* Expects { email, password } and returns { token } on success.
*/
export const login = async ({ email, password }) => {
const res = await api.post('/auth/login', { email, password });
return res.data; // { token }
};
/**
3
* Send signup request to backend.
* Expects { name, email, password } and returns { token } on success.
*/
export const signup = async ({ name, email, password }) => {
const res = await api.post('/auth/register', { name, email, password });
return res.data; // { token }
};
// src/services/documents.js
import api from './api';
/**
* Get list of documents for the logged-in user.
* Adds the JWT token in Authorization header.
*/
export const getDocuments = async (token) => {
const res = await api.get('/documents', {
headers: { Authorization: `Bearer ${token}` },
});
return res.data; // e.g. { documents: [...] }
};
/**
* Upload a new document (PDF/image).
* `formData` is a FormData object with file and metadata.
*/
export const uploadDocument = async (formData, token) => {
const res = await api.post('/documents', formData, {
headers: {
'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${token}`,
},
});
return res.data; // e.g. { message: 'Uploaded' }
};
We retrieve the JWT token from AuthContext when calling these services, and include it in the
Authorization header. This ensures proper alignment with the backend’s expected routes and security.
Routing with React Router
In App.js , we define routes using React Router v6. Public routes include /login and /signup . All
other main pages (dashboard, upload, profile) are wrapped in ProtectedRoute so that only
authenticated users can view them 4 . We also render a navigation bar on protected pages for easy access.
4
// src/App.js
import React from 'react';
import { Routes, Route, BrowserRouter } from 'react-router-dom';
import { AuthProvider } from './context/AuthContext';
import LoginPage from './pages/LoginPage';
import SignupPage from './pages/SignupPage';
import DashboardPage from './pages/DashboardPage';
import UploadPage from './pages/UploadPage';
import ProfilePage from './pages/ProfilePage';
import { ProtectedRoute } from './components/ProtectedRoute';
// Wrap App with Router and AuthProvider
function App() {
return (
<BrowserRouter>
<AuthProvider>
<Routes>
{/* Public routes */}
<Route path="/login" element={<LoginPage />} />
<Route path="/signup" element={<SignupPage />} />
{/* Protected routes */}
<Route path="/" element={<ProtectedRoute><DashboardPage /></
ProtectedRoute>} />
<Route path="/upload" element={<ProtectedRoute><UploadPage /></
ProtectedRoute>} />
<Route path="/profile" element={<ProtectedRoute><ProfilePage /></
ProtectedRoute>} />
</Routes>
</AuthProvider>
</BrowserRouter>
);
}
export default App;
In the example above, accessing / , /upload , or /profile will check if the user is authenticated; if
not, they are redirected to /login . This pattern is recommended for React Router v6 protected routes
4 .
Components and Pages
We create component-based pages for each part of the UI. Tailwind CSS classes are used throughout for
styling and responsive layout. Below are excerpts of key files:
5
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css'; // Tailwind styles
ReactDOM.render(<App />, document.getElementById('root'));
/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* You can add global styles or custom utilities here if needed */
// src/components/Navbar.js
import React from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
/**
* Navigation bar shown on protected pages.
* Links to Dashboard, Upload, Profile, and Logout.
*/
const Navbar = () => {
const { logout } = useAuth();
const navigate = useNavigate();
const handleLogout = () => {
logout();
navigate('/login');
};
return (
<nav className="bg-blue-600 p-4 flex justify-between items-center">
<div className="flex space-x-4">
<Link to="/" className="text-white font-semibold">Dashboard</Link>
<Link to="/upload" className="text-white font-semibold">Upload</Link>
<Link to="/profile" className="text-white font-semibold">Profile</Link>
</div>
<button onClick={handleLogout} className="text-white">Logout</button>
</nav>
);
};
6
export default Navbar;
// src/pages/LoginPage.js
import React, { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { login as loginUser } from '../services/auth';
import { useAuth } from '../context/AuthContext';
/**
* Login page with email/password fields.
* On success, saves token via AuthContext and redirects to Dashboard.
*/
const LoginPage = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { login } = useAuth();
const navigate = useNavigate();
const [error, setError] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
try {
const data = await loginUser({ email, password });
login(data.token); // Save token in context/localStorage
navigate('/'); // Go to dashboard
} catch (err) {
setError(err.response?.data?.message || 'Login failed');
}
};
return (
<div className="max-w-md mx-auto mt-16 p-4 border rounded">
<h2 className="text-2xl mb-4">Login</h2>
{error && <p className="text-red-500">{error}</p>}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label>Email:</label>
<input
type="email" value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full border px-2 py-1"
required
/>
</div>
<div>
7
<label>Password:</label>
<input
type="password" value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full border px-2 py-1"
required
/>
</div>
<button type="submit" className="bg-blue-600 text-white px-4
py-2">Login</button>
</form>
<p className="mt-4">
Don't have an account? <Link to="/signup" className="text-
blue-600">Sign Up</Link>
</p>
</div>
);
};
export default LoginPage;
// src/pages/SignupPage.js
import React, { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { signup as signupUser } from '../services/auth';
import { useAuth } from '../context/AuthContext';
/**
* Signup page for new users.
* On success, also logs in the user by saving the JWT.
*/
const SignupPage = () => {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { login } = useAuth();
const navigate = useNavigate();
const [error, setError] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
try {
const data = await signupUser({ name, email, password });
login(data.token);
navigate('/');
} catch (err) {
8
setError(err.response?.data?.message || 'Signup failed');
}
};
return (
<div className="max-w-md mx-auto mt-16 p-4 border rounded">
<h2 className="text-2xl mb-4">Sign Up</h2>
{error && <p className="text-red-500">{error}</p>}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label>Name:</label>
<input
type="text" value={name}
onChange={(e) => setName(e.target.value)}
className="w-full border px-2 py-1"
required
/>
</div>
<div>
<label>Email:</label>
<input
type="email" value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full border px-2 py-1"
required
/>
</div>
<div>
<label>Password:</label>
<input
type="password" value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full border px-2 py-1"
required
/>
</div>
<button type="submit" className="bg-green-600 text-white px-4
py-2">Register</button>
</form>
<p className="mt-4">
Already have an account? <Link to="/login" className="text-
blue-600">Login</Link>
</p>
</div>
);
};
export default SignupPage;
9
// src/pages/DashboardPage.js
import React, { useEffect, useState } from 'react';
import { getDocuments } from '../services/documents';
import { useAuth } from '../context/AuthContext';
import Navbar from '../components/Navbar';
/**
* Dashboard page shows the list of uploaded documents.
*/
const DashboardPage = () => {
const { token } = useAuth();
const [documents, setDocuments] = useState([]);
const [error, setError] = useState('');
useEffect(() => {
// Fetch documents from API on mount
const fetchData = async () => {
try {
const data = await getDocuments(token);
setDocuments(data.documents || []);
} catch (err) {
setError('Failed to load documents');
}
};
fetchData();
}, [token]);
return (
<div>
<Navbar />
<div className="max-w-4xl mx-auto mt-8">
<h1 className="text-3xl mb-4">My Documents</h1>
{error && <p className="text-red-500">{error}</p>}
{documents.length === 0 ? (
<p>No documents uploaded yet.</p>
) : (
<ul className="space-y-2">
{documents.map(doc => (
<li key={doc._id} className="border p-2 flex justify-between
items-center">
<span>{doc.originalName}</span>
{/* Clicking opens the document in a new tab */}
<a
href={`${process.env.REACT_APP_API_URL || 'http://localhost:
5000'}/api/documents/${doc._id}`}
target="_blank"
rel="noopener noreferrer"
10
className="text-blue-600"
>
Preview
</a>
</li>
))}
</ul>
)}
</div>
</div>
);
};
export default DashboardPage;
// src/pages/UploadPage.js
import React, { useState } from 'react';
import { useAuth } from '../context/AuthContext';
import { uploadDocument } from '../services/documents';
import Navbar from '../components/Navbar';
/**
* Upload page allows the user to select a PDF or image file and upload it.
* It also previews the selected file using URL.createObjectURL 5 6 .
*/
const UploadPage = () => {
const { token } = useAuth();
const [file, setFile] = useState(null); // The selected file object
const [previewUrl, setPreviewUrl] = useState(''); // URL for previewing
const [message, setMessage] = useState('');
const handleFileChange = (e) => {
const selected = e.target.files[0];
if (!selected) return;
setFile(selected);
// Create a temporary URL for preview (supports images and PDFs)
setPreviewUrl(URL.createObjectURL(selected));
};
const handleUpload = async (e) => {
e.preventDefault();
if (!file) return;
const formData = new FormData();
formData.append('file', file);
try {
await uploadDocument(formData, token);
11
setMessage('Upload successful!');
} catch (err) {
setMessage('Upload failed.');
}
};
return (
<div>
<Navbar />
<div className="max-w-md mx-auto mt-8 p-4 border rounded">
<h2 className="text-2xl mb-4">Upload Document</h2>
{message && <p>{message}</p>}
<form onSubmit={handleUpload} className="space-y-4">
<input
type="file"
accept=".pdf,image/*"
onChange={handleFileChange}
className="border"
/>
{/* Preview area: image or embedded PDF */}
{previewUrl && (
<div className="mt-4">
{file.type === 'application/pdf' ? (
<iframe
src={previewUrl}
title="PDF Preview"
className="w-full h-64 border"
></iframe>
) : (
<img src={previewUrl} alt="Preview" className="max-w-full h-
auto" />
)}
</div>
)}
<button type="submit" className="bg-blue-600 text-white px-4
py-2">Upload</button>
</form>
</div>
</div>
);
};
export default UploadPage;
// src/pages/ProfilePage.js
import React from 'react';
12
import { useAuth } from '../context/AuthContext';
import Navbar from '../components/Navbar';
/**
* Profile page displays user information.
* For simplicity, we decode the JWT to show the user's email (or other data).
*/
const ProfilePage = () => {
const { token } = useAuth();
let email = '';
if (token) {
try {
// Decode JWT payload (assumes base64-encoded JSON payload)
const payload = JSON.parse(atob(token.split('.')[1]));
email = payload.email || '';
} catch (e) {
console.error('Failed to decode token');
}
}
return (
<div>
<Navbar />
<div className="max-w-md mx-auto mt-8 p-4 border rounded">
<h2 className="text-2xl mb-4">User Profile</h2>
<p><strong>Email:</strong> {email}</p>
{/* Add more user fields as needed */}
</div>
</div>
);
};
export default ProfilePage;
Responsive Design with Tailwind
All components use Tailwind’s utility classes to ensure responsiveness. For example, we use container
classes and responsive widths ( w-full , max-w-md , etc.), and prepend breakpoint prefixes ( sm: , md: ,
etc.) where needed. According to Tailwind’s documentation, “every utility class in Tailwind can be applied
conditionally at different breakpoints” 3 . Thus our layout gracefully adapts to mobile and desktop screen
sizes.
13
Notes:
• We include the Tailwind CSS directives in index.css so that all utility classes are available. No
extra CSS files are needed thanks to Tailwind’s utility-first approach.
• The upload form uses the browser’s URL.createObjectURL API to show a preview of the chosen
file before submitting 5 6 .
• Navigation ( Navbar ) is hidden on login/signup and shown on protected pages to guide the user
around (Dashboard, Upload, Profile, Logout).
• All API calls attach the JWT as Authorization: Bearer <token> . The backend routes and
payload structures (e.g. { email, password } , { file } ) are matched exactly so the front
end works seamlessly with the existing backend.
• This component-based structure (separate files under components/ and pages/ ) follows
recommended React project organization for maintainability 1 .
By following these patterns and integrating the pieces above, the React frontend fully supports JWT
authentication, file upload/preview, protected routing, and a responsive UI – meeting all specified
requirements.
Sources: React Router and Auth practices 4 7 , Tailwind CSS utilities 3 , and React file preview
techniques 5 6 .
1 2 Optimal Structure for React & Tailwind Project | by Silas Jones | Medium
https://starmastar1126.medium.com/optimal-structure-for-react-tailwind-project-c77ce0dc17de
3 Responsive design - Core concepts - Tailwind CSS
https://tailwindcss.com/docs/responsive-design
4 Authentication with React Router v6: A complete guide - LogRocket Blog
https://blog.logrocket.com/authentication-react-router-v6/
5 Preview a PDF file before uploading - React PDF Viewer
https://react-pdf-viewer.dev/examples/preview-a-pdf-file-before-uploading/
6 How to Upload Image and Preview it Using ReactJS? | GeeksforGeeks
https://www.geeksforgeeks.org/how-to-upload-image-and-preview-it-using-reactjs/
7 Using JWT for authentication in React
https://blog.openreplay.com/using-jwt-for-authentication-in-react/
14