6 Commits

28 changed files with 450 additions and 2312 deletions

918
README.md
View File

@@ -1,912 +1,70 @@
# LOAF Membership Platform - Frontend # Getting Started with Create React App
React 19-based frontend application for the LOAF (LGBT Organization and Friends) membership management platform. This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Table of Contents ## Available Scripts
- [Setup & Installation](#setup--installation) In the project directory, you can run:
- [Architecture & Code Structure](#architecture--code-structure)
- [Design System](#design-system)
- [Deployment Guide](#deployment-guide)
- [Troubleshooting](#troubleshooting)
--- ### `npm start`
## Setup & Installation Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
### Prerequisites The page will reload when you make changes.\
You may also see any lint errors in the console.
- **Node.js**: 18.0 or higher ### `npm test`
- **Yarn**: 1.22+ (or npm 8+)
- **Backend API**: Running on http://localhost:8000
### 1. Install Dependencies Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
```bash ### `npm run build`
cd frontend
# Using Yarn (recommended) Builds the app for production to the `build` folder.\
yarn install It correctly bundles React in production mode and optimizes the build for the best performance.
# Or using npm The build is minified and the filenames include the hashes.\
npm install Your app is ready to be deployed!
```
**Key Dependencies:** See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
- `react@19.0.0` - UI library
- `react-router-dom@7.5.1` - Routing
- `axios@1.8.4` - HTTP client
- `react-hook-form@7.56.2` - Form handling
- `zod@3.24.4` - Schema validation
- `@radix-ui/*` - UI components (45+ components)
- `tailwindcss@3.4.17` - CSS framework
- `lucide-react@0.507.0` - Icons
- `sonner@1.7.4` - Toast notifications
### 2. Environment Configuration
Create `.env` file in the frontend directory:
```bash
# Backend API URL
REACT_APP_BACKEND_URL=http://localhost:8000
# Optional: Analytics, Sentry, etc.
# REACT_APP_SENTRY_DSN=your-sentry-dsn
# REACT_APP_GA_TRACKING_ID=UA-XXXXXXXXX-X
```
**Important:**
- All environment variables must start with `REACT_APP_`
- Restart development server after changing `.env`
### 3. Start Development Server
```bash
# Start development server
yarn start
# Or with npm
npm start
```
**Development server will be available at:**
- Frontend: http://localhost:3000
- Auto-reloads on file changes
### 4. Build for Production
```bash
# Create production build
yarn build
# Or with npm
npm build
```
Build output will be in `/build` directory.
### 5. Run Tests
```bash
# Run tests in watch mode
yarn test
# Run tests with coverage
yarn test --coverage
# Or with npm
npm test
```
---
## Architecture & Code Structure
### Project Structure
```
frontend/
├── public/ # Static assets
│ ├── loaf-logo.png # LOAF logo for admin sidebar
│ ├── hero-loaf.png # Landing page hero image
│ └── index.html # HTML template
├── src/
│ ├── pages/ # Page components (20+ pages)
│ │ ├── Landing.js # Public landing page
│ │ ├── Register.js # 4-step registration (388 lines)
│ │ ├── Login.js # Authentication
│ │ ├── Dashboard.js # Member dashboard
│ │ ├── Profile.js # User profile with photo upload
│ │ ├── Events.js # Event listing
│ │ ├── EventDetails.js # Event details + RSVP
│ │ └── admin/ # Admin pages
│ │ ├── AdminDashboard.js
│ │ ├── AdminUsers.js
│ │ ├── AdminValidations.js # Approve/reject workflow
│ │ ├── AdminEvents.js
│ │ ├── AdminSubscriptions.js # With CSV export
│ │ └── AdminDonations.js # Donation tracking
│ ├── components/ # Reusable components
│ │ ├── Navbar.js # Main navigation
│ │ ├── AdminSidebar.js # Admin sidebar with logo
│ │ ├── RejectionDialog.js # Rejection workflow dialog
│ │ └── ui/ # 45+ Radix UI components
│ │ ├── button.js
│ │ ├── dialog.js
│ │ ├── input.js
│ │ ├── select.js
│ │ └── ... (40+ more)
│ ├── context/
│ │ └── AuthContext.js # Global auth state
│ ├── hooks/
│ │ └── use-toast.js # Toast notification hook
│ ├── utils/
│ │ └── api.js # Axios instance with JWT interceptor
│ ├── App.js # Main routing setup
│ ├── index.js # React entry point
│ └── index.css # Global styles + Tailwind
├── craco.config.js # Craco configuration
├── tailwind.config.js # Tailwind CSS configuration
├── package.json # Dependencies
└── .env # Environment variables (not in git)
```
### 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 |
### 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';
function MyComponent() {
const { user, login, logout, isAuthenticated } = useAuth();
// user contains: id, email, first_name, last_name, role, status
}
```
#### Protected Routes
**PrivateRoute Wrapper:**
```jsx
<Route path="/dashboard" element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
} />
// Admin-only route
<Route path="/admin" element={
<PrivateRoute adminOnly>
<AdminDashboard />
</PrivateRoute>
} />
```
#### 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';
// GET request
const response = await api.get('/members/profile');
// POST request with data
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' }
});
```
#### Form Handling
**React Hook Form + Zod Pattern:**
```jsx
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')
});
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema)
});
const onSubmit = async (data) => {
try {
await api.post('/auth/login', data);
toast.success('Login successful!');
} catch (error) {
toast.error(error.response?.data?.detail || 'Login failed');
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
{/* ... */}
</form>
);
}
```
#### Toast Notifications
**Using Sonner:**
```jsx
import { toast } from 'sonner';
// Success
toast.success('Profile updated successfully!');
// Error
toast.error('Failed to upload photo');
// Custom
toast('Processing...', {
description: 'Please wait while we process your request'
});
```
### Page Components
#### 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)
3. Partner Information (optional)
4. Lead Sources & Referral
- Form validation with Zod
- Progress indicator
- Email verification trigger
**Login.js**
- Email/password authentication
- JWT token storage
- Remember me functionality
- Redirect to dashboard on success
#### 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
- Partner information
- 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
- Google Maps integration (optional)
#### 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
- Cover image upload
- RSVP tracking
- Attendance marking
### Component Library (Radix UI)
**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>
```
### 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
- **EVENTS & MEDIA** - Events, Gallery
- **DOCUMENTATION** - Newsletters, Financial Reports, Bylaws
- **Permissions** (Superadmin only)
---
## Design System
### Color Palette
**LOAF Brand Colors:**
```css
--primary: #422268 /* Deep Purple - Primary brand */
--secondary: #664fa3 /* Light Purple - Secondary elements */
--accent: #ff9e77 /* Coral - Accents and highlights */
--muted: #ddd8eb /* Light Purple Gray - Borders */
--background: #f9f5ff /* Very Light Purple - Backgrounds */
--foreground: #422268 /* Text color */
```
**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>
```
### 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)
text-base 1rem (16px)
text-lg 1.125rem (18px)
text-xl 1.25rem (20px)
text-2xl 1.5rem (24px)
text-3xl 1.875rem (30px)
```
**Usage:**
```jsx
<h1 className="text-3xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
Page Title
</h1>
<p className="text-base text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Body text
</p>
```
### Spacing System
**Tailwind Spacing Scale:**
```css
p-2 0.5rem (8px)
p-4 1rem (16px)
p-6 1.5rem (24px)
p-8 2rem (32px)
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]">
{/* Content */}
</Card>
```
**Buttons:**
```jsx
// Primary
<Button className="bg-[#664fa3] text-white hover:bg-[#422268] 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">
Secondary Action
</Button>
// Destructive
<Button className="bg-red-600 text-white 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]" />
<Select>
<SelectTrigger className="rounded-xl border-2 border-[#ddd8eb]">
<SelectValue placeholder="Select..." />
</SelectTrigger>
</Select>
```
**Badges:**
```jsx
// Status badges
<Badge className="bg-green-100 text-green-800">Active</Badge>
<Badge className="bg-yellow-100 text-yellow-800">Pending</Badge>
<Badge className="bg-red-100 text-red-800">Rejected</Badge>
```
### 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 className="h-5 w-5 text-[#664fa3]" />
```
### Responsive Design
**Breakpoints:**
```css
sm: 640px @media (min-width: 640px)
md: 768px @media (min-width: 768px)
lg: 1024px @media (min-width: 1024px)
xl: 1280px @media (min-width: 1280px)
2xl: 1536px @media (min-width: 1536px)
```
**Mobile-First Approach:**
```jsx
<div className="flex flex-col md:flex-row gap-4">
{/* Stacks vertically on mobile, horizontal on tablet+ */}
</div>
<div className="hidden md:block">
{/* Only shows on tablet+ */}
</div>
<div className="md:hidden">
{/* Only shows on mobile */}
</div>
```
### Animations
**Tailwind Transitions:**
```jsx
<Button className="transition-all duration-200 hover:scale-105">
Hover me
</Button>
<div className="animate-fade-in">
{/* Fades in on mount */}
</div>
```
---
## Deployment Guide
### Production Build
#### 1. Environment Variables
Create `.env.production`:
```bash
REACT_APP_BACKEND_URL=https://api.loaf.org
REACT_APP_SENTRY_DSN=your-production-sentry-dsn
```
#### 2. Build Application
```bash
# Install dependencies
yarn install
# Create production build
yarn build
```
Build output will be in `/build` directory.
#### 3. Deployment Options
### Option A: Netlify (Recommended)
**Via Netlify CLI:**
```bash
# Install Netlify CLI
npm install -g netlify-cli
# Login
netlify login
# Deploy
netlify deploy --prod --dir=build
```
**Via Git Integration:**
1. Push code to GitHub/GitLab
2. Connect repository in Netlify dashboard
3. Configure:
- Build command: `yarn build`
- Publish directory: `build`
- Environment variables (from .env.production)
4. Deploy automatically on push
**Configure Redirects** (`public/_redirects`):
```
/* /index.html 200
```
This enables client-side routing.
### Option B: Vercel
```bash
# Install Vercel CLI
npm install -g vercel
# Deploy
vercel --prod
```
**Configure** (`vercel.json`):
```json
{
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
],
"env": {
"REACT_APP_BACKEND_URL": "https://api.loaf.org"
}
}
```
### 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;
server_name app.loaf.org;
root /var/www/loaf-frontend;
index index.html;
location / {
try_files $uri /index.html;
}
# Cache static assets
location /static/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript;
}
```
**3. Enable site and restart:**
```bash
sudo ln -s /etc/nginx/sites-available/loaf-frontend /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
```
**4. SSL Certificate:**
```bash
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
```
**3. Create CloudFront distribution** pointing to S3 bucket with custom error pages (all errors → /index.html).
### Performance Optimization
**1. Code Splitting:**
```jsx
// Lazy load routes
import { lazy, Suspense } from 'react';
const AdminDonations = lazy(() => import('./pages/admin/AdminDonations'));
<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
# Analyze bundle
yarn build
npx webpack-bundle-analyzer build/static/js/*.js
```
### CI/CD Pipeline
**GitHub Actions** (`.github/workflows/deploy.yml`):
```yaml
name: Deploy Frontend
on:
push:
branches: [main]
jobs: ### `npm run eject`
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: yarn install
- run: yarn build
env:
REACT_APP_BACKEND_URL: ${{ secrets.BACKEND_URL }}
- uses: netlify/actions/cli@master
with:
args: deploy --dir=build --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
```
---
## Troubleshooting
### Common Issues
#### 1. Module Not Found Errors **Note: this is a one-way operation. Once you `eject`, you can't go back!**
**Error:** `Module not found: Can't resolve 'package-name'` If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
**Solution:** Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
```bash
# Clear node_modules and reinstall
rm -rf node_modules yarn.lock
yarn install
# Or with npm You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
rm -rf node_modules package-lock.json
npm install
```
#### 2. CORS Errors ## Learn More
**Error:** `Access to XMLHttpRequest blocked by CORS policy` You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
**Solution:** To learn React, check out the [React documentation](https://reactjs.org/).
- 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`
#### 3. 401 Unauthorized ### Code Splitting
**Error:** API returns 401 after some time This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
**Solution:** ### Analyzing the Bundle Size
- JWT token expired (default 30 minutes)
- User needs to log in again
- Check token is being sent in Authorization header
#### 4. Environment Variables Not Working This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
**Error:** `process.env.REACT_APP_BACKEND_URL is undefined` ### Making a Progressive Web App
**Solution:** This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
- Ensure variable name starts with `REACT_APP_`
- Restart development server after changing .env
- Don't commit .env to git (use .env.example)
#### 5. Build Fails ### Advanced Configuration
**Error:** `npm run build` fails with memory error
**Solution:** This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
```bash
# Increase Node memory limit
NODE_OPTIONS=--max_old_space_size=4096 yarn build
```
#### 6. Routing Not Working in Production ### Deployment
**Error:** Refresh on /dashboard returns 404 This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
**Solution:** ### `npm run build` fails to minify
- Configure server to redirect all routes to index.html
- Netlify: Add `_redirects` file
- Nginx: Use `try_files $uri /index.html`
- Vercel: Add vercel.json with rewrites
#### 7. Images Not Loading This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
**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
### Debug Mode
```jsx
// Add to any component for debugging
console.log('Component rendered', { user, props });
// Check API responses
api.interceptors.response.use(
response => {
console.log('API Response:', response);
return response;
},
error => {
console.error('API Error:', error.response);
return Promise.reject(error);
}
);
```
### Getting Help
- **Project Context**: See `CLAUDE.md` and `PRD.md`
- **API Docs**: http://localhost:8000/docs (backend Swagger)
- **React Docs**: https://react.dev/
- **Tailwind CSS**: https://tailwindcss.com/docs
- **Radix UI**: https://www.radix-ui.com/primitives
---
## Additional Resources
- **React Documentation**: https://react.dev/
- **React Router**: https://reactrouter.com/
- **Tailwind CSS**: https://tailwindcss.com/
- **Radix UI**: https://www.radix-ui.com/
- **React Hook Form**: https://react-hook-form.com/
- **Zod**: https://zod.dev/
- **Lucide Icons**: https://lucide.dev/
---
**Last Updated**: December 18, 2024
**Version**: 1.0.0
**Maintainer**: LOAF Development Team

View File

@@ -26,7 +26,6 @@ import AdminEvents from './pages/admin/AdminEvents';
import AdminValidations from './pages/admin/AdminValidations'; import AdminValidations from './pages/admin/AdminValidations';
import AdminPlans from './pages/admin/AdminPlans'; import AdminPlans from './pages/admin/AdminPlans';
import AdminSubscriptions from './pages/admin/AdminSubscriptions'; import AdminSubscriptions from './pages/admin/AdminSubscriptions';
import AdminDonations from './pages/admin/AdminDonations';
import AdminLayout from './layouts/AdminLayout'; import AdminLayout from './layouts/AdminLayout';
import { AuthProvider, useAuth } from './context/AuthContext'; import { AuthProvider, useAuth } from './context/AuthContext';
import MemberRoute from './components/MemberRoute'; import MemberRoute from './components/MemberRoute';
@@ -232,13 +231,6 @@ function App() {
</AdminLayout> </AdminLayout>
</PrivateRoute> </PrivateRoute>
} /> } />
<Route path="/admin/donations" element={
<PrivateRoute adminOnly>
<AdminLayout>
<AdminDonations />
</AdminLayout>
</PrivateRoute>
} />
<Route path="/admin/gallery" element={ <Route path="/admin/gallery" element={
<PrivateRoute adminOnly> <PrivateRoute adminOnly>
<AdminLayout> <AdminLayout>

View File

@@ -21,8 +21,7 @@ import {
DollarSign, DollarSign,
Scale, Scale,
HardDrive, HardDrive,
Repeat, Repeat
Heart
} from 'lucide-react'; } from 'lucide-react';
const AdminSidebar = ({ isOpen, onToggle, isMobile }) => { const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
@@ -124,12 +123,6 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
path: '/admin/subscriptions', path: '/admin/subscriptions',
disabled: false disabled: false
}, },
{
name: 'Donations',
icon: Heart,
path: '/admin/donations',
disabled: false
},
{ {
name: 'Events', name: 'Events',
icon: Calendar, icon: Calendar,
@@ -184,9 +177,43 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
return location.pathname.startsWith(path); return location.pathname.startsWith(path);
}; };
const renderNavItem = (item) => { return (
if (!item) return null; <>
{/* Sidebar */}
<aside
className={`
bg-white border-r border-[#ddd8eb] transition-all duration-300 ease-out
${isMobile ? 'fixed inset-y-0 left-0 z-40' : 'relative'}
${isOpen ? 'w-64' : 'w-16'}
${isMobile && !isOpen ? '-translate-x-full' : 'translate-x-0'}
flex flex-col
`}
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-[#ddd8eb]">
{isOpen && (
<h2 className="text-xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
Admin
</h2>
)}
<button
onClick={onToggle}
className="p-2 rounded-lg hover:bg-[#DDD8EB]/20 transition-colors ml-auto"
aria-label={isOpen ? 'Collapse sidebar' : 'Expand sidebar'}
>
{isMobile ? (
<Menu className="h-5 w-5 text-[#422268]" />
) : isOpen ? (
<ChevronLeft className="h-5 w-5 text-[#422268]" />
) : (
<ChevronRight className="h-5 w-5 text-[#422268]" />
)}
</button>
</div>
{/* Navigation */}
<nav className="flex-1 overflow-y-auto p-4 space-y-2">
{filteredNavItems.map((item) => {
const Icon = item.icon; const Icon = item.icon;
const active = isActive(item.path); const active = isActive(item.path);
@@ -249,117 +276,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
)} )}
</div> </div>
); );
}; })}
return (
<>
{/* Sidebar */}
<aside
className={`
bg-white border-r border-[#ddd8eb] transition-all duration-300 ease-out
${isMobile ? 'fixed inset-y-0 left-0 z-40' : 'relative'}
${isOpen ? 'w-64' : 'w-16'}
${isMobile && !isOpen ? '-translate-x-full' : 'translate-x-0'}
flex flex-col
`}
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-[#ddd8eb]">
<div className="flex items-center gap-3">
<img
src={`${process.env.PUBLIC_URL}/loaf-logo.png`}
alt="LOAF Logo"
className={`object-contain transition-all duration-200 ${
isOpen ? 'h-10 w-10' : 'h-8 w-8'
}`}
/>
{isOpen && (
<h2 className="text-xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
Admin
</h2>
)}
</div>
<button
onClick={onToggle}
className="p-2 rounded-lg hover:bg-[#DDD8EB]/20 transition-colors"
aria-label={isOpen ? 'Collapse sidebar' : 'Expand sidebar'}
>
{isMobile ? (
<Menu className="h-5 w-5 text-[#422268]" />
) : isOpen ? (
<ChevronLeft className="h-5 w-5 text-[#422268]" />
) : (
<ChevronRight className="h-5 w-5 text-[#422268]" />
)}
</button>
</div>
{/* Navigation */}
<nav className="flex-1 overflow-y-auto p-4">
{/* Dashboard - Standalone */}
{renderNavItem(filteredNavItems.find(item => item.name === 'Dashboard'))}
{/* MEMBERSHIP Section */}
{isOpen && (
<div className="px-4 py-2 mt-6">
<h3 className="text-xs font-semibold text-[#664fa3] uppercase tracking-wider">
Membership
</h3>
</div>
)}
<div className="space-y-1">
{renderNavItem(filteredNavItems.find(item => item.name === 'Staff'))}
{renderNavItem(filteredNavItems.find(item => item.name === 'Members'))}
{renderNavItem(filteredNavItems.find(item => item.name === 'Validations'))}
</div>
{/* FINANCIALS Section */}
{isOpen && (
<div className="px-4 py-2 mt-6">
<h3 className="text-xs font-semibold text-[#664fa3] uppercase tracking-wider">
Financials
</h3>
</div>
)}
<div className="space-y-1">
{renderNavItem(filteredNavItems.find(item => item.name === 'Plans'))}
{renderNavItem(filteredNavItems.find(item => item.name === 'Subscriptions'))}
{renderNavItem(filteredNavItems.find(item => item.name === 'Donations'))}
</div>
{/* EVENTS & MEDIA Section */}
{isOpen && (
<div className="px-4 py-2 mt-6">
<h3 className="text-xs font-semibold text-[#664fa3] uppercase tracking-wider">
Events & Media
</h3>
</div>
)}
<div className="space-y-1">
{renderNavItem(filteredNavItems.find(item => item.name === 'Events'))}
{renderNavItem(filteredNavItems.find(item => item.name === 'Gallery'))}
</div>
{/* DOCUMENTATION Section */}
{isOpen && (
<div className="px-4 py-2 mt-6">
<h3 className="text-xs font-semibold text-[#664fa3] uppercase tracking-wider">
Documentation
</h3>
</div>
)}
<div className="space-y-1">
{renderNavItem(filteredNavItems.find(item => item.name === 'Newsletters'))}
{renderNavItem(filteredNavItems.find(item => item.name === 'Financials'))}
{renderNavItem(filteredNavItems.find(item => item.name === 'Bylaws'))}
</div>
{/* Permissions - Superadmin only (no header) */}
{user?.role === 'superadmin' && (
<div className="mt-6">
{renderNavItem(filteredNavItems.find(item => item.name === 'Permissions'))}
</div>
)}
</nav> </nav>
{/* User Section */} {/* User Section */}

View File

@@ -0,0 +1,84 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Button } from './ui/button';
const PublicFooter = () => {
const loafLogo = `${process.env.PUBLIC_URL}/loaf-logo.png`;
return (
<>
{/* Main Footer */}
<footer className="bg-[#644c9f] px-20 border-t border-[rgba(0,0,0,0.1)] py-4 md:py-[35px] flex items-center justify-center">
<div className=" flex flex-col lg:flex-row gap-8 sm:gap-12 md:gap-16 lg:gap-20 xl:gap-30 items-center justify-around w-full max-w-7xl">
<div className="w-32 sm:w-40 md:w-48 lg:w-[180px] flex-shrink-0">
<img src={loafLogo} alt="LOAF Logo" className="w-full h-auto aspect-square object-contain" />
</div>
<nav className="flex sm:px-4 px-2 flex-col sm:flex-row gap-32 items-start justify-center w-full lg:w-auto">
<div className="flex-col gap-2 w-full sm:w-auto sm:min-w-[163px] hidden sm:flex">
<div className="">
<p className="text-white text-xl font-semibold" style={{ fontFamily: "'Poppinss Sans', sans-serif" }}>About</p>
</div>
<Link to="/about/history" className="text-[#ddd8eb] text-sm sm:text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>History</Link>
<Link to="/about/mission-values" className="text-[#ddd8eb] text-sm sm:text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Mission and Values</Link>
<Link to="/about/board" className="text-[#ddd8eb] text-sm sm:text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Board of Directors</Link>
</div>
<div className="hidden sm:flex flex-col gap-2 w-full sm:w-auto sm:min-w-[148px]">
<div className="">
<p className="text-white text-xl font-semibold" style={{ fontFamily: "'Poppins Sans', sans-serif" }}>Connect</p>
</div>
<Link to="/become-a-member" className="text-[#ddd8eb] text-sm sm:text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Become a Member</Link>
<Link to="/contact-us" className="text-[#ddd8eb] text-sm sm:text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Contact Us</Link>
<Link to="/resources" className="text-[#ddd8eb] text-sm sm:text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Resources</Link>
</div>
<div className="flex flex-col gap-2 justify-center w-full sm:w-auto sm:min-w-[220px] md:min-w-[271px] text-center">
<div className="pb-4 w-full flex ">
<Link to="/donate" className="block">
<Button style={{ fontFamily: "'Nunito Sans', sans-serif" }} className="bg-[#ff9e77] hover:bg-[#ff8c64] text-[#48286e] rounded-full px-14 py-6 text-[19px] leading-6 sm:text-lg font-semibold ">
Donate
</Button>
</Link>
</div>
<div className=' w-full flex justify-center '>
<p className="text-[#ddd8eb] text-sm sm:text-base font-medium text-left w-full" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
LOAF is supported by<br />the Hollyfield Foundation
</p>
</div>
</div>
</nav>
</div>
</footer>
{/* Bottom Footer */}
<footer className="bg-gradient-to-r from-[#48286e] to-[#644c9f] border-t border-[rgba(0,0,0,0.1)] px-4 sm:px-16 py-5">
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6 justify-between items-center max-w-7xl mx-auto">
<nav className="flex flex-col sm:flex-row gap-4 sm:gap-8 items-center order-1 sm:order-none">
<<<<<<< HEAD
<a href="/#terms" className="text-[#c5b4e3] text-sm sm:text-base font-medium hover:text-white transition-colors whitespace-nowrap" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Terms of Service
</a>
<a href="/#privacy" className="text-[#c5b4e3] text-sm sm:text-base font-medium hover:text-white transition-colors whitespace-nowrap" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
=======
<Link to="/terms-of-service" className="text-[#c5b4e3] text-sm sm:text-base font-medium hover:text-white transition-colors whitespace-nowrap" style={{ fontFamily: "'Inter', sans-serif" }}>
Terms of Service
</Link>
<Link to="/privacy-policy" className="text-[#c5b4e3] text-sm sm:text-base font-medium hover:text-white transition-colors whitespace-nowrap" style={{ fontFamily: "'Inter', sans-serif" }}>
>>>>>>> main
Privacy Policy
</Link>
</nav>
<p className="text-[#c5b4e3] text-sm sm:text-base font-medium text-center order-2 sm:order-none" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
© 2025 LOAF. All Rights Reserved.
</p>
<p className="text-[#c5b4e3] text-sm sm:text-base font-medium text-center order-3 sm:order-none" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Designed and Managed by{' '}
<a href="https://konceptkit.com/" className="text-white transition-colors whitespace-nowrap">
Koncept Kit
</a>
</p>
</div>
</footer>
</>
);
};
export default PublicFooter;

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate, useLocation } from 'react-router-dom';
import { Button } from './ui/button'; import { Button } from './ui/button';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { ChevronDown, Menu, X } from 'lucide-react'; import { ChevronDown, Menu, X } from 'lucide-react';
@@ -13,8 +13,23 @@ import {
const PublicNavbar = () => { const PublicNavbar = () => {
const { user, logout } = useAuth(); const { user, logout } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
// Helper function to check if a path is active
const isActive = (path) => {
if (path.includes('#')) {
// For hash links like /#welcome
return location.pathname + location.hash === path || location.hash === path.replace('/', '');
}
return location.pathname === path || location.pathname.startsWith(path + '/');
};
// Check if any About Us sub-page is active
const isAboutActive = () => {
return location.pathname.startsWith('/about');
};
// LOAF logo (local) // LOAF logo (local)
const loafLogo = `${process.env.PUBLIC_URL}/loaf-logo.png`; const loafLogo = `${process.env.PUBLIC_URL}/loaf-logo.png`;
@@ -27,14 +42,43 @@ const PublicNavbar = () => {
} }
}; };
// Active and inactive link styles for desktop
const getDesktopLinkClasses = (path) => {
const baseClasses = "text-[17.5px] font-medium transition-all px-3 py-1 rounded-md";
if (isActive(path)) {
return `${baseClasses} text-[#ff9e77] hover:text-[#ff8c64] `;
}
return `${baseClasses} text-white hover:opacity-80`;
};
// Active and inactive link styles for mobile
const getMobileLinkClasses = (path) => {
const baseClasses = "text-base font-medium px-4 py-3 rounded-md transition-colors";
if (isActive(path)) {
return `${baseClasses} bg-[#ff9e77] hover:bg-[#ff8c64] text-[#48286e]`;
}
return `${baseClasses} text-white hover:bg-[#48286e]`;
};
// Active and inactive link styles for mobile sub-items (About Us)
const getMobileSubLinkClasses = (path) => {
const baseClasses = "text-sm font-medium px-6 py-2 rounded-md transition-colors block";
if (isActive(path)) {
return `${baseClasses} bg-[#ff9e77] hover:bg-[#ff8c64] text-[#48286e]`;
}
return `${baseClasses} text-[#ddd8eb] hover:bg-[#48286e] hover:text-white`;
};
return ( return (
<> <>
{/* Top Header - Auth Actions */} {/* Top Header - Auth Actions */}
<header className="bg-gradient-to-r from-[#644c9f] to-[#48286e] px-4 sm:px-8 md:px-16 py-4 flex justify-end items-center gap-4 sm:gap-6"> <header className="bg-gradient-to-r from-[#644c9f] to-[#48286e] px-[20px] py-[10px] flex md:justify-end justify-between items-center gap-4 sm:gap-6">
<div className='flex gap-4 sm:gap-6'>
<button <button
onClick={handleAuthAction} onClick={handleAuthAction}
className="text-white text-base font-medium hover:opacity-80 transition-opacity bg-transparent border-none cursor-pointer" className="text-white text-base font-medium hover:opacity-80 transition-opacity bg-transparent border-none cursor-pointer"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
> >
{user ? 'Logout' : 'Login'} {user ? 'Logout' : 'Login'}
</button> </button>
@@ -42,14 +86,15 @@ const PublicNavbar = () => {
<Link <Link
to="/register" to="/register"
className="text-white text-base font-medium hover:opacity-80 transition-opacity" className="text-white text-base font-medium hover:opacity-80 transition-opacity"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
> >
Register Register
</Link> </Link>
)} )}
</div>
<Link to="/donate"> <Link to="/donate">
<Button <Button
className="bg-[#ff9e77] hover:bg-[#ff8c64] text-[#48286e] rounded-[25px] px-[54px] py-[10px] text-[16.5px] font-semibold h-[41px]" className="bg-[#ff9e77] hover:bg-[#ff8c64] text-[#48286e] rounded-[25px] px-[50px] py-[5px] text-[16.5px] font-semibold h-[41px]"
style={{ fontFamily: "'Montserrat', sans-serif" }} style={{ fontFamily: "'Montserrat', sans-serif" }}
> >
Donate Donate
@@ -58,7 +103,7 @@ const PublicNavbar = () => {
</header> </header>
{/* Main Header - Navigation */} {/* Main Header - Navigation */}
<header className="bg-[#664fa3] px-4 sm:px-8 md:px-16 py-2 flex justify-between items-center"> <header className="sticky top-0 inset-x-0 z-40 bg-[#664fa3] px-[20px] py-2 flex justify-between items-center">
<Link to="/"> <Link to="/">
<img src={loafLogo} alt="LOAF Logo" className="h-16 w-16 sm:h-20 sm:w-20 md:h-28 md:w-28 object-contain" /> <img src={loafLogo} alt="LOAF Logo" className="h-16 w-16 sm:h-20 sm:w-20 md:h-28 md:w-28 object-contain" />
</Link> </Link>
@@ -73,18 +118,21 @@ const PublicNavbar = () => {
</button> </button>
{/* Desktop Navigation */} {/* Desktop Navigation */}
<nav className="hidden lg:flex gap-10 items-center"> <nav className="hidden lg:flex gap-6 items-center">
<Link <Link
to="/#welcome" to="/#welcome"
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity" className={getDesktopLinkClasses('/#welcome')}
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
> >
Welcome Welcome
</Link> </Link>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity flex items-center gap-1 bg-transparent border-none cursor-pointer" <button
style={{ fontFamily: "'Poppins', sans-serif" }}> className={`${isAboutActive()
? "text-[#ff9e77] hover:text-[#ff8c64]"
: "text-white hover:opacity-80"} text-[17.5px] font-medium transition-all flex items-center gap-1 bg-transparent border-none cursor-pointer px-3 py-1 rounded-md`}
style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
About Us About Us
<ChevronDown className="h-4 w-4" /> <ChevronDown className="h-4 w-4" />
</button> </button>
@@ -92,19 +140,19 @@ const PublicNavbar = () => {
<DropdownMenuContent align="start" className="bg-white min-w-[220px]"> <DropdownMenuContent align="start" className="bg-white min-w-[220px]">
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link to="/about/history" className="w-full px-3 py-2 text-[#48286e] hover:bg-[#f1eef9] cursor-pointer" <Link to="/about/history" className="w-full px-3 py-2 text-[#48286e] hover:bg-[#f1eef9] cursor-pointer"
style={{ fontFamily: "'Poppins', sans-serif" }}> style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
History History
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link to="/about/mission-values" className="w-full px-3 py-2 text-[#48286e] hover:bg-[#f1eef9] cursor-pointer" <Link to="/about/mission-values" className="w-full px-3 py-2 text-[#48286e] hover:bg-[#f1eef9] cursor-pointer"
style={{ fontFamily: "'Poppins', sans-serif" }}> style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Mission and Values Mission and Values
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link to="/about/board" className="w-full px-3 py-2 text-[#48286e] hover:bg-[#f1eef9] cursor-pointer" <Link to="/about/board" className="w-full px-3 py-2 text-[#48286e] hover:bg-[#f1eef9] cursor-pointer"
style={{ fontFamily: "'Poppins', sans-serif" }}> style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Board of Directors Board of Directors
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
@@ -112,31 +160,31 @@ const PublicNavbar = () => {
</DropdownMenu> </DropdownMenu>
<Link <Link
to={user ? "/dashboard" : "/become-a-member"} to={user ? "/dashboard" : "/become-a-member"}
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity" className={getDesktopLinkClasses(user ? "/dashboard" : "/become-a-member")}
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
> >
{user ? 'Dashboard' : 'Become a Member'} {user ? 'Dashboard' : 'Become a Member'}
</Link> </Link>
{!user && ( {!user && (
<Link <Link
to="/login" to="/login"
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity" className={getDesktopLinkClasses('/login')}
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
> >
Members Only Members Only
</Link> </Link>
)} )}
<Link <Link
to="/resources" to="/resources"
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity" className={getDesktopLinkClasses('/resources')}
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
> >
Resources Resources
</Link> </Link>
<Link <Link
to="/contact-us" to="/contact-us"
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity" className={getDesktopLinkClasses('/contact-us')}
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
> >
Contact Us Contact Us
</Link> </Link>
@@ -156,7 +204,7 @@ const PublicNavbar = () => {
<div className="fixed right-0 top-0 h-full w-[280px] bg-[#664fa3] shadow-xl overflow-y-auto"> <div className="fixed right-0 top-0 h-full w-[280px] bg-[#664fa3] shadow-xl overflow-y-auto">
{/* Header */} {/* Header */}
<div className="flex justify-between items-center p-6 border-b border-[#48286e]"> <div className="flex justify-between items-center p-6 border-b border-[#48286e]">
<span className="text-white text-lg font-semibold" style={{ fontFamily: "'Poppins', sans-serif" }}> <span className="text-white text-lg font-semibold" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Menu Menu
</span> </span>
<button <button
@@ -173,38 +221,41 @@ const PublicNavbar = () => {
<Link <Link
to="/#welcome" to="/#welcome"
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className="text-white text-base font-medium hover:bg-[#48286e] px-4 py-3 rounded-md transition-colors" className={getMobileLinkClasses('/#welcome')}
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
> >
Welcome Welcome
</Link> </Link>
{/* About Us Section */} {/* About Us Section */}
<div className="space-y-2"> <div className="space-y-2">
<p className="text-white text-base font-semibold px-4 py-2" style={{ fontFamily: "'Poppins', sans-serif" }}> <p
className={`text-base font-semibold px-4 py-2 rounded-md ${isAboutActive() ? 'text-[#ff9e77]' : 'text-white'}`}
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
About Us About Us
</p> </p>
<Link <Link
to="/about/history" to="/about/history"
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className="text-[#ddd8eb] text-sm font-medium hover:bg-[#48286e] hover:text-white px-6 py-2 rounded-md transition-colors block" className={getMobileSubLinkClasses('/about/history')}
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
> >
History History
</Link> </Link>
<Link <Link
to="/about/mission-values" to="/about/mission-values"
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className="text-[#ddd8eb] text-sm font-medium hover:bg-[#48286e] hover:text-white px-6 py-2 rounded-md transition-colors block" className={getMobileSubLinkClasses('/about/mission-values')}
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
> >
Mission and Values Mission and Values
</Link> </Link>
<Link <Link
to="/about/board" to="/about/board"
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className="text-[#ddd8eb] text-sm font-medium hover:bg-[#48286e] hover:text-white px-6 py-2 rounded-md transition-colors block" className={getMobileSubLinkClasses('/about/board')}
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
> >
Board of Directors Board of Directors
</Link> </Link>
@@ -213,8 +264,8 @@ const PublicNavbar = () => {
<Link <Link
to={user ? "/dashboard" : "/become-a-member"} to={user ? "/dashboard" : "/become-a-member"}
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className="text-white text-base font-medium hover:bg-[#48286e] px-4 py-3 rounded-md transition-colors" className={getMobileLinkClasses(user ? "/dashboard" : "/become-a-member")}
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
> >
{user ? 'Dashboard' : 'Become a Member'} {user ? 'Dashboard' : 'Become a Member'}
</Link> </Link>
@@ -223,8 +274,8 @@ const PublicNavbar = () => {
<Link <Link
to="/login" to="/login"
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className="text-white text-base font-medium hover:bg-[#48286e] px-4 py-3 rounded-md transition-colors" className={getMobileLinkClasses('/login')}
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
> >
Members Only Members Only
</Link> </Link>
@@ -233,8 +284,8 @@ const PublicNavbar = () => {
<Link <Link
to="/resources" to="/resources"
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className="text-white text-base font-medium hover:bg-[#48286e] px-4 py-3 rounded-md transition-colors" className={getMobileLinkClasses('/resources')}
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
> >
Resources Resources
</Link> </Link>
@@ -242,8 +293,8 @@ const PublicNavbar = () => {
<Link <Link
to="/contact-us" to="/contact-us"
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className="text-white text-base font-medium hover:bg-[#48286e] px-4 py-3 rounded-md transition-colors" className={getMobileLinkClasses('/contact-us')}
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
> >
Contact Us Contact Us
</Link> </Link>
@@ -256,7 +307,7 @@ const PublicNavbar = () => {
setIsMobileMenuOpen(false); setIsMobileMenuOpen(false);
}} }}
className="w-full text-left text-white text-base font-medium hover:bg-[#48286e] px-4 py-3 rounded-md transition-colors" className="w-full text-left text-white text-base font-medium hover:bg-[#48286e] px-4 py-3 rounded-md transition-colors"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
> >
{user ? 'Logout' : 'Login'} {user ? 'Logout' : 'Login'}
</button> </button>
@@ -265,7 +316,7 @@ const PublicNavbar = () => {
to="/register" to="/register"
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className="block text-white text-base font-medium hover:bg-[#48286e] px-4 py-3 rounded-md transition-colors" className="block text-white text-base font-medium hover:bg-[#48286e] px-4 py-3 rounded-md transition-colors"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
> >
Register Register
</Link> </Link>

View File

@@ -1,107 +0,0 @@
import React, { useState } from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from './ui/dialog';
import { Button } from './ui/button';
import { Textarea } from './ui/textarea';
import { Label } from './ui/label';
import { AlertTriangle, X } from 'lucide-react';
export default function RejectionDialog({ open, onOpenChange, onConfirm, user, loading }) {
const [reason, setReason] = useState('');
const [error, setError] = useState('');
const handleSubmit = () => {
if (!reason.trim()) {
setError('Rejection reason is required');
return;
}
onConfirm(reason);
};
const handleClose = () => {
setReason('');
setError('');
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-[500px] rounded-2xl border-2 border-[#ddd8eb]">
<DialogHeader>
<div className="flex items-center gap-3 mb-2">
<div className="p-3 bg-red-100 rounded-full">
<AlertTriangle className="h-6 w-6 text-red-600" />
</div>
<DialogTitle className="text-2xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
Reject Application
</DialogTitle>
</div>
<DialogDescription className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
You are about to reject <strong>{user?.first_name} {user?.last_name}</strong>'s membership application.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="bg-[#f9f5ff] border border-[#ddd8eb] rounded-lg p-4">
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<strong>Applicant:</strong> {user?.email}
</p>
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<strong>Status:</strong> {user?.status}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="reason" className="text-[#422268] font-medium">
Rejection Reason <span className="text-red-500">*</span>
</Label>
<Textarea
id="reason"
value={reason}
onChange={(e) => {
setReason(e.target.value);
setError('');
}}
placeholder="Please provide a clear reason for rejection. This will be sent to the applicant."
className="rounded-xl border-2 border-[#ddd8eb] focus:border-red-500 min-h-[120px]"
disabled={loading}
/>
{error && (
<p className="text-sm text-red-500">{error}</p>
)}
<p className="text-xs text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
The applicant will receive an email with this reason.
</p>
</div>
</div>
<div className="flex gap-3 justify-end">
<Button
type="button"
onClick={handleClose}
variant="outline"
className="border-2 border-[#ddd8eb] text-[#664fa3] hover:bg-[#f1eef9] rounded-full px-6"
disabled={loading}
>
<X className="h-4 w-4 mr-2" />
Cancel
</Button>
<Button
type="button"
onClick={handleSubmit}
className="bg-red-600 text-white hover:bg-red-700 rounded-full px-6"
disabled={loading}
>
<AlertTriangle className="h-4 w-4 mr-2" />
{loading ? 'Rejecting...' : 'Confirm Rejection'}
</Button>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Button } from '../components/ui/button'; import { Button } from '../components/ui/button';
import PublicNavbar from '../components/PublicNavbar'; import PublicNavbar from '../components/PublicNavbar';
import PublicFooter from '../components/PublicFooter'; import PublicFooter from '../components/PublicFooter-kc';
import { ArrowDown } from 'lucide-react'; import { ArrowDown } from 'lucide-react';
const BecomeMember = () => { const BecomeMember = () => {

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PublicNavbar from '../components/PublicNavbar'; import PublicNavbar from '../components/PublicNavbar';
import PublicFooter from '../components/PublicFooter'; import PublicFooter from '../components/PublicFooter-kc';
import { Card } from '../components/ui/card'; import { Card } from '../components/ui/card';
const BoardOfDirectors = () => { const BoardOfDirectors = () => {

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import PublicNavbar from '../components/PublicNavbar'; import PublicNavbar from '../components/PublicNavbar';
import PublicFooter from '../components/PublicFooter'; import PublicFooter from '../components/PublicFooter-kc';
import { Button } from '../components/ui/button'; import { Button } from '../components/ui/button';
import { Card } from '../components/ui/card'; import { Card } from '../components/ui/card';
import { Input } from '../components/ui/input'; import { Input } from '../components/ui/input';

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import PublicNavbar from '../components/PublicNavbar'; import PublicNavbar from '../components/PublicNavbar';
import PublicFooter from '../components/PublicFooter'; import PublicFooter from '../components/PublicFooter-kc';
import { Button } from '../components/ui/button'; import { Button } from '../components/ui/button';
import { Card } from '../components/ui/card'; import { Card } from '../components/ui/card';
import { Input } from '../components/ui/input'; import { Input } from '../components/ui/input';

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import PublicNavbar from '../components/PublicNavbar'; import PublicNavbar from '../components/PublicNavbar';
import PublicFooter from '../components/PublicFooter'; import PublicFooter from '../components/PublicFooter-kc';
import { Card } from '../components/ui/card'; import { Card } from '../components/ui/card';
import { Button } from '../components/ui/button'; import { Button } from '../components/ui/button';
import { CheckCircle, Heart } from 'lucide-react'; import { CheckCircle, Heart } from 'lucide-react';

View File

@@ -7,7 +7,7 @@ import { Label } from '../components/ui/label';
import { Card } from '../components/ui/card'; import { Card } from '../components/ui/card';
import { toast } from 'sonner'; import { toast } from 'sonner';
import PublicNavbar from '../components/PublicNavbar'; import PublicNavbar from '../components/PublicNavbar';
import PublicFooter from '../components/PublicFooter'; import PublicFooter from '../components/PublicFooter-kc';
import { ArrowRight, ArrowLeft, Mail, CheckCircle } from 'lucide-react'; import { ArrowRight, ArrowLeft, Mail, CheckCircle } from 'lucide-react';
const ForgotPassword = () => { const ForgotPassword = () => {

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PublicNavbar from '../components/PublicNavbar'; import PublicNavbar from '../components/PublicNavbar';
import PublicFooter from '../components/PublicFooter'; import PublicFooter from '../components/PublicFooter-kc';
import { Button } from '../components/ui/button'; import { Button } from '../components/ui/button';
import { Card } from '../components/ui/card'; import { Card } from '../components/ui/card';
import { Pen } from 'lucide-react'; import { Pen } from 'lucide-react';

View File

@@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
import { Button } from '../components/ui/button'; import { Button } from '../components/ui/button';
import { Card } from '../components/ui/card'; import { Card } from '../components/ui/card';
import PublicNavbar from '../components/PublicNavbar'; import PublicNavbar from '../components/PublicNavbar';
import PublicFooter from '../components/PublicFooter'; import PublicFooter from '../components/PublicFooter-kc';
const Landing = () => { const Landing = () => {
// LOAF brand assets (local) // LOAF brand assets (local)
@@ -14,13 +14,68 @@ const Landing = () => {
const iconActive = `${process.env.PUBLIC_URL}/icon-active.png`; const iconActive = `${process.env.PUBLIC_URL}/icon-active.png`;
const heroLoaf = `${process.env.PUBLIC_URL}/hero-loaf.png`; const heroLoaf = `${process.env.PUBLIC_URL}/hero-loaf.png`;
const InfoCard = ({ iconSrc, infoTitle, description }) => (
<Card className="relative bg-white rounded-2xl overflow-visible flex flex-col gap-3.5 items-center pt-16 pb-0 w-full max-w-[363px]">
<div className="absolute -top-52 flex justify-center w-full">
<img
src={iconSrc}
alt={infoTitle}
className="w-full max-w-[330px] h-auto aspect-[10/9] object-contain"
/>
</div>
<div className="p-6 flex flex-col pt-10 gap-4.5 w-full">
<h5 className="text-[#48286e] text-[28px] leading-10 pb-10 font-semibold text-center" style={{ fontFamily: "'Poppins', sans-serif" }}>
{infoTitle}
</h5>
{description}
</div>
</Card>
);
const infoCardData = [
{
iconSrc: iconMeetGreet,
infoTitle: 'Meet and Greet',
description: (
<p className="text-[#48286e] text-lg text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
The MEET and GREETs provide opportunities for prospective members to get acquainted with LOAF, have conversations
with members, and ask the board of directors questions. They are held the 3rd Sunday of the month and usually
take place at a restaurant or other fun places conducive to its purpose. Please email{' '}
<a href="mailto:info@loaftx.org" className="underline">info@loaftx.org</a> for upcoming times and locations.
</p>
),
},
{
iconSrc: iconSocials,
infoTitle: 'Socials',
description: (
<p className="text-[#48286e] text-lg text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Our social events provide opportunities for members to explore Houston and connect with other lesbians. Past
social events include bowling, museums, painting lessons, sporting events, Miller Outdoor Theater, bingo and board
games, pool parties, putt putt golf, camping and holiday get togethers. No matter your age or ability, there is
something for everyone.
</p>
),
},
{
iconSrc: iconActive,
infoTitle: 'Active LOAFers',
description: (
<p className="text-[#48286e] text-lg text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
ActiveLOAFers events provide members with opportunities to be physically active. Past activities have included
hiking/walking in the park, swimming (or floating), pickleball, kayaking, bike riding, axe throwing, and strolling
through the botanic gardens or the Arboretum.
</p>
),
},
];
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-white">
<PublicNavbar /> <PublicNavbar />
{/* Hero Section */} {/* Hero Section */}
<section className="bg-gradient-to-b from-[#48286e] to-[#664fa3] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-8 md:py-12 lg:py-0 flex flex-col lg:flex-row gap-8 md:gap-12 lg:gap-16 items-center justify-center"> <section className="bg-gradient-to-b from-[#48286e] to-[#664fa3] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-20 sm:py-8 md:py-12 lg:py-16 flex flex-col lg:flex-row gap-8 md:gap-12 lg:gap-16 items-center justify-center">
<div className="py-8 md:py-10 flex flex-col gap-6 sm:gap-8 items-center justify-center w-full lg:w-[420px] lg:flex-shrink-0"> <div className="py-20 md:py-10 flex flex-col gap-6 sm:gap-8 items-center justify-center w-full lg:w-[420px] lg:flex-shrink-0">
<div className="flex flex-col gap-6 items-center"> <div className="flex flex-col gap-6 items-center">
<img src={heroLoaf} alt="LOAF" className="w-full max-w-[334px] h-auto object-contain" /> <img src={heroLoaf} alt="LOAF" className="w-full max-w-[334px] h-auto object-contain" />
</div> </div>
@@ -41,7 +96,7 @@ const Landing = () => {
</section> </section>
{/* About Section */} {/* About Section */}
<section id="about" className="bg-gradient-to-b from-white to-[#f1eef9] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 pt-12 sm:pt-16 md:pt-20 lg:pt-30 pb-0 flex flex-col gap-6 sm:gap-8"> <section id="about" className="bg-gradient-to-b pb-44 from-white to-[#f1eef9] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 pt-12 sm:pt-16 md:pt-20 lg:pt-30 flex flex-col gap-6 sm:gap-8">
<div className="flex flex-col items-center pt-12"> <div className="flex flex-col items-center pt-12">
<h3 className="text-[#48286e] text-3xl sm:text-4xl md:text-5xl font-extrabold text-center" style={{ fontFamily: "'Inter', sans-serif" }}> <h3 className="text-[#48286e] text-3xl sm:text-4xl md:text-5xl font-extrabold text-center" style={{ fontFamily: "'Inter', sans-serif" }}>
Welcome to LOAF Welcome to LOAF
@@ -54,55 +109,23 @@ const Landing = () => {
</section> </section>
{/* Feature Cards Section */} {/* Feature Cards Section */}
<section className="bg-gradient-to-b from-[#f1eef9] to-[#ddd8eb] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-12 sm:py-16 md:py-20 lg:py-30 flex flex-col md:flex-row gap-6 sm:gap-8 items-start justify-center"> <section className="bg-gradient-to-b from-[#f1eef9] to-[#ddd8eb] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-12 sm:py-16 md:py-20 lg:py-30 flex flex-col md:flex-row gap-6 sm:gap-8 items-stretch justify-center">
<Card className="bg-white rounded-2xl overflow-hidden flex flex-col gap-3.5 items-center pt-5 pb-0 w-full max-w-[363px]"> {infoCardData.map((card) => (
<img src={iconMeetGreet} alt="Meet and Greet" className="w-full max-w-[300px] h-auto aspect-[10/9] object-contain" /> <InfoCard key={card.infoTitle} {...card} />
<div className="p-6 flex flex-col gap-4.5 w-full"> ))}
<h5 className="text-[#48286e] text-2xl font-semibold text-center" style={{ fontFamily: "'Inter', sans-serif" }}>
Meet and Greet
</h5>
<p className="text-[#48286e] text-lg text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
The MEET and GREETs provide opportunities for prospective members to get acquainted with LOAF, have conversations with members, and ask the board of directors questions. They are held the 3rd Sunday of the month and usually take place at a restaurant or other fun places conducive to its purpose. Please email{' '}
<a href="mailto:info@loaftx.org" className="underline">info@loaftx.org</a> for upcoming times and locations.
</p>
</div>
</Card>
<Card className="bg-white rounded-2xl overflow-hidden flex flex-col gap-3.5 items-center pt-5 pb-0 w-full max-w-[363px]">
<img src={iconSocials} alt="Socials" className="w-full max-w-[300px] h-auto aspect-[10/9] object-contain" />
<div className="p-6 flex flex-col gap-4.5 w-full">
<h5 className="text-[#48286e] text-2xl font-semibold text-center" style={{ fontFamily: "'Inter', sans-serif" }}>
Socials
</h5>
<p className="text-[#48286e] text-lg text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Our social events provide opportunities for members to explore Houston and connect with other lesbians. Past social events include, bowling, museums, painting lessons, sporting events, Miller Outdoor Theater, bingo and board games, pool parties, putt putt golf, camping and holiday get togethers. No matter your age or ability, there is something for everyone.
</p>
</div>
</Card>
<Card className="bg-white rounded-2xl overflow-hidden flex flex-col gap-3.5 items-center pt-5 pb-0 w-full max-w-[363px]">
<img src={iconActive} alt="Active LOAFers" className="w-full max-w-[300px] h-auto aspect-[10/9] object-contain" />
<div className="p-6 flex flex-col gap-4.5 w-full">
<h5 className="text-[#48286e] text-2xl font-semibold text-center" style={{ fontFamily: "'Inter', sans-serif" }}>
Active LOAFers
</h5>
<p className="text-[#48286e] text-lg text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
ActiveLOAFers events provide members with opportunities to be physically active. Past activities have included, hiking/walking in the park, swimming (or floating), pickleball, kayaking, bike riding, axe throwing, and strolling through the botanic gardens or the Arboretum.
</p>
</div>
</Card>
</section> </section>
{/* CTA Section */} {/* CTA Section */}
<section className="bg-gradient-to-b from-[#644c9f] to-[#48286e] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-12 sm:py-16 md:py-20 lg:py-30 flex items-center justify-center"> <section className="bg-gradient-to-b from-[#644c9f] to-[#48286e] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-12 sm:py-16 md:py-20 lg:py-30 flex items-center justify-center">
<div className="flex flex-col lg:flex-row gap-8 sm:gap-10 md:gap-12 items-center justify-center w-full max-w-6xl"> <div className="flex flex-col lg:flex-row gap-8 sm:gap-10 md:gap-12 items-center justify-center w-full max-w-6xl">
<Link to="/register" className="w-full sm:w-auto"> <Link to="/register" className="w-full sm:w-auto">
<Button className="bg-[#DDD8EB] hover:bg-white text-[#422268] rounded-full px-6 py-6 sm:py-[32px] text-base sm:text-lg font-medium w-full sm:w-[392px] transition-colors"> <Button className="bg-[#DDD8EB] hover:bg-white text-[#422268] rounded-full px-3
py-7 text-base sm:text-lg font-medium w-full sm:w-[392px] transition-colors ">
Become a Member Become a Member
</Button> </Button>
</Link> </Link>
<div className="flex-1 flex items-center justify-center"> <div className="flex-1 flex items-center justify-center">
<h4 className="text-white text-2xl sm:text-3xl md:text-4xl font-bold text-center lg:text-left max-w-[718px]" style={{ fontFamily: "'Inter', sans-serif" }}> <h4 className="text-white text-3xl font-bold text-center lg:text-left max-w-[718px]" style={{ fontFamily: "'Poppins', sans-serif" }}>
No matter your age or ability, there is something for everyone. No matter your age or ability, there is something for everyone.
</h4> </h4>
</div> </div>

View File

@@ -8,7 +8,7 @@ import { Label } from '../components/ui/label';
import { Card } from '../components/ui/card'; import { Card } from '../components/ui/card';
import { toast } from 'sonner'; import { toast } from 'sonner';
import PublicNavbar from '../components/PublicNavbar'; import PublicNavbar from '../components/PublicNavbar';
import PublicFooter from '../components/PublicFooter'; import PublicFooter from '../components/PublicFooter-kc';
import { ArrowRight, ArrowLeft } from 'lucide-react'; import { ArrowRight, ArrowLeft } from 'lucide-react';
const Login = () => { const Login = () => {

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PublicNavbar from '../components/PublicNavbar'; import PublicNavbar from '../components/PublicNavbar';
import PublicFooter from '../components/PublicFooter'; import PublicFooter from '../components/PublicFooter-kc';
import { Card } from '../components/ui/card'; import { Card } from '../components/ui/card';
const MissionValues = () => { const MissionValues = () => {

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import PublicNavbar from '../components/PublicNavbar'; import PublicNavbar from '../components/PublicNavbar';
import PublicFooter from '../components/PublicFooter'; import PublicFooter from '../components/PublicFooter-kc';
export default function PrivacyPolicy() { export default function PrivacyPolicy() {
return ( return (

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect } from 'react';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import api from '../utils/api'; import api from '../utils/api';
import { Card } from '../components/ui/card'; import { Card } from '../components/ui/card';
@@ -9,8 +9,7 @@ import { Textarea } from '../components/ui/textarea';
import { toast } from 'sonner'; import { toast } from 'sonner';
import Navbar from '../components/Navbar'; import Navbar from '../components/Navbar';
import MemberFooter from '../components/MemberFooter'; import MemberFooter from '../components/MemberFooter';
import { User, Save, Lock, Heart, Users, Mail, BookUser, Camera, Upload, Trash2 } from 'lucide-react'; import { User, Save, Lock, Heart, Users, Mail, BookUser } from 'lucide-react';
import { Avatar, AvatarImage, AvatarFallback } from '../components/ui/avatar';
import ChangePasswordDialog from '../components/ChangePasswordDialog'; import ChangePasswordDialog from '../components/ChangePasswordDialog';
const Profile = () => { const Profile = () => {
@@ -18,12 +17,6 @@ const Profile = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [profileData, setProfileData] = useState(null); const [profileData, setProfileData] = useState(null);
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false); const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
const [profilePhotoUrl, setProfilePhotoUrl] = useState(null);
const [previewImage, setPreviewImage] = useState(null);
const [uploadingPhoto, setUploadingPhoto] = useState(false);
const fileInputRef = useRef(null);
const [maxFileSizeMB, setMaxFileSizeMB] = useState(50); // Default 50MB
const [maxFileSizeBytes, setMaxFileSizeBytes] = useState(52428800); // Default 50MB in bytes
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
// Personal Information // Personal Information
first_name: '', first_name: '',
@@ -56,27 +49,13 @@ const Profile = () => {
}); });
useEffect(() => { useEffect(() => {
fetchConfig();
fetchProfile(); fetchProfile();
}, []); }, []);
const fetchConfig = async () => {
try {
const response = await api.get('/config');
setMaxFileSizeMB(response.data.max_file_size_mb);
setMaxFileSizeBytes(response.data.max_file_size_bytes);
} catch (error) {
console.error('Failed to fetch config, using defaults:', error);
// Keep default values if fetch fails
}
};
const fetchProfile = async () => { const fetchProfile = async () => {
try { try {
const response = await api.get('/users/profile'); const response = await api.get('/users/profile');
setProfileData(response.data); setProfileData(response.data);
setProfilePhotoUrl(response.data.profile_photo_url);
setPreviewImage(response.data.profile_photo_url);
setFormData({ setFormData({
// Personal Information // Personal Information
first_name: response.data.first_name || '', first_name: response.data.first_name || '',
@@ -145,57 +124,6 @@ const Profile = () => {
'Hospitality' 'Hospitality'
]; ];
const handlePhotoUpload = async (e) => {
const file = e.target.files[0];
if (!file) return;
// Validate file type
if (!file.type.startsWith('image/')) {
toast.error('Please select an image file');
return;
}
// Validate file size
if (file.size > maxFileSizeBytes) {
toast.error(`File size must be less than ${maxFileSizeMB}MB`);
return;
}
setUploadingPhoto(true);
try {
const formData = new FormData();
formData.append('file', file);
const response = await api.post('/members/profile/upload-photo', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
setProfilePhotoUrl(response.data.profile_photo_url);
setPreviewImage(response.data.profile_photo_url);
toast.success('Profile photo updated successfully!');
} catch (error) {
toast.error('Failed to upload photo');
} finally {
setUploadingPhoto(false);
}
};
const handlePhotoDelete = async () => {
if (!profilePhotoUrl) return;
setUploadingPhoto(true);
try {
await api.delete('/members/profile/delete-photo');
setProfilePhotoUrl(null);
setPreviewImage(null);
toast.success('Profile photo deleted successfully!');
} catch (error) {
toast.error('Failed to delete photo');
} finally {
setUploadingPhoto(false);
}
};
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
setLoading(true); setLoading(true);
@@ -277,59 +205,6 @@ const Profile = () => {
</div> </div>
</div> </div>
{/* Profile Photo Section */}
<div className="pb-8 mb-8 border-b border-[#ddd8eb]">
<h2 className="text-2xl font-semibold text-[#422268] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<Camera className="h-6 w-6 text-[#664fa3]" />
Profile Photo
</h2>
<div className="flex flex-col md:flex-row items-center gap-6">
<Avatar className="h-32 w-32 border-4 border-[#ddd8eb]">
<AvatarImage src={previewImage} alt="Profile" />
<AvatarFallback className="bg-[#f1eef9] text-[#664fa3] text-3xl">
{profileData?.first_name?.charAt(0)}{profileData?.last_name?.charAt(0)}
</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-3">
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handlePhotoUpload}
className="hidden"
/>
<Button
type="button"
onClick={() => fileInputRef.current?.click()}
disabled={uploadingPhoto}
className="bg-[#664fa3] text-white hover:bg-[#422268] rounded-full px-6 py-3"
>
<Upload className="h-4 w-4 mr-2" />
{uploadingPhoto ? 'Uploading...' : 'Upload Photo'}
</Button>
{profilePhotoUrl && (
<Button
type="button"
onClick={handlePhotoDelete}
disabled={uploadingPhoto}
variant="outline"
className="border-2 border-red-500 text-red-500 hover:bg-red-50 rounded-full px-6 py-3"
>
<Trash2 className="h-4 w-4 mr-2" />
Delete Photo
</Button>
)}
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Upload a profile photo (Max {maxFileSizeMB}MB)
</p>
</div>
</div>
</div>
{/* Editable Form */} {/* Editable Form */}
<form onSubmit={handleSubmit} className="space-y-6" data-testid="profile-form"> <form onSubmit={handleSubmit} className="space-y-6" data-testid="profile-form">
<h2 className="text-2xl font-semibold text-[#422268] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}> <h2 className="text-2xl font-semibold text-[#422268] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>

View File

@@ -5,7 +5,7 @@ import { Button } from '../components/ui/button';
import { Card } from '../components/ui/card'; import { Card } from '../components/ui/card';
import { toast } from 'sonner'; import { toast } from 'sonner';
import PublicNavbar from '../components/PublicNavbar'; import PublicNavbar from '../components/PublicNavbar';
import PublicFooter from '../components/PublicFooter'; import PublicFooter from '../components/PublicFooter-kc';
import { ArrowRight, ArrowLeft } from 'lucide-react'; import { ArrowRight, ArrowLeft } from 'lucide-react';
import RegistrationStepIndicator from '../components/registration/RegistrationStepIndicator'; import RegistrationStepIndicator from '../components/registration/RegistrationStepIndicator';
import RegistrationStep1 from '../components/registration/RegistrationStep1'; import RegistrationStep1 from '../components/registration/RegistrationStep1';

View File

@@ -7,7 +7,7 @@ import { Label } from '../components/ui/label';
import { Card } from '../components/ui/card'; import { Card } from '../components/ui/card';
import { toast } from 'sonner'; import { toast } from 'sonner';
import PublicNavbar from '../components/PublicNavbar'; import PublicNavbar from '../components/PublicNavbar';
import PublicFooter from '../components/PublicFooter'; import PublicFooter from '../components/PublicFooter-kc';
import { ArrowRight, Lock, AlertCircle } from 'lucide-react'; import { ArrowRight, Lock, AlertCircle } from 'lucide-react';
const ResetPassword = () => { const ResetPassword = () => {

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import PublicNavbar from '../components/PublicNavbar'; import PublicNavbar from '../components/PublicNavbar';
import PublicFooter from '../components/PublicFooter'; import PublicFooter from '../components/PublicFooter-kc';
import { Card } from '../components/ui/card'; import { Card } from '../components/ui/card';
import { ChevronDown, ExternalLink, Phone, Mail, MapPin } from 'lucide-react'; import { ChevronDown, ExternalLink, Phone, Mail, MapPin } from 'lucide-react';
@@ -133,16 +133,14 @@ const Resources = () => {
{resource.name} {resource.name}
</span> </span>
<ChevronDown <ChevronDown
className={`h-6 w-6 flex-shrink-0 ml-3 transition-transform duration-300 ${ className={`h-6 w-6 flex-shrink-0 ml-3 transition-transform duration-300 ${isExpanded ? 'rotate-180' : ''
isExpanded ? 'rotate-180' : ''
}`} }`}
/> />
</button> </button>
{/* Accordion Content */} {/* Accordion Content */}
<div <div
className={`transition-all duration-300 ease-in-out ${ className={`transition-all duration-300 ease-in-out ${isExpanded ? 'max-h-[1000px] opacity-100 mt-3' : 'max-h-0 opacity-0'
isExpanded ? 'max-h-[1000px] opacity-100 mt-3' : 'max-h-0 opacity-0'
}`} }`}
> >
<Card className="p-6 bg-white rounded-2xl border-2 border-[#ddd8eb] shadow-lg"> <Card className="p-6 bg-white rounded-2xl border-2 border-[#ddd8eb] shadow-lg">

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import PublicNavbar from '../components/PublicNavbar'; import PublicNavbar from '../components/PublicNavbar';
import PublicFooter from '../components/PublicFooter'; import PublicFooter from '../components/PublicFooter-kc';
export default function TermsOfService() { export default function TermsOfService() {
return ( return (

View File

@@ -5,7 +5,7 @@ import { Button } from '../components/ui/button';
import { Card } from '../components/ui/card'; import { Card } from '../components/ui/card';
import { CheckCircle, XCircle, Loader2 } from 'lucide-react'; import { CheckCircle, XCircle, Loader2 } from 'lucide-react';
import PublicNavbar from '../components/PublicNavbar'; import PublicNavbar from '../components/PublicNavbar';
import PublicFooter from '../components/PublicFooter'; import PublicFooter from '../components/PublicFooter-kc';
const API_URL = process.env.REACT_APP_BACKEND_URL; const API_URL = process.env.REACT_APP_BACKEND_URL;

View File

@@ -1,472 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Card } from '../../components/ui/card';
import { Button } from '../../components/ui/button';
import { Input } from '../../components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '../../components/ui/select';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '../../components/ui/dropdown-menu';
import { Badge } from '../../components/ui/badge';
import api from '../../utils/api';
import { toast } from 'sonner';
import {
DollarSign,
Heart,
Users,
Globe,
Search,
Loader2,
Download,
FileDown,
Calendar
} from 'lucide-react';
const AdminDonations = () => {
const [donations, setDonations] = useState([]);
const [filteredDonations, setFilteredDonations] = useState([]);
const [stats, setStats] = useState({});
const [loading, setLoading] = useState(true);
const [exporting, setExporting] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [typeFilter, setTypeFilter] = useState('all');
const [statusFilter, setStatusFilter] = useState('all');
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
useEffect(() => {
fetchData();
}, []);
useEffect(() => {
filterDonations();
}, [searchQuery, typeFilter, statusFilter, startDate, endDate, donations]);
const fetchData = async () => {
setLoading(true);
try {
const [donationsResponse, statsResponse] = await Promise.all([
api.get('/admin/donations'),
api.get('/admin/donations/stats')
]);
setDonations(donationsResponse.data);
setStats(statsResponse.data);
} catch (error) {
console.error('Failed to fetch donation data:', error);
toast.error('Failed to load donation data');
} finally {
setLoading(false);
}
};
const filterDonations = () => {
let filtered = [...donations];
// Search filter (donor name or email)
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
filtered = filtered.filter(donation =>
donation.donor_name?.toLowerCase().includes(query) ||
donation.donor_email?.toLowerCase().includes(query)
);
}
// Type filter
if (typeFilter !== 'all') {
filtered = filtered.filter(donation => donation.donation_type === typeFilter);
}
// Status filter
if (statusFilter !== 'all') {
filtered = filtered.filter(donation => donation.status === statusFilter);
}
// Date range filter
if (startDate) {
filtered = filtered.filter(donation =>
new Date(donation.created_at) >= new Date(startDate)
);
}
if (endDate) {
filtered = filtered.filter(donation =>
new Date(donation.created_at) <= new Date(endDate)
);
}
setFilteredDonations(filtered);
};
const handleExport = async (exportType) => {
setExporting(true);
try {
const params = exportType === 'current' ? {
donation_type: typeFilter !== 'all' ? typeFilter : undefined,
status: statusFilter !== 'all' ? statusFilter : undefined,
start_date: startDate || undefined,
end_date: endDate || undefined,
search: searchQuery || undefined
} : {};
const response = await api.get('/admin/donations/export', {
params,
responseType: 'blob'
});
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `donations_export_${new Date().toISOString().split('T')[0]}.csv`);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
toast.success('Donations exported successfully');
} catch (error) {
console.error('Failed to export donations:', error);
toast.error('Failed to export donations');
} finally {
setExporting(false);
}
};
const formatPrice = (cents) => {
return `$${(cents / 100).toFixed(2)}`;
};
const formatDate = (dateString) => {
if (!dateString) return 'N/A';
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
const getStatusBadgeVariant = (status) => {
const variants = {
completed: 'default',
pending: 'secondary',
failed: 'destructive'
};
return variants[status] || 'outline';
};
const getTypeBadgeColor = (type) => {
return type === 'member' ? 'bg-[#81B29A]' : 'bg-[#664fa3]';
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<Loader2 className="h-12 w-12 animate-spin text-[#664fa3]" />
</div>
);
}
return (
<div className="space-y-8">
{/* Header */}
<div>
<h1 className="text-3xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
Donation Management
</h1>
<p className="text-[#664fa3] mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Track and manage all donations from members and the public
</p>
</div>
{/* Stats Cards */}
<div className="grid md:grid-cols-4 gap-6">
<Card className="p-6 bg-white rounded-2xl border-2 border-[#ddd8eb]">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Total Donations
</p>
<p className="text-3xl font-bold text-[#422268] mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
{stats.total_donations || 0}
</p>
</div>
<div className="p-3 bg-[#DDD8EB]/20 rounded-full">
<Heart className="h-6 w-6 text-[#664fa3]" />
</div>
</div>
</Card>
<Card className="p-6 bg-white rounded-2xl border-2 border-[#ddd8eb]">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Member Donations
</p>
<p className="text-3xl font-bold text-[#81B29A] mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
{stats.member_donations || 0}
</p>
</div>
<div className="p-3 bg-[#81B29A]/10 rounded-full">
<Users className="h-6 w-6 text-[#81B29A]" />
</div>
</div>
</Card>
<Card className="p-6 bg-white rounded-2xl border-2 border-[#ddd8eb]">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Public Donations
</p>
<p className="text-3xl font-bold text-[#664fa3] mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
{stats.public_donations || 0}
</p>
</div>
<div className="p-3 bg-[#DDD8EB]/20 rounded-full">
<Globe className="h-6 w-6 text-[#664fa3]" />
</div>
</div>
</Card>
<Card className="p-6 bg-white rounded-2xl border-2 border-[#ddd8eb]">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Total Amount
</p>
<p className="text-3xl font-bold text-[#422268] mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
{stats.total_amount || '$0.00'}
</p>
</div>
<div className="p-3 bg-[#DDD8EB]/20 rounded-full">
<DollarSign className="h-6 w-6 text-[#664fa3]" />
</div>
</div>
</Card>
</div>
{/* Filters and Actions */}
<Card className="p-6 bg-white rounded-2xl border-2 border-[#ddd8eb]">
<div className="space-y-4">
{/* Search and Export Row */}
<div className="flex flex-col md:flex-row gap-4 justify-between">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-[#664fa3]" />
<Input
placeholder="Search by donor name or email..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 rounded-full border-2 border-[#ddd8eb] focus:border-[#664fa3]"
/>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
disabled={exporting}
className="bg-[#81B29A] text-white hover:bg-[#6a9680] rounded-full px-6 py-3 flex items-center gap-2"
>
<Download className="h-4 w-4" />
{exporting ? 'Exporting...' : 'Export'}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56 bg-white rounded-xl border-2 border-[#ddd8eb] shadow-lg">
<DropdownMenuItem
onClick={() => handleExport('all')}
className="cursor-pointer hover:bg-[#f1eef9] rounded-lg p-3"
>
<FileDown className="h-4 w-4 mr-2 text-[#664fa3]" />
<span className="text-[#422268]">Export All Donations</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleExport('current')}
className="cursor-pointer hover:bg-[#f1eef9] rounded-lg p-3"
>
<FileDown className="h-4 w-4 mr-2 text-[#664fa3]" />
<span className="text-[#422268]">Export Current View</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* Filters Row */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<Select value={typeFilter} onValueChange={setTypeFilter}>
<SelectTrigger className="rounded-full border-2 border-[#ddd8eb]">
<SelectValue placeholder="All Types" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
<SelectItem value="member">Member Donations</SelectItem>
<SelectItem value="public">Public Donations</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="rounded-full border-2 border-[#ddd8eb]">
<SelectValue placeholder="All Statuses" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Statuses</SelectItem>
<SelectItem value="completed">Completed</SelectItem>
<SelectItem value="pending">Pending</SelectItem>
<SelectItem value="failed">Failed</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="rounded-full border-2 border-[#ddd8eb]"
placeholder="Start Date"
/>
</div>
<div>
<Input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="rounded-full border-2 border-[#ddd8eb]"
placeholder="End Date"
/>
</div>
</div>
{/* Active Filters Summary */}
{(searchQuery || typeFilter !== 'all' || statusFilter !== 'all' || startDate || endDate) && (
<div className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Showing {filteredDonations.length} of {donations.length} donations
</div>
)}
</div>
</Card>
{/* Donations Table */}
<Card className="bg-white rounded-2xl border-2 border-[#ddd8eb] overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-[#f1eef9] border-b-2 border-[#ddd8eb]">
<tr>
<th className="px-6 py-4 text-left text-sm font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
Donor
</th>
<th className="px-6 py-4 text-left text-sm font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
Type
</th>
<th className="px-6 py-4 text-left text-sm font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
Amount
</th>
<th className="px-6 py-4 text-left text-sm font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
Status
</th>
<th className="px-6 py-4 text-left text-sm font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
Date
</th>
<th className="px-6 py-4 text-left text-sm font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
Payment Method
</th>
</tr>
</thead>
<tbody className="divide-y divide-[#ddd8eb]">
{filteredDonations.length === 0 ? (
<tr>
<td colSpan="6" className="px-6 py-12 text-center">
<div className="flex flex-col items-center gap-3">
<Heart className="h-12 w-12 text-[#ddd8eb]" />
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{donations.length === 0 ? 'No donations yet' : 'No donations match your filters'}
</p>
</div>
</td>
</tr>
) : (
filteredDonations.map((donation) => (
<tr key={donation.id} className="hover:bg-[#f9f5ff] transition-colors">
<td className="px-6 py-4">
<div>
<p className="font-medium text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
{donation.donor_name || 'Anonymous'}
</p>
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{donation.donor_email || 'No email'}
</p>
</div>
</td>
<td className="px-6 py-4">
<Badge
className={`${getTypeBadgeColor(donation.donation_type)} text-white border-none rounded-full px-3 py-1`}
style={{ fontFamily: "'Inter', sans-serif" }}
>
{donation.donation_type === 'member' ? 'Member' : 'Public'}
</Badge>
</td>
<td className="px-6 py-4">
<p className="font-semibold text-[#422268] text-lg" style={{ fontFamily: "'Inter', sans-serif" }}>
{donation.amount}
</p>
</td>
<td className="px-6 py-4">
<Badge variant={getStatusBadgeVariant(donation.status)} className="rounded-full">
{donation.status.charAt(0).toUpperCase() + donation.status.slice(1)}
</Badge>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2 text-[#664fa3]">
<Calendar className="h-4 w-4" />
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{formatDate(donation.created_at)}
</span>
</div>
</td>
<td className="px-6 py-4">
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{donation.payment_method || 'N/A'}
</p>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</Card>
{/* This Month Summary */}
{stats.this_month_count > 0 && (
<Card className="p-6 bg-gradient-to-r from-[#f9f5ff] to-[#f1eef9] rounded-2xl border-2 border-[#ddd8eb]">
<div className="flex items-center justify-between">
<div>
<p className="text-[#664fa3] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
This Month's Donations
</p>
<p className="text-2xl font-bold text-[#422268] mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
{stats.this_month_count} donations • {stats.this_month_amount}
</p>
</div>
<div className="p-4 bg-white rounded-full shadow-sm">
<Heart className="h-8 w-8 text-[#ff9e77]" />
</div>
</div>
</Card>
)}
</div>
);
};
export default AdminDonations;

View File

@@ -1,5 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import api from '../../utils/api'; import api from '../../utils/api';
import { Card } from '../../components/ui/card'; import { Card } from '../../components/ui/card';
@@ -12,10 +11,9 @@ import CreateStaffDialog from '../../components/CreateStaffDialog';
import InviteStaffDialog from '../../components/InviteStaffDialog'; import InviteStaffDialog from '../../components/InviteStaffDialog';
import PendingInvitationsTable from '../../components/PendingInvitationsTable'; import PendingInvitationsTable from '../../components/PendingInvitationsTable';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { UserCog, Search, Shield, UserPlus, Mail, Edit, Eye } from 'lucide-react'; import { UserCog, Search, Shield, UserPlus, Mail } from 'lucide-react';
const AdminStaff = () => { const AdminStaff = () => {
const navigate = useNavigate();
const { hasPermission } = useAuth(); const { hasPermission } = useAuth();
const [users, setUsers] = useState([]); const [users, setUsers] = useState([]);
const [filteredUsers, setFilteredUsers] = useState([]); const [filteredUsers, setFilteredUsers] = useState([]);
@@ -248,18 +246,6 @@ const AdminStaff = () => {
</div> </div>
</div> </div>
</div> </div>
{/* Actions */}
<div className="flex gap-2">
<Button
onClick={() => navigate(`/admin/users/${user.id}`)}
variant="outline"
className="border-2 border-[#664fa3] text-[#664fa3] hover:bg-[#f1eef9] rounded-full px-4 py-2"
>
<Edit className="h-4 w-4 mr-2" />
Manage
</Button>
</div>
</div> </div>
</Card> </Card>
))} ))}

View File

@@ -30,18 +30,8 @@ import {
Loader2, Loader2,
Calendar, Calendar,
Edit, Edit,
XCircle, XCircle
Download,
FileDown,
AlertTriangle,
Info
} from 'lucide-react'; } from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '../../components/ui/dropdown-menu';
const AdminSubscriptions = () => { const AdminSubscriptions = () => {
const [subscriptions, setSubscriptions] = useState([]); const [subscriptions, setSubscriptions] = useState([]);
@@ -52,7 +42,6 @@ const AdminSubscriptions = () => {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all');
const [planFilter, setPlanFilter] = useState('all'); const [planFilter, setPlanFilter] = useState('all');
const [exporting, setExporting] = useState(false);
// Edit subscription dialog state // Edit subscription dialog state
const [editDialogOpen, setEditDialogOpen] = useState(false); const [editDialogOpen, setEditDialogOpen] = useState(false);
@@ -129,62 +118,6 @@ const AdminSubscriptions = () => {
const handleSaveSubscription = async () => { const handleSaveSubscription = async () => {
if (!selectedSubscription) return; if (!selectedSubscription) return;
// Check if status is changing
const statusChanged = editFormData.status !== selectedSubscription.status;
if (statusChanged) {
// Get status change consequences
let warningMessage = '';
let confirmText = '';
if (editFormData.status === 'cancelled') {
warningMessage = `⚠️ CRITICAL: Cancelling this subscription will:
• Set the user's status to INACTIVE
• Remove their member access immediately
• Stop all future billing
• This action affects: ${selectedSubscription.user.first_name} ${selectedSubscription.user.last_name} (${selectedSubscription.user.email})
Current Status: ${selectedSubscription.status.toUpperCase()}
New Status: CANCELLED
Are you absolutely sure you want to proceed?`;
confirmText = 'Yes, Cancel Subscription';
} else if (editFormData.status === 'expired') {
warningMessage = `⚠️ WARNING: Setting this subscription to EXPIRED will:
• Set the user's status to INACTIVE
• Remove their member access
• Mark the subscription as ended
• This action affects: ${selectedSubscription.user.first_name} ${selectedSubscription.user.last_name} (${selectedSubscription.user.email})
Current Status: ${selectedSubscription.status.toUpperCase()}
New Status: EXPIRED
Are you sure you want to proceed?`;
confirmText = 'Yes, Mark as Expired';
} else if (editFormData.status === 'active') {
warningMessage = `✓ Activating this subscription will:
• Set the user's status to ACTIVE
• Grant full member access
• Resume billing if applicable
• This action affects: ${selectedSubscription.user.first_name} ${selectedSubscription.user.last_name} (${selectedSubscription.user.email})
Current Status: ${selectedSubscription.status.toUpperCase()}
New Status: ACTIVE
Proceed with activation?`;
confirmText = 'Yes, Activate Subscription';
}
// Show confirmation dialog
const confirmed = window.confirm(warningMessage);
if (!confirmed) {
return; // User cancelled
}
}
setIsUpdating(true); setIsUpdating(true);
try { try {
await api.put(`/admin/subscriptions/${selectedSubscription.id}`, { await api.put(`/admin/subscriptions/${selectedSubscription.id}`, {
@@ -218,38 +151,6 @@ Proceed with activation?`;
} }
}; };
const handleExport = async (exportType) => {
setExporting(true);
try {
const params = exportType === 'current' ? {
status: statusFilter !== 'all' ? statusFilter : undefined,
plan_id: planFilter !== 'all' ? planFilter : undefined,
search: searchQuery || undefined
} : {};
const response = await api.get('/admin/subscriptions/export', {
params,
responseType: 'blob'
});
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `subscriptions_export_${new Date().toISOString().split('T')[0]}.csv`);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
toast.success('Subscriptions exported successfully');
} catch (error) {
console.error('Failed to export subscriptions:', error);
toast.error('Failed to export subscriptions');
} finally {
setExporting(false);
}
};
const formatPrice = (cents) => { const formatPrice = (cents) => {
return `$${(cents / 100).toFixed(2)}`; return `$${(cents / 100).toFixed(2)}`;
}; };
@@ -406,40 +307,9 @@ Proceed with activation?`;
</div> </div>
</div> </div>
<div className="mt-4 flex items-center justify-between"> <div className="mt-4 text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<div className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Showing {filteredSubscriptions.length} of {subscriptions.length} subscriptions Showing {filteredSubscriptions.length} of {subscriptions.length} subscriptions
</div> </div>
{/* Export Dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
disabled={exporting}
className="bg-[#81B29A] text-white hover:bg-[#6a9680] rounded-full px-6 py-2 flex items-center gap-2"
>
<Download className="h-4 w-4" />
{exporting ? 'Exporting...' : 'Export'}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56 bg-white rounded-xl border-2 border-[#ddd8eb] shadow-lg">
<DropdownMenuItem
onClick={() => handleExport('all')}
className="cursor-pointer hover:bg-[#f1eef9] rounded-lg p-3"
>
<FileDown className="h-4 w-4 mr-2 text-[#664fa3]" />
<span className="text-[#422268]">Export All Subscriptions</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleExport('current')}
className="cursor-pointer hover:bg-[#f1eef9] rounded-lg p-3"
>
<FileDown className="h-4 w-4 mr-2 text-[#664fa3]" />
<span className="text-[#422268]">Export Current View</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</Card> </Card>
{/* Subscriptions Table */} {/* Subscriptions Table */}
@@ -672,59 +542,6 @@ Proceed with activation?`;
<SelectItem value="expired">Expired</SelectItem> <SelectItem value="expired">Expired</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
{/* Warning Box - Show when status is different */}
{selectedSubscription && editFormData.status !== selectedSubscription.status && (
<div className={`mt-3 p-4 rounded-xl border-2 ${
editFormData.status === 'cancelled'
? 'bg-red-50 border-red-300'
: editFormData.status === 'expired'
? 'bg-orange-50 border-orange-300'
: 'bg-green-50 border-green-300'
}`}>
<div className="flex items-start gap-3">
{editFormData.status === 'cancelled' || editFormData.status === 'expired' ? (
<AlertTriangle className="h-5 w-5 text-red-600 flex-shrink-0 mt-0.5" />
) : (
<Info className="h-5 w-5 text-green-600 flex-shrink-0 mt-0.5" />
)}
<div className="flex-1">
<p className="font-semibold text-sm mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
{editFormData.status === 'cancelled' && 'Critical: This will cancel the subscription'}
{editFormData.status === 'expired' && 'Warning: This will mark subscription as expired'}
{editFormData.status === 'active' && 'This will activate the subscription'}
</p>
<ul className="text-xs space-y-1 list-disc list-inside" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{editFormData.status === 'cancelled' && (
<>
<li>User status will be set to INACTIVE</li>
<li>Member access will be removed immediately</li>
<li>All future billing will be stopped</li>
</>
)}
{editFormData.status === 'expired' && (
<>
<li>User status will be set to INACTIVE</li>
<li>Member access will be removed</li>
<li>Subscription will be marked as ended</li>
</>
)}
{editFormData.status === 'active' && (
<>
<li>User status will be set to ACTIVE</li>
<li>Full member access will be granted</li>
<li>Billing will resume if applicable</li>
</>
)}
</ul>
<p className="text-xs mt-2 font-medium">
Current: <span className="font-bold">{selectedSubscription.status.toUpperCase()}</span>
New: <span className="font-bold">{editFormData.status.toUpperCase()}</span>
</p>
</div>
</div>
</div>
)}
</div> </div>
{/* End Date */} {/* End Date */}

View File

@@ -1,11 +1,10 @@
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import api from '../../utils/api'; import api from '../../utils/api';
import { Card } from '../../components/ui/card'; import { Card } from '../../components/ui/card';
import { Button } from '../../components/ui/button'; import { Button } from '../../components/ui/button';
import { Badge } from '../../components/ui/badge'; import { Badge } from '../../components/ui/badge';
import { Avatar, AvatarImage, AvatarFallback } from '../../components/ui/avatar'; import { ArrowLeft, Mail, Phone, MapPin, Calendar, Lock, AlertTriangle } from 'lucide-react';
import { ArrowLeft, Mail, Phone, MapPin, Calendar, Lock, AlertTriangle, Camera, Upload, Trash2 } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import ConfirmationDialog from '../../components/ConfirmationDialog'; import ConfirmationDialog from '../../components/ConfirmationDialog';
@@ -20,13 +19,8 @@ const AdminUserView = () => {
const [subscriptionsLoading, setSubscriptionsLoading] = useState(true); const [subscriptionsLoading, setSubscriptionsLoading] = useState(true);
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false); const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
const [pendingAction, setPendingAction] = useState(null); const [pendingAction, setPendingAction] = useState(null);
const [uploadingPhoto, setUploadingPhoto] = useState(false);
const [maxFileSizeMB, setMaxFileSizeMB] = useState(50);
const [maxFileSizeBytes, setMaxFileSizeBytes] = useState(52428800);
const fileInputRef = useRef(null);
useEffect(() => { useEffect(() => {
fetchConfig();
fetchUserProfile(); fetchUserProfile();
fetchSubscriptions(); fetchSubscriptions();
}, [userId]); }, [userId]);
@@ -54,80 +48,6 @@ const AdminUserView = () => {
} }
}; };
const fetchConfig = async () => {
try {
const response = await api.get('/config');
setMaxFileSizeMB(response.data.max_file_size_mb);
setMaxFileSizeBytes(response.data.max_file_size_bytes);
} catch (error) {
console.error('Failed to fetch config, using defaults:', error);
}
};
const handlePhotoUpload = async (e) => {
const file = e.target.files[0];
if (!file) return;
// Validate file type
if (!file.type.startsWith('image/')) {
toast.error('Please select an image file');
return;
}
// Validate file size
if (file.size > maxFileSizeBytes) {
toast.error(`File size must be less than ${maxFileSizeMB}MB`);
return;
}
setUploadingPhoto(true);
try {
const formData = new FormData();
formData.append('file', file);
const response = await api.post(`/admin/users/${userId}/upload-photo`, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
// Update user state with new photo URL
setUser(prev => ({
...prev,
profile_photo_url: response.data.profile_photo_url
}));
toast.success('Profile photo updated successfully');
} catch (error) {
toast.error(error.response?.data?.detail || 'Failed to upload photo');
} finally {
setUploadingPhoto(false);
// Reset file input
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
};
const handlePhotoDelete = async () => {
if (!user?.profile_photo_url) return;
setUploadingPhoto(true);
try {
await api.delete(`/admin/users/${userId}/delete-photo`);
// Update user state to remove photo URL
setUser(prev => ({
...prev,
profile_photo_url: null
}));
toast.success('Profile photo deleted successfully');
} catch (error) {
toast.error(error.response?.data?.detail || 'Failed to delete photo');
} finally {
setUploadingPhoto(false);
}
};
const handleResetPasswordRequest = () => { const handleResetPasswordRequest = () => {
setPendingAction({ type: 'reset_password' }); setPendingAction({ type: 'reset_password' });
setConfirmDialogOpen(true); setConfirmDialogOpen(true);
@@ -221,12 +141,9 @@ const AdminUserView = () => {
<Card className="p-8 bg-white rounded-2xl border border-[#ddd8eb] mb-8"> <Card className="p-8 bg-white rounded-2xl border border-[#ddd8eb] mb-8">
<div className="flex items-start gap-6"> <div className="flex items-start gap-6">
{/* Avatar */} {/* Avatar */}
<Avatar className="h-24 w-24 border-4 border-[#ddd8eb]"> <div className="h-24 w-24 rounded-full bg-[#DDD8EB] flex items-center justify-center text-[#422268] font-semibold text-3xl">
<AvatarImage src={user.profile_photo_url} alt={`${user.first_name} ${user.last_name}`} />
<AvatarFallback className="bg-[#DDD8EB] text-[#422268] font-semibold text-3xl">
{user.first_name?.[0]}{user.last_name?.[0]} {user.first_name?.[0]}{user.last_name?.[0]}
</AvatarFallback> </div>
</Avatar>
{/* User Info */} {/* User Info */}
<div className="flex-1"> <div className="flex-1">
@@ -290,37 +207,6 @@ const AdminUserView = () => {
</Button> </Button>
)} )}
{/* Profile Photo Management */}
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handlePhotoUpload}
className="hidden"
/>
<Button
onClick={() => fileInputRef.current?.click()}
disabled={uploadingPhoto}
variant="outline"
className="border-2 border-[#81B29A] text-[#81B29A] hover:bg-[#E8F5E9] rounded-full px-4 py-2 disabled:opacity-50"
>
<Upload className="h-4 w-4 mr-2" />
{uploadingPhoto ? 'Uploading...' : 'Upload Photo'}
</Button>
{user.profile_photo_url && (
<Button
onClick={handlePhotoDelete}
disabled={uploadingPhoto}
variant="outline"
className="border-2 border-red-500 text-red-500 hover:bg-red-50 rounded-full px-4 py-2 disabled:opacity-50"
>
<Trash2 className="h-4 w-4 mr-2" />
Delete Photo
</Button>
)}
<div className="flex items-center gap-2 text-sm text-[#664fa3] ml-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <div className="flex items-center gap-2 text-sm text-[#664fa3] ml-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<AlertTriangle className="h-4 w-4" /> <AlertTriangle className="h-4 w-4" />
<span>User will receive a temporary password via email</span> <span>User will receive a temporary password via email</span>

View File

@@ -29,10 +29,9 @@ import {
PaginationEllipsis, PaginationEllipsis,
} from '../../components/ui/pagination'; } from '../../components/ui/pagination';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { CheckCircle, Clock, Search, ArrowUp, ArrowDown, X } from 'lucide-react'; import { CheckCircle, Clock, Search, ArrowUp, ArrowDown } from 'lucide-react';
import PaymentActivationDialog from '../../components/PaymentActivationDialog'; import PaymentActivationDialog from '../../components/PaymentActivationDialog';
import ConfirmationDialog from '../../components/ConfirmationDialog'; import ConfirmationDialog from '../../components/ConfirmationDialog';
import RejectionDialog from '../../components/RejectionDialog';
const AdminValidations = () => { const AdminValidations = () => {
const [pendingUsers, setPendingUsers] = useState([]); const [pendingUsers, setPendingUsers] = useState([]);
@@ -43,8 +42,6 @@ const AdminValidations = () => {
const [selectedUserForPayment, setSelectedUserForPayment] = useState(null); const [selectedUserForPayment, setSelectedUserForPayment] = useState(null);
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false); const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
const [pendingAction, setPendingAction] = useState(null); const [pendingAction, setPendingAction] = useState(null);
const [rejectionDialogOpen, setRejectionDialogOpen] = useState(false);
const [userToReject, setUserToReject] = useState(null);
// Filtering state // Filtering state
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
@@ -196,28 +193,6 @@ const AdminValidations = () => {
fetchPendingUsers(); // Refresh list fetchPendingUsers(); // Refresh list
}; };
const handleRejectUser = (user) => {
setUserToReject(user);
setRejectionDialogOpen(true);
};
const confirmRejection = async (reason) => {
if (!userToReject) return;
setActionLoading(userToReject.id);
try {
await api.post(`/admin/users/${userToReject.id}/reject`, { reason });
toast.success(`${userToReject.first_name} ${userToReject.last_name} has been rejected`);
fetchPendingUsers();
setRejectionDialogOpen(false);
setUserToReject(null);
} catch (error) {
toast.error(error.response?.data?.detail || 'Failed to reject user');
} finally {
setActionLoading(null);
}
};
const getStatusBadge = (status) => { const getStatusBadge = (status) => {
const config = { const config = {
pending_email: { label: 'Awaiting Email', className: 'bg-orange-100 text-orange-700' }, pending_email: { label: 'Awaiting Email', className: 'bg-orange-100 text-orange-700' },
@@ -385,7 +360,6 @@ const AdminValidations = () => {
<TableCell> <TableCell>
<div className="flex gap-2"> <div className="flex gap-2">
{user.status === 'pending_email' ? ( {user.status === 'pending_email' ? (
<>
<Button <Button
onClick={() => handleBypassAndValidateRequest(user)} onClick={() => handleBypassAndValidateRequest(user)}
disabled={actionLoading === user.id} disabled={actionLoading === user.id}
@@ -394,19 +368,7 @@ const AdminValidations = () => {
> >
{actionLoading === user.id ? 'Validating...' : 'Bypass & Validate'} {actionLoading === user.id ? 'Validating...' : 'Bypass & Validate'}
</Button> </Button>
<Button
onClick={() => handleRejectUser(user)}
disabled={actionLoading === user.id}
size="sm"
variant="outline"
className="border-2 border-red-500 text-red-500 hover:bg-red-50"
>
<X className="h-4 w-4 mr-1" />
Reject
</Button>
</>
) : user.status === 'payment_pending' ? ( ) : user.status === 'payment_pending' ? (
<>
<Button <Button
onClick={() => handleActivatePayment(user)} onClick={() => handleActivatePayment(user)}
size="sm" size="sm"
@@ -415,19 +377,7 @@ const AdminValidations = () => {
<CheckCircle className="h-4 w-4 mr-1" /> <CheckCircle className="h-4 w-4 mr-1" />
Activate Payment Activate Payment
</Button> </Button>
<Button
onClick={() => handleRejectUser(user)}
disabled={actionLoading === user.id}
size="sm"
variant="outline"
className="border-2 border-red-500 text-red-500 hover:bg-red-50"
>
<X className="h-4 w-4 mr-1" />
Reject
</Button>
</>
) : ( ) : (
<>
<Button <Button
onClick={() => handleValidateRequest(user)} onClick={() => handleValidateRequest(user)}
disabled={actionLoading === user.id} disabled={actionLoading === user.id}
@@ -436,17 +386,6 @@ const AdminValidations = () => {
> >
{actionLoading === user.id ? 'Validating...' : 'Validate'} {actionLoading === user.id ? 'Validating...' : 'Validate'}
</Button> </Button>
<Button
onClick={() => handleRejectUser(user)}
disabled={actionLoading === user.id}
size="sm"
variant="outline"
className="border-2 border-red-500 text-red-500 hover:bg-red-50"
>
<X className="h-4 w-4 mr-1" />
Reject
</Button>
</>
)} )}
</div> </div>
</TableCell> </TableCell>
@@ -563,15 +502,6 @@ const AdminValidations = () => {
loading={actionLoading !== null} loading={actionLoading !== null}
{...getActionMessage()} {...getActionMessage()}
/> />
{/* Rejection Dialog */}
<RejectionDialog
open={rejectionDialogOpen}
onOpenChange={setRejectionDialogOpen}
onConfirm={confirmRejection}
user={userToReject}
loading={actionLoading !== null}
/>
</> </>
); );
}; };