Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9bdd1d0a6 | ||
|
|
f7fef8572a | ||
|
|
23163a7a2b | ||
|
|
4b0517b92c | ||
|
|
bebbba1ece | ||
|
|
5a46375212 | ||
|
|
d683ec6b5b | ||
|
|
03eb349f0e | ||
|
|
b842130b62 | ||
|
|
eee26cf108 | ||
|
|
ac850d65d3 | ||
|
|
40a8930b93 | ||
|
|
4d80f9aca5 |
918
README.md
918
README.md
@@ -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
|
|
||||||
|
|||||||
BIN
public/shooting_star_2.png
Normal file
BIN
public/shooting_star_2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -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>
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -8,37 +8,39 @@ const PublicFooter = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Main Footer */}
|
{/* Main Footer */}
|
||||||
<footer className="bg-[#644c9f] px-4 sm:px-8 md:px-16 py-12 md:py-20 flex items-center justify-center min-h-[420px]">
|
<footer className="bg-[#644c9f] border-t border-[rgba(0,0,0,0.1)] py-8 px-2 lg:px-10 flex items-center justify-between">
|
||||||
<div className="border-t border-[rgba(0,0,0,0.1)] py-8 md:py-12 lg:py-20 flex flex-col lg:flex-row gap-8 sm:gap-12 md:gap-16 lg:gap-20 xl:gap-30 items-center justify-center w-full max-w-7xl">
|
<div className=" flex flex-col md:flex-row gap-14 md:gap-2 lg:gap-32 xl:gap-40 items-center justify-center text-left md:justify-between w-full max-w-7xl mx-auto">
|
||||||
<div className="w-32 sm:w-40 md:w-48 lg:w-[232px] flex-shrink-0">
|
<div className="w-40 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" />
|
<img src={loafLogo} alt="LOAF Logo" className="w-full h-auto aspect-square object-contain" />
|
||||||
</div>
|
</div>
|
||||||
<nav className="flex flex-col sm:flex-row gap-8 sm:gap-12 md:gap-16 lg:gap-20 xl:gap-28 items-start justify-center w-full lg:w-auto">
|
<nav className="flex flex-col sm:flex-row sm:flex-nowrap gap-8 sm:gap-4 lg:gap-20 xl:gap-28 items-start justify-center w-full lg:w-auto">
|
||||||
<div className="flex flex-col gap-2 w-full sm:w-auto sm:min-w-[163px]">
|
|
||||||
<div className="pb-4">
|
<div className="md:flex hidden flex-col gap-2 items-start text-left w-full sm:w-auto sm:min-w-[163px]">
|
||||||
<p className="text-white text-base font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>About</p>
|
<div className="pb-2 lg:pb-4">
|
||||||
|
<p className="text-white text-xl font-medium" style={{ fontFamily: "'Poppins', sans-serif" }}>About</p>
|
||||||
</div>
|
</div>
|
||||||
<Link to="/about/history" className="text-[#ddd8eb] text-sm sm:text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Inter', sans-serif" }}>History</Link>
|
<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: "'Inter', sans-serif" }}>Mission and Values</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: "'Inter', sans-serif" }}>Board of Directors</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>
|
||||||
<div className="flex flex-col gap-2 w-full sm:w-auto sm:min-w-[148px]">
|
<div className="hidden md:flex flex-col gap-2 items-start text-left w-full sm:w-auto sm:min-w-[148px]">
|
||||||
<div className="pb-4">
|
<div className="pb-2 lg:pb-4">
|
||||||
<p className="text-white text-base font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>Connect</p>
|
<p className="text-white text-xl font-medium" style={{ fontFamily: "'Poppins', sans-serif" }}>Connect</p>
|
||||||
</div>
|
</div>
|
||||||
<Link to="/become-a-member" className="text-[#ddd8eb] text-sm sm:text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Inter', sans-serif" }}>Become a Member</Link>
|
<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: "'Inter', sans-serif" }}>Contact Us</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: "'Inter', sans-serif" }}>Resources</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>
|
||||||
<div className="flex flex-col gap-2 items-center sm:items-start w-full sm:w-auto sm:min-w-[220px] md:min-w-[271px]">
|
|
||||||
<div className="pb-4 w-full">
|
<div className="flex flex-col gap-2 items-center justify-center md:items-start text-left w-full sm:w-auto sm:min-w-[200px] md:min-w-[200px] lg:min-w-[220px]">
|
||||||
|
<div className="pb-4 w-full flex justify-center lg:justify-start">
|
||||||
<Link to="/donate" className="block">
|
<Link to="/donate" className="block">
|
||||||
<Button className="bg-[#ff9e77] hover:bg-[#ff8c64] text-[#48286e] rounded-full px-6 py-3 text-base sm:text-lg font-medium w-full sm:w-[217px]">
|
<Button className="bg-[#ff9e77] hover:bg-[#ff8c64] text-[#48286e] rounded-full px-12 lg:px-16 py-6 text-lg sm:text-lg font-medium ">
|
||||||
Donate
|
Donate
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[#ddd8eb] text-sm sm:text-base font-medium text-center sm:text-left w-full" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<p className="text-[#ddd8eb] text-sm sm:text-base font-medium text-center md:text-left w-full" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
LOAF is supported by<br />the Hollyfield Foundation
|
LOAF is supported by<br />the Hollyfield Foundation
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,22 +49,22 @@ const PublicFooter = () => {
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
{/* Bottom 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-8 md:px-16 py-6 md:py-8">
|
<footer className="bg-gradient-to-r from-[#48286e] to-[#644c9f] border-t border-[rgba(0,0,0,0.1)] px-4 sm:px-8 md:px-20 py-5">
|
||||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6 justify-between items-center max-w-7xl mx-auto">
|
<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">
|
<nav className="flex flex-col sm:flex-row gap-4 sm:gap-8 items-center order-1 sm:order-none">
|
||||||
<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" }}>
|
<Link to="/terms-of-service" 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
|
Terms of Service
|
||||||
</Link>
|
</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" }}>
|
<Link to="/privacy-policy" className="text-[#c5b4e3] text-sm sm:text-base font-medium hover:text-white transition-colors whitespace-nowrap" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
Privacy Policy
|
Privacy Policy
|
||||||
</Link>
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
<p className="text-[#c5b4e3] text-sm sm:text-base font-medium text-center order-2 sm:order-none" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<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.
|
© 2025 LOAF. All Rights Reserved.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[#c5b4e3] text-sm sm:text-base font-medium text-center order-3 sm:order-none" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<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{' '}
|
Designed and Managed by{' '}
|
||||||
<a href="https://konceptkit.com/" className="text-[#d1c3e9] underline hover:text-white transition-colors whitespace-nowrap">
|
<a href="https://konceptkit.com/" className=" text-white transition-colors whitespace-nowrap">
|
||||||
Koncept Kit
|
Koncept Kit
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -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,45 @@ 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">
|
<div className='sticky top-0 inset-x-0 z-40'>
|
||||||
|
|
||||||
|
<header className="bg-gradient-to-r flex-wrap 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 +88,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 +105,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=" 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>
|
||||||
@@ -69,22 +116,25 @@ const PublicNavbar = () => {
|
|||||||
className="lg:hidden p-2 text-white hover:bg-[#48286e] rounded-md transition-colors"
|
className="lg:hidden p-2 text-white hover:bg-[#48286e] rounded-md transition-colors"
|
||||||
aria-label="Open menu"
|
aria-label="Open menu"
|
||||||
>
|
>
|
||||||
<Menu className="h-6 w-6" />
|
<Menu className="size-14" />
|
||||||
</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 +142,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,37 +162,38 @@ 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>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
</div>
|
||||||
{/* Mobile Menu Drawer */}
|
{/* Mobile Menu Drawer */}
|
||||||
{isMobileMenuOpen && (
|
{isMobileMenuOpen && (
|
||||||
<div className="fixed inset-0 z-50 lg:hidden">
|
<div className="fixed inset-0 z-50 lg:hidden">
|
||||||
@@ -156,7 +207,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 +224,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 +267,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 +277,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 +287,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 +296,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 +310,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 +319,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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,35 @@ import PublicFooter from '../components/PublicFooter';
|
|||||||
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';
|
||||||
|
import { LuArrowDown } from "react-icons/lu";
|
||||||
|
|
||||||
|
const CardSection = ({ children, className = '', arrow = true }) => (
|
||||||
|
<section className={` ${className}`}>
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<Card className="p-14 bg-white rounded-3xl">
|
||||||
|
{children}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
{arrow && (<div className="flex text-2xl my-5 justify-center">
|
||||||
|
<LuArrowDown />
|
||||||
|
</div>)}
|
||||||
|
{!arrow && (
|
||||||
|
<div className="mb-12"></div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Title = ({ children }) => (
|
||||||
|
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
|
{children}
|
||||||
|
</h2>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Paragragh = ({ children }) => (
|
||||||
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
|
||||||
const History = () => {
|
const History = () => {
|
||||||
const ardenCharlotteImg = `${process.env.PUBLIC_URL}/history-arden-charlotte.png`;
|
const ardenCharlotteImg = `${process.env.PUBLIC_URL}/history-arden-charlotte.png`;
|
||||||
@@ -12,20 +41,21 @@ const History = () => {
|
|||||||
const part3Img = `${process.env.PUBLIC_URL}/history-part3.png`;
|
const part3Img = `${process.env.PUBLIC_URL}/history-part3.png`;
|
||||||
const part7Img = `${process.env.PUBLIC_URL}/history-part7.png`;
|
const part7Img = `${process.env.PUBLIC_URL}/history-part7.png`;
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-white">
|
||||||
<PublicNavbar />
|
<PublicNavbar />
|
||||||
|
|
||||||
<main className="bg-gradient-to-b from-white to-[#f1eef9] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-8 sm:py-10 md:py-12">
|
<main className="bg-gradient-to-br from-[#F9FAFB] to-[#DCD7EA] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-8 sm:py-10 md:py-12">
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section className="py-12">
|
<section className="py-12">
|
||||||
<div className="max-w-5xl mx-auto text-center">
|
<div className="max-w-3xl mx-auto flex justify-around mb-12 flex-col gap-6 items-center lg:flex-row">
|
||||||
<h1 className="text-3xl sm:text-4xl md:text-5xl font-bold text-[#48286e] mb-4"
|
<h1 className="text-3xl sm:text-4xl md:text-5xl font-extrabold text-[#48286e] "
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
History of LOAF
|
History of LOAF
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex items-center justify-center gap-2 text-[#48286e]">
|
<div className="flex items-center justify-center gap-6 text-[#48286e]">
|
||||||
<Pen className="h-5 w-5" />
|
<Pen className="size-7" />
|
||||||
<p className="text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
By Arden Eversmeyer
|
By Arden Eversmeyer
|
||||||
</p>
|
</p>
|
||||||
@@ -34,10 +64,8 @@ const History = () => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Part 1 - With Image */}
|
{/* Part 1 - With Image */}
|
||||||
<section className="py-12">
|
<CardSection>
|
||||||
<div className="max-w-5xl mx-auto">
|
<div className="flex flex-col md:flex-row gap-14 items-center">
|
||||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
|
||||||
<div className="flex flex-col md:flex-row gap-8 items-start">
|
|
||||||
<div className="md:w-1/3 ">
|
<div className="md:w-1/3 ">
|
||||||
<img src={ardenCharlotteImg} alt="Arden Eversmeyer and Charlotte Avery"
|
<img src={ardenCharlotteImg} alt="Arden Eversmeyer and Charlotte Avery"
|
||||||
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
||||||
@@ -46,190 +74,180 @@ const History = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:w-2/3">
|
<div className="md:w-2/3">
|
||||||
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<Title>Part 1</Title>
|
||||||
Part 1
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
</h2>
|
In 1985 my life partner of 33 years died. For many years we had been part of a large “friendship group”, many of whom had been together longer than we had. But I was the first to lose a partner. After a few months I began to feel the need to explore community. Already retired, the necessity of being closeted was gone. I soon discovered there was no group for mid-life an old lesbians in Houston, and began the search for such groups around the U.S.
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
In 1985 my life partner of 33 years died. For many years we had been part of a large "friendship group" that got together for meals and games. After her death, I found myself on the edge of the group. I felt invisible. The group, composed primarily of couples, didn't know what to do with the single person they had suddenly become.
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-md mb-4 text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
When I moved to Houston in 1992, I again found myself isolated. I had friends, but not being "coupled" in a "couples world" left me on the outside. I was aware of my advancing age – I was 63 at the time - and I was sure that I was the only "old lesbian" in Houston. I checked out the Montrose bars, but to my dismay, found that older lesbians were non-existent; at least they didn't hang out in bars.
|
In April of 1987 I loaded my camper and headed for California. I started in San Diego, and worked my way up the coast to San Francisco finding and visiting senior LGBT groups. I came home with much information, and some suggestions about organizing. One fact that was consistent with all the groups was that if the group was for both men and women - the women dropped out. The recommendation was to start a group for women only.
|
||||||
</p>
|
</p>
|
||||||
|
<p className="text-md text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>I had become friends with some young lesbians here in Houston who knew the Gay community. We started meeting and brainstorming, and the group now known as LOAF was born.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</CardSection>
|
||||||
</div>
|
{/* Arrow */}
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Part 2 */}
|
{/* Part 2 */}
|
||||||
<section className="py-12">
|
<CardSection >
|
||||||
<div className="max-w-5xl mx-auto">
|
<Title>Part 2</Title>
|
||||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
Part 2
|
|
||||||
</h2>
|
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
The Founding Mothers of LOAF are Ruth Sathre (nurse), JoAnn Beene (psychologist), Delores Nason (business woman), JoAnn Loulan (psychologist and writer, now living in Guerneville, CA), and Judy Peyton (social worker). We decided to form a group for Lesbians Over Fifty and began the search for others "like us."
|
The Founding Mothers of LOAF are Ruth Sathre (nurse), JoAnn Beene (psychologist), Delores Nason (business woman), JoAnn Loulan (psychologist and writer, now living in Guerneville, CA), and Judy Peyton (social worker). We decided to form a group for Lesbians Over Fifty and began the search for others "like us."
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
In 1993, we advertised in OutSmart Magazine, the Houston gay magazine, and invited interested women to join us at a local restaurant. Founding mothers and 19 other women came to that first meeting. Since then the group has gone through many evolutions.
|
In 1993, we advertised in OutSmart Magazine, the Houston gay magazine, and invited interested women to join us at a local restaurant. Founding mothers and 19 other women came to that first meeting. Since then the group has gone through many evolutions.
|
||||||
</p>
|
</p>
|
||||||
<ul className="list-disc ml-6 mt-4 space-y-2 text-lg text-[#48286e]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<ul className="list-disc ml-6 mt-4 space-y-2 text-md text-[#48286e]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
<li><strong>AGE OF PARTICIPANTS</strong> - we started off as LOAFF - Lesbians over Age Fifty-Five. The extra F stood for 55, which didn't work very well, so we changed to LOAF and lowered the age to 50.</li>
|
<li>AGE OF PARTICIPANTS - We launched as LOAFF (Lesbians Over Age Fifty-Five) and quickly lowered the entry age to fifty so more women could join.</li>
|
||||||
<li><strong>NAME FOR THE GROUP</strong> - LOAFF and then LOAF</li>
|
<li>NAME FOR THE GROUP - The acronym began as LOAFF, then we streamlined it to the now-familiar LOAF.</li>
|
||||||
<li><strong>NUMBER OF EVENTS</strong> - Some of the early years we had events every Saturday afternoon, but as we aged, we cut back to one event each month, then we went to the current format of one event during the week, either afternoon or evening, and a weekend activity.</li>
|
<li>AMOUNT OF STRUCTURING - Too many rules can smother a grassroots group, so we kept things loose and let participating women guide direction as needs evolved.</li>
|
||||||
<li><strong>TYPES OF EVENTS</strong> - We've had LOTS of different events. Some of the events we have had include: going to a museum, going to the symphony, seeing a play or movie together, going out to dinner, pot luck dinners, game nights, campfires, hiking, kayaking, and more.</li>
|
<li>SAFE HAVEN FOR MEETINGS - Gatherings had to be in discreet, transit-accessible locations, scheduled during daylight (often Sundays) so closeted or non-driving members could attend comfortably.</li>
|
||||||
|
<li>NEWSLETTER - A monthly mailing went out before each month's end, highlighting news plus upcoming activities tailored to the community.</li>
|
||||||
|
<li>DUES - Contributions were set at $2 per month per woman, with a standing policy that anyone unable to pay was still welcome—unchanged since inception.</li>
|
||||||
|
<li>CREATIVE PUBLICITY - We produced flyers and placed them with LGBTQ+ organizations, counselors, and other allies, recognizing the women we hoped to reach wouldn't necessarily be found in bars and would arrive mostly via word of mouth.</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</Card>
|
|
||||||
</div>
|
</CardSection>
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Part 3 - With Image */}
|
{/* Part 3 - With Image */}
|
||||||
<section className="py-12">
|
<CardSection >
|
||||||
<div className="max-w-5xl mx-auto">
|
<div className="flex gap-14 flex-col min-w-2xl md:flex-row justify-center items-center">
|
||||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
<div className="">
|
||||||
<div className="flex flex-col md:flex-row gap-8 items-start">
|
|
||||||
<div className="md:w-2/3">
|
|
||||||
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
Part 3
|
|
||||||
</h2>
|
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
We have never had a formal organization with by-laws and officers. We have operated on a consensus basis with the founders making most of the decisions. One of the early decisions we made was that we would not have any kind of formal membership. We wanted to be as inclusive as possible and not create any barriers to participation.
|
|
||||||
</p>
|
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
We have always been self-supporting. We have never charged dues or asked for donations. Each person pays for their own meal or activity. We have never had a budget or a bank account. We have been able to operate this way because we have always kept our activities simple and inexpensive.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="md:w-1/3">
|
|
||||||
<img src={part3Img} alt="LOAF Community"
|
<img src={part3Img} alt="LOAF Community"
|
||||||
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
||||||
<p className="text-sm text-[#48286e] mt-2 text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
LOAF Community
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Part 4 - With Image */}
|
</div>
|
||||||
<section className="py-12">
|
<div className="">
|
||||||
<div className="max-w-5xl mx-auto">
|
<Title>Part 3</Title>
|
||||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
<div className="flex flex-col md:flex-row gap-8 items-start">
|
The first meeting of LOAF was the third Sunday of October 1987 at Womynspace. There were six women besides myself at that first meeting. attending were Betty Rudnick, Billie Carter, Josephine Jones, Sylvia Porter, Marjorie Fulp, and Charlotte Avery. Of those six women, only Sylvia Porter and Charlotte Avery are still alive.
|
||||||
<div className="md:w-2/3">
|
|
||||||
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
Part 4
|
|
||||||
</h2>
|
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
Over the years, LOAF has been a place where women can be themselves, where they can talk about their lives and their experiences without fear of judgment. We have created a safe space for women to explore their sexuality and their identity as lesbians.
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
Many women have told us that LOAF has been a lifeline for them, especially as they age and find themselves increasingly isolated. LOAF has provided a community and a sense of belonging that has been invaluable.
|
Much that we discussed that day is still the heart of the group today - decisions by consensus at monthly meetings and activities governed by needs and wishes of participating women. It was soon decided to make the age requirement for membership age 50, and we became LOAF.
|
||||||
|
</p>
|
||||||
|
<p className="text-md text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
In 1989 member Jo Stewart, social worker at Methodist Hospital, started urging LOAF to incorporate as a non-profit. The work began in 1990 with Moore & Hunt (Debbie Hunt) as our Corporate Attorneys. Jo died of cancer in 1990. The work for application of our 501(c)3 was done by Floi Ewing, Arden's sister, and our non-profit status was granted in January 1991. Loaf incorporated as a social networking and support group without a membership roll to protect the anonymity of the women in LOAF.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:w-1/3">
|
</div>
|
||||||
|
|
||||||
|
</CardSection>
|
||||||
|
|
||||||
|
{/* Part 4 */}
|
||||||
|
<CardSection>
|
||||||
|
<div className=" ">
|
||||||
|
<Title>Part 4</Title>
|
||||||
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Third Sunday meeting places have changed over the years. We moved from Womynspace to Autrey House near Rice University. We were there from November 1987 until May 1990 when the new Bishop dis-invited all GLBT groups because of homophobia. We spent a couple months at Montrose Counseling Center (on Lovett), and then moved to the Metropolitan Multi- Service Center on W. Gray. We met there from August 1990 until January 1993. We left because the city started closing the centers on Sunday, and we were not willing to change our meeting day. From February through June we met at Inklings Book Store . In July we started our long occupancy with Houston Mission Church, and met there until the church dissolved in April 2001. We then met at the Hollyfield Center for seven months. From there we went to the GLBT Community Center on Hawthorne where we stayed until July 2003. Attendance was dropping off, and some of the women were not comfortable in a gay identified place. So Third Sunday Meetings moved to Charlotte and Arden's home - and we met there from August 2003 until April 2011. Membership had grown until the meetings had reached critical mass and parking was a problem. So a team of board members started researching for a new home. And on the third Sunday of May 2011 LOAF started meeting at the Montrose Counseling Center. A new era had started.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardSection>
|
||||||
|
|
||||||
|
{/* Part 5 - With Image */}
|
||||||
|
<CardSection >
|
||||||
|
<div className="flex gap-8 flex-col lg:flex-row justify-center items-center md:items-start">
|
||||||
|
<div className="flex flex-col gap-8 w-full lg:w-1/2">
|
||||||
<img src={pride1Img} alt="Pride Parade"
|
<img src={pride1Img} alt="Pride Parade"
|
||||||
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
||||||
<img src={pride2Img} alt="Pride Parade"
|
<img src={pride2Img} alt="Pride Parade"
|
||||||
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
||||||
<p className="text-sm text-[#48286e] mt-2 text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
LOAF at Pride
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Part 5 */}
|
<div className="w-full lg:w-1/2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
<section className="py-12">
|
<Title>Part 5</Title>
|
||||||
<div className="max-w-5xl mx-auto">
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" >
|
||||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
The activities of the monthly meetings were decided by the participating women. Rules were very limited, and decisions were made monthly by the women attending. We soon decided to set up a quarterly meeting schedule of pot-luck, speaker, and games. We followed this schedule for at least ten years. New ideas/programs were added as time allowed. Our first speaker was Pokey Anderson, an icon in the GLBT community. She provided much information about our history in Houston. We also decided to ask Deb Hunt to talk to us about documents, and she has done this periodically over the years. She spoke to us well before we decided to incorporate, and she then became our Corporate Attorney.
|
||||||
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
Part 5
|
|
||||||
</h2>
|
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
LOAF has also been a place where women can give back to the community. Many of our members have been active in various LGBTQ+ organizations and causes. We have marched in Pride parades, volunteered at LGBTQ+ events, and supported various LGBTQ+ initiatives.
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
As we look to the future, we are committed to continuing to provide a welcoming and inclusive space for lesbians over 50. We know that there are many women out there who are looking for a community like ours, and we want to make sure that they know that LOAF is here for them.
|
In November 1988 we instituted our annual Benevolent Project. We collected items to contribute to Stone Soup Kitchen - a GLBT food pantry. We have done this every year since, and have contributed to groups such as Omega House, The Rose, Battered Women, local lesbian organizations, and some individuals.
|
||||||
|
</p>
|
||||||
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
In June 1989 we entered our first Pride Parade. That year we had a convertible and a walking group. Several of the LOAF women participate with other groups in the parade, but we have participated every year since 1989. In 2010 we entered our first float and won a trophy for best representation of the theme.
|
||||||
|
</p>
|
||||||
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
All of these decisions were made by the women at the Third Sunday meetings. There have never been rules instigated by the Board of Directors. Because many women don't want to attend meetings, we changed Third Sunday Meeting to Meet 'N Greet several years ago. And that is what we do - take care of any necessary business. But greet newcomers and socialize with our friends.
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
|
</CardSection>
|
||||||
|
|
||||||
|
|
||||||
{/* Part 6 */}
|
{/* Part 6 */}
|
||||||
<section className="py-12">
|
<CardSection >
|
||||||
<div className="max-w-5xl mx-auto">
|
|
||||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
Part 6
|
Part 6
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
One of the things that has made LOAF special is the diversity of our members. We have women from all walks of life, all backgrounds, all races, all religions, and all political persuasions. What we have in common is our age and our sexual orientation.
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Socials have always been a big part of the activities for LOAF, and having a social as well as the monthly “Third Sunday” meeting was always on the calendar. Pot lucks topped the list for many years, and they were hosted by women in their homes. That is more difficult now because of the size of the membership. Bev and Sandy have hosted a potluck since 1995, and Eva Geer for several years.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
We have learned so much from each other over the years. We have shared our stories, our wisdom, and our experiences. We have laughed together, cried together, and supported each other through good times and bad.
|
<p className="text-md text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
The ice cream socials started in 1989. There are still women who have never cranked or eaten home made ice cream.The “picnic in the park” started in 2000. We have held picnics in a couple State Parks as well as Tom Bass Park in recent years.In 1988 we started attending the TUTS Broadway Musical at Miller Theater in July. We bring a snack supper and a chair and sit on the hill.In 2000 we started eating at Sudie's Catfish House in January. A breather from a busy party season, but a good way to connect.From 1987 to 1994 we had “Second Tuesday Dancing”. First at The Ranch, and then at Ms B's, it was our way to celebrate birthdays of the month. It was well attended.
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
|
||||||
</div>
|
<p className="text-md text-[#48286e] leading-relaxed mt-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
</section>
|
From 1989 to 1993 we had a five day Thanksgiving camp-out at a State Park. We roasted turkey and women came for potluck on Thanksgiving Day.We had from 10 to 20 campers, and maybe 25 for dinner.We have had a Christmas party every year since 1987.We have had several Port of Houston tours on the Sam Houston; several Houston Zoo tours; museum tours; and out-of town tours for bluebonnets, miniature horses, and Blue Bell ice cream. And occasionally, for lack of inspiration for an event social, we simply met for lunch at a local restaurant.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="text-md text-[#48286e] leading-relaxed mt-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
LOAF has always been a social group. We have never had support groups or counseling. We are a place to meet other lesbians over fifty, make friends, and have fun.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</CardSection>
|
||||||
|
|
||||||
{/* Part 7 - With Image */}
|
{/* Part 7 - With Image */}
|
||||||
<section className="py-12">
|
<CardSection>
|
||||||
<div className="max-w-5xl mx-auto">
|
|
||||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
<div className="flex flex-col md:flex-row gap-8 items-center">
|
||||||
<div className="flex flex-col md:flex-row gap-8 items-start">
|
|
||||||
<div className="md:w-1/3">
|
<div className="md:w-1/3">
|
||||||
<img src={part7Img} alt="LOAF Members"
|
<img src={part7Img} alt="LOAF Members"
|
||||||
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
||||||
<p className="text-sm text-[#48286e] mt-2 text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
LOAF Members
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="md:w-2/3">
|
<div className="md:w-2/3">
|
||||||
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
Part 7
|
Part 7
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
LOAF has evolved over the years, but our core mission has remained the same: to provide a welcoming and inclusive community for lesbians over 50. We have adapted to the changing times and the changing needs of our members, but we have never lost sight of what makes LOAF special.
|
The LOAF Library has been an important part of the offering to the women. It started about 1987 when Arden discovered there were books - both fiction and non-fiction - about lesbians. We had one bookstore then -” Wilde 'N Stein” - that had a limited selection of lesbian books. Then Arden discovered Womencraft Books, a mail order book company. This began the collection now in the library. Over the years women have donated books. At one time we took duplicate titles to our book stores (Inkilngs and Book Woman) and traded them for titles we didn't have on the shelf. When the last book store closed we started donating duplicate copies to HATCH, and they are building their library. We have a collection that includes feminist, fantasy/sci-fi, poetry, non-fiction, as well as fiction books. We have a collection of out-of-print periodicals, women's music, and a video library. We have some beautiful “coffee table” books. We have copies of many of the “pulp” books.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-md text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
We are proud of what we have accomplished over the years, and we are excited about the future. We know that there will always be a need for a community like LOAF, and we are committed to being here for as long as we are needed.
|
LOAF also was gifted with a beautiful pianola player piano and about 150 piano rolls. Marie Mariano donated it several years ago. It was in the “Allison” flood, and when it was restored Arden was told it was a beautiful instrument, and quite valuable.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
|
||||||
</div>
|
</CardSection>
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Part 8 */}
|
{/* Part 8 */}
|
||||||
<section className="py-12">
|
<CardSection arrow={false} >
|
||||||
<div className="max-w-5xl mx-auto">
|
|
||||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
Part 8
|
Part 8
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
As I reflect on the history of LOAF, I am filled with gratitude for all of the women who have been part of this community over the years. Each one of you has made LOAF what it is today, and I am so proud of what we have created together.
|
LOAF has become a unique organization in that it is the oldest lesbian organization in Houston, and the only one of its kind in Texas. Over the years there has been quite a bit of exposure and promotion for LOAF.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
LOAF has been a place where we can be ourselves, where we can celebrate who we are, and where we can support each other through all of life's challenges. It has been a place of joy, laughter, friendship, and love.
|
For 17 consecutive years, from 1987 to 2004, we had a Texas Lesbian Conference that rotated between Houston, San Antonio, Austin, and Dallas. LOAF presented workshops at five of these conferences. LOAF did a workshop at the National Lesbian Conference in Atlanta in 1991. We did a workshop at at the PFLAG “Healing the Hurt” conference in 1994. We did a program at the Silver Threads conference in St Petersburg, FL. We have done programs at three OLOC conferences. Charlotte and Arden participated in a live TV show about senior GLBT persons in Dallas. We participated in a documentary on GLBT seniors produced in Canada. And another documentary for Golden Threads at Cape Cod. We participated on a panel for the Women’s Studies Department at the University of Houston for their “Living archive” series. We have done several programs for the Women’s Group in Houston, and appeared on the After Hours radio show on KPFT several times.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-md text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
Thank you for being part of LOAF. Thank you for making this community what it is. And thank you for continuing to support LOAF into the future.
|
All of these appearances are documented and part of the LOAF Archives located at the University of Houston Special Collections Library. One result of these workshops and programs has been the formation of similar groups for mid-life and old lesbians throughout the country. But most important is the connection with other lesbians of our generation and avoiding isolation..
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
|
||||||
</div>
|
</CardSection>
|
||||||
</section>
|
|
||||||
|
</main>
|
||||||
|
|
||||||
{/* CTA Section */}
|
{/* CTA Section */}
|
||||||
<section className="py-12 bg-[#48286e] rounded-2xl">
|
<section className="py-20 bg-[#48286e] mx-0">
|
||||||
<div className="max-w-5xl mx-auto px-8">
|
<div className="max-w-7xl mx-auto px-8">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
<div className="flex gap-8 md:flex-row flex-col">
|
||||||
<Card className="p-8 text-center bg-white rounded-2xl shadow-lg hover:shadow-xl transition-shadow">
|
<Card className="p-8 text-center bg-white rounded-2xl shadow-lg hover:shadow-xl transition-shadow">
|
||||||
<h3 className="text-2xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h3 className="text-2xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
A Life Remembered
|
A Life Remembered
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-[#48286e] mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-[#48286e] mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
@@ -243,7 +261,7 @@ const History = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="p-8 text-center bg-white rounded-2xl shadow-lg hover:shadow-xl transition-shadow">
|
<Card className="p-8 text-center bg-white rounded-2xl shadow-lg hover:shadow-xl transition-shadow">
|
||||||
<h3 className="text-2xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h3 className="text-2xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
The Old Lesbian Oral Herstory Project
|
The Old Lesbian Oral Herstory Project
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-[#48286e] mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-[#48286e] mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
@@ -258,7 +276,6 @@ const History = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
|
||||||
|
|
||||||
<PublicFooter />
|
<PublicFooter />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,25 +8,81 @@ import PublicFooter from '../components/PublicFooter';
|
|||||||
const Landing = () => {
|
const Landing = () => {
|
||||||
// LOAF brand assets (local)
|
// LOAF brand assets (local)
|
||||||
const taglineImage = `${process.env.PUBLIC_URL}/tagline-image.png`;
|
const taglineImage = `${process.env.PUBLIC_URL}/tagline-image.png`;
|
||||||
const shootingStar = `${process.env.PUBLIC_URL}/shooting-star.png`;
|
const shootingStar = `${process.env.PUBLIC_URL}/shooting_star_2.png`;
|
||||||
const iconMeetGreet = `${process.env.PUBLIC_URL}/icon-meet-greet.png`;
|
const iconMeetGreet = `${process.env.PUBLIC_URL}/icon-meet-greet.png`;
|
||||||
const iconSocials = `${process.env.PUBLIC_URL}/icon-socials.png`;
|
const iconSocials = `${process.env.PUBLIC_URL}/icon-socials.png`;
|
||||||
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-none lg:max-w-[363px]">
|
||||||
|
<div className="absolute -top-20 md:-top-40 flex justify-center w-full">
|
||||||
|
<img
|
||||||
|
src={iconSrc}
|
||||||
|
alt={infoTitle}
|
||||||
|
className=" w-40 md:w-64 lg: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="lg:py-20 py-7 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-xs md:max-w-[334px] h-auto object-contain" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 items-center justify-center w-full max-w-[339px]">
|
<div className="flex flex-col gap-4 items-center justify-center w-full max-w-[339px]">
|
||||||
<Link to="/register" className="w-full">
|
<Link to="/register" className="w-full">
|
||||||
<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 transition-colors">
|
<Button style={{ fontFamily: "'Nunito sans', sans-serif" }} 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 transition-colors">
|
||||||
Become a Member
|
Become a Member
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -36,73 +92,41 @@ const Landing = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="py-8 md:py-12 lg:py-16 flex items-center justify-center w-full lg:w-[594px] h-auto">
|
<div className="py-8 md:py-12 lg:py-16 flex items-center justify-center w-full lg:w-[594px] h-auto">
|
||||||
<img src={taglineImage} alt="LOAF Tagline" className="w-full max-w-[483px] h-auto object-contain" />
|
<img src={taglineImage} alt="LOAF Tagline" className="w-full max-w-[330px] md:max-w-[483px] h-auto object-contain" />
|
||||||
</div>
|
</div>
|
||||||
</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-10 lg:pb-44 from-white to-[#f1eef9] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 pt-4 sm:pt-16 md:pt-20 lg:pt-30 flex flex-col">
|
||||||
<div className="flex flex-col items-center pt-12">
|
<div className="flex flex-col items-center pt-4">
|
||||||
<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] px-4 pb-6 md:py-8 text-4xl leading-[60px] md:text-5xl lg:text-6xl font-extrabold text-center" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
Welcome to LOAF
|
Welcome to LOAF
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[rgba(0,0,0,0.55)] text-lg text-center font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<p className="text-[rgba(0,0,0,0.55)] text-lg lg:text-2xl text-center font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
LOAF is Houston's social networking group for lesbians who are 50 years of age and older. LOAF hosts three main activities each month, Meet and Greets, Socials, and ActiveLOAFers. TheaterLOAFers coordinate events throughout the year.
|
LOAF is Houston's social networking group for lesbians who are 50 years of age and older. LOAF hosts three main activities each month, Meet and Greets, Socials, and ActiveLOAFers. TheaterLOAFers coordinate events throughout the year.
|
||||||
</p>
|
</p>
|
||||||
<img src={shootingStar} alt="Decorative element" className="w-full h-[197px] object-contain" />
|
<img src={shootingStar} alt="Decorative element" className="w-full h-[197px] object-contain" />
|
||||||
</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 pb-20 from-[#f1eef9] to-[#ddd8eb] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-4 md:py-20 lg:py-30 flex flex-col sm:w-full lg:flex-row gap-40 md:gap-64 lg: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-reverse md: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 flex items-center justify-center">
|
||||||
<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
|
||||||
|
py-8 text-xl font-normal px-12 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 px-4 font-bold text-center lg:text-left leading-normal 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>
|
||||||
|
|||||||
@@ -10,33 +10,31 @@ const MissionValues = () => {
|
|||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-white">
|
||||||
<PublicNavbar />
|
<PublicNavbar />
|
||||||
|
|
||||||
<main className="bg-gradient-to-b from-[#f9fafb] to-[#ddd8eb] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-8 sm:py-12 md:py-16">
|
<main className="bg-gradient-to-b from-[#f9fafb] to-[#ddd8eb] px-4 sm:px-6 py-8 sm:py-12 md:py-20">
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
<div className="flex md:flex-row flex-col gap-10">
|
||||||
{/* Left Card - Mission (Purple Gradient) */}
|
{/* Left Card - Mission (Purple Gradient) */}
|
||||||
<Card className="bg-gradient-to-br from-[#664fa3] to-[#48286e] p-8 rounded-2xl shadow-lg">
|
<Card className="bg-gradient-to-br from-[#664fa3] to-[#48286e] p-16 rounded-2xl shadow-lg flex flex-col items-center justify-between md:flex-1">
|
||||||
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold text-white text-center mb-6"
|
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold text-white text-center mb-6"
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
LOAF Mission
|
LOAF Mission
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-white text-lg text-center leading-relaxed"
|
<p className="text-white text-xl text-center leading-relaxed"
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
LOAF's mission is to alleviate isolation and enrich the lives of lesbians
|
LOAF’s mission is to alleviate isolation and enrich the lives of lesbians over the age of 50 by providing several social networking events every month in Houston and the surrounding areas.
|
||||||
over the age of 50 by providing several social networking events every month
|
|
||||||
in Houston and the surrounding areas.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="flex justify-center mb-6">
|
<div className="flex justify-center mb-6">
|
||||||
<img src={loafLogo} alt="LOAF Logo" className="w-32 h-32 sm:w-40 sm:h-40 md:w-48 md:h-48 lg:w-64 lg:h-64 object-contain" />
|
<img src={loafLogo} alt="LOAF Logo" className="size-32 sm:size-40 md:size-64 lg:size-96 object-contain" />
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Right Card - Values */}
|
{/* Right Card - Values */}
|
||||||
<Card className="bg-white p-8 rounded-2xl shadow-lg">
|
<Card className="bg-white p-16 rounded-2xl shadow-lg md:flex-1 ">
|
||||||
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold text-[#48286e] text-center mb-6"
|
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold text-[#48286e] text-center mb-6"
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
LOAF Values
|
LOAF Values
|
||||||
</h2>
|
</h2>
|
||||||
<ol className="list-decimal list-inside space-y-3 text-lg text-[#48286e]"
|
<ol className="list-decimal list-inside space-y-8 text-lg text-[#48286e]"
|
||||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
<li>Safe environments for lesbians to gather for a variety of social activities and interaction.</li>
|
<li>Safe environments for lesbians to gather for a variety of social activities and interaction.</li>
|
||||||
<li>Social support for lesbians.</li>
|
<li>Social support for lesbians.</li>
|
||||||
|
|||||||
@@ -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" }}>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user