refactor: update styles in MembersDirectory and NewsletterArchive for consistency and improved theming
- Updated color classes to use CSS variables for better maintainability and theming. - Refactored component styles in MembersDirectory.js to enhance visual consistency. - Adjusted loading states and empty states in NewsletterArchive.js for improved user experience. - Added new brand colors to tailwind.config.js for future use.
This commit is contained in:
243
README.md
243
README.md
@@ -33,6 +33,7 @@ npm install
|
||||
```
|
||||
|
||||
**Key Dependencies:**
|
||||
|
||||
- `react@19.0.0` - UI library
|
||||
- `react-router-dom@7.5.1` - Routing
|
||||
- `axios@1.8.4` - HTTP client
|
||||
@@ -57,6 +58,7 @@ REACT_APP_BACKEND_URL=http://localhost:8000
|
||||
```
|
||||
|
||||
**Important:**
|
||||
|
||||
- All environment variables must start with `REACT_APP_`
|
||||
- Restart development server after changing `.env`
|
||||
|
||||
@@ -71,6 +73,7 @@ npm start
|
||||
```
|
||||
|
||||
**Development server will be available at:**
|
||||
|
||||
- Frontend: http://localhost:3000
|
||||
- Auto-reloads on file changes
|
||||
|
||||
@@ -154,31 +157,33 @@ frontend/
|
||||
|
||||
### Core Technologies
|
||||
|
||||
| Technology | Purpose | Version |
|
||||
|------------|---------|---------|
|
||||
| React | UI library | 19.0.0 |
|
||||
| React Router | Client-side routing | 7.5.1 |
|
||||
| Axios | HTTP requests | 1.8.4 |
|
||||
| React Hook Form | Form handling | 7.56.2 |
|
||||
| Zod | Schema validation | 3.24.4 |
|
||||
| Tailwind CSS | CSS framework | 3.4.17 |
|
||||
| Radix UI | Component library | Latest |
|
||||
| Lucide React | Icons | 0.507.0 |
|
||||
| Sonner | Toast notifications | 1.7.4 |
|
||||
| Technology | Purpose | Version |
|
||||
| --------------- | ------------------- | ------- |
|
||||
| React | UI library | 19.0.0 |
|
||||
| React Router | Client-side routing | 7.5.1 |
|
||||
| Axios | HTTP requests | 1.8.4 |
|
||||
| React Hook Form | Form handling | 7.56.2 |
|
||||
| Zod | Schema validation | 3.24.4 |
|
||||
| Tailwind CSS | CSS framework | 3.4.17 |
|
||||
| Radix UI | Component library | Latest |
|
||||
| Lucide React | Icons | 0.507.0 |
|
||||
| Sonner | Toast notifications | 1.7.4 |
|
||||
|
||||
### Key Features
|
||||
|
||||
#### Authentication System
|
||||
|
||||
**Global Auth Context** (`src/context/AuthContext.js`):
|
||||
|
||||
- JWT token storage in localStorage
|
||||
- Automatic token injection via Axios interceptor
|
||||
- User state management
|
||||
- Protected route wrapper
|
||||
|
||||
**Usage:**
|
||||
|
||||
```jsx
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
|
||||
function MyComponent() {
|
||||
const { user, login, logout, isAuthenticated } = useAuth();
|
||||
@@ -190,6 +195,7 @@ function MyComponent() {
|
||||
#### Protected Routes
|
||||
|
||||
**PrivateRoute Wrapper:**
|
||||
|
||||
```jsx
|
||||
<Route path="/dashboard" element={
|
||||
<PrivateRoute>
|
||||
@@ -208,61 +214,68 @@ function MyComponent() {
|
||||
#### API Integration
|
||||
|
||||
**Axios Instance** (`src/utils/api.js`):
|
||||
|
||||
- Automatic JWT token injection
|
||||
- Request/response interceptors
|
||||
- Error handling
|
||||
- Base URL configuration
|
||||
|
||||
**Usage:**
|
||||
|
||||
```jsx
|
||||
import api from '../utils/api';
|
||||
import api from "../utils/api";
|
||||
|
||||
// GET request
|
||||
const response = await api.get('/members/profile');
|
||||
const response = await api.get("/members/profile");
|
||||
|
||||
// POST request with data
|
||||
const response = await api.post('/admin/users/123/reject', {
|
||||
reason: 'Incomplete application'
|
||||
const response = await api.post("/admin/users/123/reject", {
|
||||
reason: "Incomplete application",
|
||||
});
|
||||
|
||||
// File upload
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const response = await api.post('/members/profile/upload-photo', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
formData.append("file", file);
|
||||
const response = await api.post("/members/profile/upload-photo", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
```
|
||||
|
||||
#### Form Handling
|
||||
|
||||
**React Hook Form + Zod Pattern:**
|
||||
|
||||
```jsx
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email('Invalid email address'),
|
||||
password: z.string().min(8, 'Password must be at least 8 characters')
|
||||
email: z.string().email("Invalid email address"),
|
||||
password: z.string().min(8, "Password must be at least 8 characters"),
|
||||
});
|
||||
|
||||
function LoginForm() {
|
||||
const { register, handleSubmit, formState: { errors } } = useForm({
|
||||
resolver: zodResolver(schema)
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
try {
|
||||
await api.post('/auth/login', data);
|
||||
toast.success('Login successful!');
|
||||
await api.post("/auth/login", data);
|
||||
toast.success("Login successful!");
|
||||
} catch (error) {
|
||||
toast.error(error.response?.data?.detail || 'Login failed');
|
||||
toast.error(error.response?.data?.detail || "Login failed");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Input {...register('email')} />
|
||||
<Input {...register("email")} />
|
||||
{errors.email && <span>{errors.email.message}</span>}
|
||||
{/* ... */}
|
||||
</form>
|
||||
@@ -273,18 +286,19 @@ function LoginForm() {
|
||||
#### Toast Notifications
|
||||
|
||||
**Using Sonner:**
|
||||
|
||||
```jsx
|
||||
import { toast } from 'sonner';
|
||||
import { toast } from "sonner";
|
||||
|
||||
// Success
|
||||
toast.success('Profile updated successfully!');
|
||||
toast.success("Profile updated successfully!");
|
||||
|
||||
// Error
|
||||
toast.error('Failed to upload photo');
|
||||
toast.error("Failed to upload photo");
|
||||
|
||||
// Custom
|
||||
toast('Processing...', {
|
||||
description: 'Please wait while we process your request'
|
||||
toast("Processing...", {
|
||||
description: "Please wait while we process your request",
|
||||
});
|
||||
```
|
||||
|
||||
@@ -293,11 +307,13 @@ toast('Processing...', {
|
||||
#### Public Pages
|
||||
|
||||
**Landing.js**
|
||||
|
||||
- Hero section with LOAF branding
|
||||
- Feature highlights
|
||||
- Call-to-action buttons
|
||||
|
||||
**Register.js** (388 lines)
|
||||
|
||||
- 4-step registration wizard:
|
||||
1. Basic Info (email, password, name)
|
||||
2. Personal Details (phone, address, DOB)
|
||||
@@ -308,6 +324,7 @@ toast('Processing...', {
|
||||
- Email verification trigger
|
||||
|
||||
**Login.js**
|
||||
|
||||
- Email/password authentication
|
||||
- JWT token storage
|
||||
- Remember me functionality
|
||||
@@ -316,12 +333,14 @@ toast('Processing...', {
|
||||
#### Member Pages
|
||||
|
||||
**Dashboard.js**
|
||||
|
||||
- Welcome message with user name
|
||||
- Upcoming events preview
|
||||
- Membership status card
|
||||
- Quick action buttons
|
||||
|
||||
**Profile.js**
|
||||
|
||||
- Profile photo upload with Avatar component
|
||||
- File validation (image types, 50MB max)
|
||||
- Personal information editing
|
||||
@@ -329,12 +348,14 @@ toast('Processing...', {
|
||||
- Save changes with API update
|
||||
|
||||
**Events.js**
|
||||
|
||||
- Event listing with filters
|
||||
- Search functionality
|
||||
- Upcoming/past events toggle
|
||||
- Event cards with cover images
|
||||
|
||||
**EventDetails.js**
|
||||
|
||||
- Full event information
|
||||
- RSVP form (Yes/No/Maybe)
|
||||
- Attendance confirmation
|
||||
@@ -343,36 +364,42 @@ toast('Processing...', {
|
||||
#### Admin Pages
|
||||
|
||||
**AdminDashboard.js**
|
||||
|
||||
- Statistics overview
|
||||
- Pending validations count
|
||||
- Recent activity feed
|
||||
- Quick links to management pages
|
||||
|
||||
**AdminUsers.js**
|
||||
|
||||
- User listing with search/filters
|
||||
- Status badges
|
||||
- Bulk operations
|
||||
- CSV export
|
||||
|
||||
**AdminValidations.js**
|
||||
|
||||
- Pending user applications
|
||||
- Approve button
|
||||
- **Reject button with RejectionDialog**
|
||||
- Validation workflow management
|
||||
|
||||
**AdminSubscriptions.js**
|
||||
|
||||
- Subscription listing
|
||||
- Status filters (active, cancelled, expired)
|
||||
- **CSV export dropdown** (Export All / Export Current View)
|
||||
- Edit subscription dialog
|
||||
|
||||
**AdminDonations.js** (400+ lines)
|
||||
|
||||
- 4 stats cards: Total, Member, Public, Total Amount
|
||||
- Filters: type, status, date range, search
|
||||
- Responsive table with mobile cards
|
||||
- **CSV export functionality**
|
||||
|
||||
**AdminEvents.js**
|
||||
|
||||
- Event management interface
|
||||
- Create/edit events
|
||||
- Publish/unpublish toggle
|
||||
@@ -385,40 +412,50 @@ toast('Processing...', {
|
||||
**45+ UI Components** in `src/components/ui/`:
|
||||
|
||||
**Form Components:**
|
||||
|
||||
- Button, Input, Textarea, Checkbox, Radio
|
||||
- Select, Label, Form
|
||||
|
||||
**Layout Components:**
|
||||
|
||||
- Card, Dialog, Sheet (Drawer), Popover
|
||||
- Dropdown Menu, Tabs, Accordion
|
||||
|
||||
**Feedback Components:**
|
||||
|
||||
- Alert, Badge, Toast (Sonner)
|
||||
- Progress, Skeleton, Spinner
|
||||
|
||||
**Navigation:**
|
||||
|
||||
- Navigation Menu, Breadcrumb, Pagination
|
||||
|
||||
**Usage Example:**
|
||||
```jsx
|
||||
import { Button } from './components/ui/button';
|
||||
import { Dialog, DialogContent, DialogTitle } from './components/ui/dialog';
|
||||
import { Select, SelectTrigger, SelectContent, SelectItem } from './components/ui/select';
|
||||
|
||||
<Button className="bg-[#664fa3] text-white">
|
||||
Click me
|
||||
</Button>
|
||||
```jsx
|
||||
import { Button } from "./components/ui/button";
|
||||
import { Dialog, DialogContent, DialogTitle } from "./components/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
} from "./components/ui/select";
|
||||
|
||||
<Button className="bg-var(--purple-lavender) text-white">Click me</Button>;
|
||||
```
|
||||
|
||||
### Admin Sidebar Features
|
||||
|
||||
**Logo Integration:**
|
||||
|
||||
- LOAF logo in header
|
||||
- Shows logo + "Admin" text when expanded
|
||||
- Logo only when collapsed
|
||||
- Smooth transition animations
|
||||
|
||||
**Navigation Groups:**
|
||||
|
||||
- **Dashboard** (standalone)
|
||||
- **MEMBERSHIP** - Staff, Members, Validations
|
||||
- **FINANCIALS** - Plans, Subscriptions, Donations
|
||||
@@ -433,6 +470,7 @@ import { Select, SelectTrigger, SelectContent, SelectItem } from './components/u
|
||||
### Color Palette
|
||||
|
||||
**LOAF Brand Colors:**
|
||||
|
||||
```css
|
||||
--primary: #422268 /* Deep Purple - Primary brand */
|
||||
--secondary: #664fa3 /* Light Purple - Secondary elements */
|
||||
@@ -443,21 +481,24 @@ import { Select, SelectTrigger, SelectContent, SelectItem } from './components/u
|
||||
```
|
||||
|
||||
**Usage in Tailwind:**
|
||||
|
||||
```jsx
|
||||
<div className="bg-[#422268] text-white">
|
||||
<h1 className="text-[#ff9e77]">Accent Text</h1>
|
||||
<p className="text-[#664fa3]">Secondary Text</p>
|
||||
<div className="bg-var(--purple-ink) text-white">
|
||||
<h1 className="text-var(--orange-light)">Accent Text</h1>
|
||||
<p className="text-var(--purple-lavender)">Secondary Text</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Typography
|
||||
|
||||
**Font Families:**
|
||||
|
||||
- **Headings**: 'Inter', sans-serif
|
||||
- **Body**: 'Nunito Sans', sans-serif
|
||||
- **Code**: 'Fira Code', monospace (if needed)
|
||||
|
||||
**Font Sizes:**
|
||||
|
||||
```css
|
||||
text-xs → 0.75rem (12px)
|
||||
text-sm → 0.875rem (14px)
|
||||
@@ -469,11 +510,12 @@ text-3xl → 1.875rem (30px)
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```jsx
|
||||
<h1 className="text-3xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-3xl font-semibold text-var(--purple-ink)" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Page Title
|
||||
</h1>
|
||||
<p className="text-base text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-base text-var(--purple-lavender)" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Body text
|
||||
</p>
|
||||
```
|
||||
@@ -481,6 +523,7 @@ text-3xl → 1.875rem (30px)
|
||||
### Spacing System
|
||||
|
||||
**Tailwind Spacing Scale:**
|
||||
|
||||
```css
|
||||
p-2 → 0.5rem (8px)
|
||||
p-4 → 1rem (16px)
|
||||
@@ -493,21 +536,23 @@ gap-2, gap-4, gap-6, gap-8 (same scale for flex/grid gaps)
|
||||
### Component Styling Patterns
|
||||
|
||||
**Cards:**
|
||||
|
||||
```jsx
|
||||
<Card className="p-6 bg-white rounded-2xl border-2 border-[#ddd8eb]">
|
||||
<Card className="p-6 bg-background rounded-2xl border-2 border-var(--neutral-800)">
|
||||
{/* Content */}
|
||||
</Card>
|
||||
```
|
||||
|
||||
**Buttons:**
|
||||
|
||||
```jsx
|
||||
// Primary
|
||||
<Button className="bg-[#664fa3] text-white hover:bg-[#422268] rounded-full px-6 py-3">
|
||||
<Button className="bg-var(--purple-lavender) text-white hover:bg-var(--purple-ink) rounded-full px-6 py-3">
|
||||
Primary Action
|
||||
</Button>
|
||||
|
||||
// Secondary
|
||||
<Button variant="outline" className="border-2 border-[#ddd8eb] text-[#664fa3] hover:bg-[#f1eef9] rounded-full">
|
||||
<Button variant="outline" className="border-2 border-var(--neutral-800) text-var(--purple-lavender) hover:bg-var(--lavender-300) rounded-full">
|
||||
Secondary Action
|
||||
</Button>
|
||||
|
||||
@@ -518,17 +563,19 @@ gap-2, gap-4, gap-6, gap-8 (same scale for flex/grid gaps)
|
||||
```
|
||||
|
||||
**Form Inputs:**
|
||||
|
||||
```jsx
|
||||
<Input className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" />
|
||||
<Textarea className="rounded-xl border-2 border-[#ddd8eb] min-h-[120px]" />
|
||||
<Input className="rounded-xl border-2 border-var(--neutral-800) focus:border-var(--purple-lavender)" />
|
||||
<Textarea className="rounded-xl border-2 border-var(--neutral-800) min-h-[120px]" />
|
||||
<Select>
|
||||
<SelectTrigger className="rounded-xl border-2 border-[#ddd8eb]">
|
||||
<SelectTrigger className="rounded-xl border-2 border-var(--neutral-800)">
|
||||
<SelectValue placeholder="Select..." />
|
||||
</SelectTrigger>
|
||||
</Select>
|
||||
```
|
||||
|
||||
**Badges:**
|
||||
|
||||
```jsx
|
||||
// Status badges
|
||||
<Badge className="bg-green-100 text-green-800">Active</Badge>
|
||||
@@ -539,22 +586,38 @@ gap-2, gap-4, gap-6, gap-8 (same scale for flex/grid gaps)
|
||||
### Icons (Lucide React)
|
||||
|
||||
**Common Icons:**
|
||||
|
||||
```jsx
|
||||
import {
|
||||
User, Mail, Phone, MapPin, // User info
|
||||
Calendar, Clock, CheckCircle, // Status
|
||||
Edit, Trash2, X, Check, // Actions
|
||||
Search, Filter, Download, // Utilities
|
||||
Heart, DollarSign, CreditCard, // Financial
|
||||
AlertTriangle, Info, HelpCircle // Alerts
|
||||
} from 'lucide-react';
|
||||
User,
|
||||
Mail,
|
||||
Phone,
|
||||
MapPin, // User info
|
||||
Calendar,
|
||||
Clock,
|
||||
CheckCircle, // Status
|
||||
Edit,
|
||||
Trash2,
|
||||
X,
|
||||
Check, // Actions
|
||||
Search,
|
||||
Filter,
|
||||
Download, // Utilities
|
||||
Heart,
|
||||
DollarSign,
|
||||
CreditCard, // Financial
|
||||
AlertTriangle,
|
||||
Info,
|
||||
HelpCircle, // Alerts
|
||||
} from "lucide-react";
|
||||
|
||||
<User className="h-5 w-5 text-[#664fa3]" />
|
||||
<User className="h-5 w-5 text-var(--purple-lavender)" />;
|
||||
```
|
||||
|
||||
### Responsive Design
|
||||
|
||||
**Breakpoints:**
|
||||
|
||||
```css
|
||||
sm: 640px → @media (min-width: 640px)
|
||||
md: 768px → @media (min-width: 768px)
|
||||
@@ -564,6 +627,7 @@ xl: 1280px → @media (min-width: 1280px)
|
||||
```
|
||||
|
||||
**Mobile-First Approach:**
|
||||
|
||||
```jsx
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
{/* Stacks vertically on mobile, horizontal on tablet+ */}
|
||||
@@ -581,6 +645,7 @@ xl: 1280px → @media (min-width: 1280px)
|
||||
### Animations
|
||||
|
||||
**Tailwind Transitions:**
|
||||
|
||||
```jsx
|
||||
<Button className="transition-all duration-200 hover:scale-105">
|
||||
Hover me
|
||||
@@ -600,6 +665,7 @@ xl: 1280px → @media (min-width: 1280px)
|
||||
#### 1. Environment Variables
|
||||
|
||||
Create `.env.production`:
|
||||
|
||||
```bash
|
||||
REACT_APP_BACKEND_URL=https://api.loaf.org
|
||||
REACT_APP_SENTRY_DSN=your-production-sentry-dsn
|
||||
@@ -622,6 +688,7 @@ Build output will be in `/build` directory.
|
||||
### Option A: Netlify (Recommended)
|
||||
|
||||
**Via Netlify CLI:**
|
||||
|
||||
```bash
|
||||
# Install Netlify CLI
|
||||
npm install -g netlify-cli
|
||||
@@ -634,6 +701,7 @@ netlify deploy --prod --dir=build
|
||||
```
|
||||
|
||||
**Via Git Integration:**
|
||||
|
||||
1. Push code to GitHub/GitLab
|
||||
2. Connect repository in Netlify dashboard
|
||||
3. Configure:
|
||||
@@ -643,9 +711,11 @@ netlify deploy --prod --dir=build
|
||||
4. Deploy automatically on push
|
||||
|
||||
**Configure Redirects** (`public/_redirects`):
|
||||
|
||||
```
|
||||
/* /index.html 200
|
||||
```
|
||||
|
||||
This enables client-side routing.
|
||||
|
||||
### Option B: Vercel
|
||||
@@ -659,11 +729,10 @@ vercel --prod
|
||||
```
|
||||
|
||||
**Configure** (`vercel.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"rewrites": [
|
||||
{ "source": "/(.*)", "destination": "/index.html" }
|
||||
],
|
||||
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }],
|
||||
"env": {
|
||||
"REACT_APP_BACKEND_URL": "https://api.loaf.org"
|
||||
}
|
||||
@@ -673,11 +742,13 @@ vercel --prod
|
||||
### Option C: Traditional Web Server (Nginx)
|
||||
|
||||
**1. Copy build files to server:**
|
||||
|
||||
```bash
|
||||
scp -r build/* user@server:/var/www/loaf-frontend/
|
||||
```
|
||||
|
||||
**2. Configure Nginx** (`/etc/nginx/sites-available/loaf-frontend`):
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
@@ -702,6 +773,7 @@ server {
|
||||
```
|
||||
|
||||
**3. Enable site and restart:**
|
||||
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/loaf-frontend /etc/nginx/sites-enabled/
|
||||
sudo nginx -t
|
||||
@@ -709,6 +781,7 @@ sudo systemctl restart nginx
|
||||
```
|
||||
|
||||
**4. SSL Certificate:**
|
||||
|
||||
```bash
|
||||
sudo certbot --nginx -d app.loaf.org
|
||||
```
|
||||
@@ -716,12 +789,14 @@ sudo certbot --nginx -d app.loaf.org
|
||||
### Option D: AWS S3 + CloudFront
|
||||
|
||||
**1. Create S3 bucket:**
|
||||
|
||||
```bash
|
||||
aws s3 mb s3://loaf-frontend
|
||||
aws s3 sync build/ s3://loaf-frontend --delete
|
||||
```
|
||||
|
||||
**2. Configure bucket for static hosting:**
|
||||
|
||||
```bash
|
||||
aws s3 website s3://loaf-frontend --index-document index.html --error-document index.html
|
||||
```
|
||||
@@ -731,25 +806,31 @@ aws s3 website s3://loaf-frontend --index-document index.html --error-document i
|
||||
### Performance Optimization
|
||||
|
||||
**1. Code Splitting:**
|
||||
|
||||
```jsx
|
||||
// Lazy load routes
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { lazy, Suspense } from "react";
|
||||
|
||||
const AdminDonations = lazy(() => import('./pages/admin/AdminDonations'));
|
||||
const AdminDonations = lazy(() => import("./pages/admin/AdminDonations"));
|
||||
|
||||
<Route path="/admin/donations" element={
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<AdminDonations />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route
|
||||
path="/admin/donations"
|
||||
element={
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<AdminDonations />
|
||||
</Suspense>
|
||||
}
|
||||
/>;
|
||||
```
|
||||
|
||||
**2. Image Optimization:**
|
||||
|
||||
- Use WebP format when possible
|
||||
- Compress images before upload
|
||||
- Use lazy loading: `loading="lazy"`
|
||||
|
||||
**3. Bundle Analysis:**
|
||||
|
||||
```bash
|
||||
# Install analyzer
|
||||
yarn add --dev webpack-bundle-analyzer
|
||||
@@ -762,6 +843,7 @@ npx webpack-bundle-analyzer build/static/js/*.js
|
||||
### CI/CD Pipeline
|
||||
|
||||
**GitHub Actions** (`.github/workflows/deploy.yml`):
|
||||
|
||||
```yaml
|
||||
name: Deploy Frontend
|
||||
|
||||
@@ -776,7 +858,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: "18"
|
||||
- run: yarn install
|
||||
- run: yarn build
|
||||
env:
|
||||
@@ -800,6 +882,7 @@ jobs:
|
||||
**Error:** `Module not found: Can't resolve 'package-name'`
|
||||
|
||||
**Solution:**
|
||||
|
||||
```bash
|
||||
# Clear node_modules and reinstall
|
||||
rm -rf node_modules yarn.lock
|
||||
@@ -815,6 +898,7 @@ npm install
|
||||
**Error:** `Access to XMLHttpRequest blocked by CORS policy`
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Backend must include frontend URL in CORS_ORIGINS
|
||||
- Check REACT_APP_BACKEND_URL is correct
|
||||
- Backend: `CORS_ORIGINS=http://localhost:3000,https://app.loaf.org`
|
||||
@@ -824,6 +908,7 @@ npm install
|
||||
**Error:** API returns 401 after some time
|
||||
|
||||
**Solution:**
|
||||
|
||||
- JWT token expired (default 30 minutes)
|
||||
- User needs to log in again
|
||||
- Check token is being sent in Authorization header
|
||||
@@ -833,6 +918,7 @@ npm install
|
||||
**Error:** `process.env.REACT_APP_BACKEND_URL is undefined`
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Ensure variable name starts with `REACT_APP_`
|
||||
- Restart development server after changing .env
|
||||
- Don't commit .env to git (use .env.example)
|
||||
@@ -842,6 +928,7 @@ npm install
|
||||
**Error:** `npm run build` fails with memory error
|
||||
|
||||
**Solution:**
|
||||
|
||||
```bash
|
||||
# Increase Node memory limit
|
||||
NODE_OPTIONS=--max_old_space_size=4096 yarn build
|
||||
@@ -852,6 +939,7 @@ NODE_OPTIONS=--max_old_space_size=4096 yarn build
|
||||
**Error:** Refresh on /dashboard returns 404
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Configure server to redirect all routes to index.html
|
||||
- Netlify: Add `_redirects` file
|
||||
- Nginx: Use `try_files $uri /index.html`
|
||||
@@ -862,6 +950,7 @@ NODE_OPTIONS=--max_old_space_size=4096 yarn build
|
||||
**Error:** Profile photos return 404
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Check R2_PUBLIC_URL is correct in backend .env
|
||||
- Verify Cloudflare R2 bucket is public
|
||||
- Check CORS settings in R2 bucket
|
||||
@@ -870,16 +959,16 @@ NODE_OPTIONS=--max_old_space_size=4096 yarn build
|
||||
|
||||
```jsx
|
||||
// Add to any component for debugging
|
||||
console.log('Component rendered', { user, props });
|
||||
console.log("Component rendered", { user, props });
|
||||
|
||||
// Check API responses
|
||||
api.interceptors.response.use(
|
||||
response => {
|
||||
console.log('API Response:', response);
|
||||
(response) => {
|
||||
console.log("API Response:", response);
|
||||
return response;
|
||||
},
|
||||
error => {
|
||||
console.error('API Error:', error.response);
|
||||
(error) => {
|
||||
console.error("API Error:", error.response);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user