Refactor Members Directory and Newsletter Archive styles to use new color palette

- Updated color classes in MembersDirectory.js to use new color variables for borders, backgrounds, and text.
- Enhanced visual consistency by replacing hardcoded colors with Tailwind CSS color utilities.
- Modified NewsletterArchive.js to align with the new design system, ensuring a cohesive look across components.
- Added new color variables in tailwind.config.js for better maintainability and scalability.
This commit is contained in:
2026-01-07 11:36:07 -06:00
parent a93e2aa863
commit 4ba44d8997
79 changed files with 2152 additions and 2033 deletions

245
README.md
View File

@@ -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-muted-foreground foreground">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-primary foreground">
<h1 className="text-accent">Accent Text</h1>
<p className="text-muted-foreground">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-primary" 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-muted-foreground" 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,42 +536,46 @@ 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-muted">
{/* Content */}
</Card>
```
**Buttons:**
```jsx
// Primary
<Button className="bg-[#664fa3] text-white hover:bg-[#422268] rounded-full px-6 py-3">
<Button className="bg-muted-foreground foreground hover:bg-primary 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-muted text-muted-foreground hover:bg-muted rounded-full">
Secondary Action
</Button>
// Destructive
<Button className="bg-red-600 text-white hover:bg-red-700 rounded-full">
<Button className="bg-red-600 foreground hover:bg-red-700 rounded-full">
Delete
</Button>
```
**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-muted focus:border-muted-foreground" />
<Textarea className="rounded-xl border-2 border-muted min-h-[120px]" />
<Select>
<SelectTrigger className="rounded-xl border-2 border-[#ddd8eb]">
<SelectTrigger className="rounded-xl border-2 border-muted">
<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-muted-foreground" />;
```
### 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);
}
);