107 Commits

Author SHA1 Message Date
kayela
7eef62560e feat: staff can edit registration responses 2026-01-29 21:49:25 -06:00
kayela
f70a133e18 feat: tabs layout for edit profile 2026-01-29 20:49:13 -06:00
kayela
d5152609b6 Donation status badge upates, admin validation tootips 2026-01-29 19:37:41 -06:00
kayela
de719d9d69 refactor subscriptionsTable.jsx 2026-01-29 18:46:16 -06:00
kayela
27d5c48805 Componentized subscription table 2026-01-29 18:36:13 -06:00
kayela
64d631d890 Members -removed un used selection options. Profile added back button 2026-01-29 18:12:48 -06:00
kayela
a77fbc47e3 styling improvements 2026-01-28 19:08:13 -06:00
kayela
d638afcdb2 add column for email expiry date
Members > Invite member says invite Staff in dialog
 resend email button
 Update form member form to say member and not staff
 review application function
 manual payment functionality
 basic implementation of theme
 actions dropdown
2026-01-28 18:59:19 -06:00
kayela
a247ac5219 feat: added theme success and warning colors
fix: invite member dialog box
feat: email expiry date column
feat: resend email verification button
fix: select item text can be seen
2026-01-28 15:03:46 -06:00
kayela
01722edad9 updated badge glitch 2026-01-27 17:30:50 -06:00
kayela
378b909398 removed transaction history from Profile.js 2026-01-27 16:38:21 -06:00
kayela
4ad1997bd5 Member tiers implementation intact. Icons updated to be Lucide React. Create/edit member tiers. Display member badge. Transaction history now in My profile dashboard. Adjusted Icons for badges. Added badges on my profile page 2026-01-27 16:30:26 -06:00
kayela
0d7e3a1286 tweaked statcard for better styling when digits are greater than 2 2026-01-27 15:19:19 -06:00
kayela
0c3d4a4edd Updated stat cards to be consistent with rest of codebase 2026-01-27 15:11:25 -06:00
kayela
97aa7860a9 feat: integrate TransactionHistory component into Dashboard and update styles for better UI consistency 2026-01-27 14:33:36 -06:00
Koncept Kit
467f34b42a - - New ThemeConfigContext provider that fetches theme on app load and applies it to the DOM (title, meta description, favicon, CSS variables,
theme-color)/- - Admin Theme settings page under Settings > Theme tab/- All logo references (5 components) now pull from the theme config with fallback to default
2026-01-27 21:32:22 +07:00
Koncept Kit
85070cf77b Update Footer to get current year 2026-01-27 16:44:56 +07:00
9dcb8e3185 Merge pull request 'theme-provider' (#20) from theme-provider into dev
Reviewed-on: andika/membership-fe#20
2026-01-27 08:41:49 +00:00
kayela
a88388ed5d Merge branch 'dev' into theme-provider 2026-01-27 00:53:27 -06:00
kayela
91e264bf7a feat: enhance Dashboard with transaction history and membership info; refactor layout for improved user experience 2026-01-27 00:32:14 -06:00
kayela
333ce62710 feat: add year filtering and search functionality to Bylaws and Financials pages; enhance report grouping by year 2026-01-26 15:18:27 -06:00
kayela
3c0b1396bc feat: enhance dialog components with overflow handling and update placeholder text for consistency 2026-01-26 14:47:04 -06:00
kayela
1ae82fc4e4 fix: badge text styling on hover in settings 2026-01-26 14:06:45 -06:00
kayela
ac8d40112e feat: add AdminMemberTiers page, MemberBadge component, and SettingsLayout; refactor routes and sidebar for improved navigation 2026-01-26 13:58:44 -06:00
kayela
7ee5cb0d9c feat: update AdminMembers and AdminStaff components for improved statistics display and fix typo in MembersDirectory 2026-01-25 12:55:26 -06:00
kayela
4548d959d7 feat: implement UsersContext and refactor user management hooks for improved user data handling 2026-01-25 12:17:30 -06:00
Koncept Kit
002ef5c897 Fixes 2026-01-24 23:56:15 +07:00
kayela
f2dd053320 feat: enhance AdminRoles to manage permissions with loading state and role slug updates 2026-01-22 15:23:50 -06:00
kayela
554b599599 feat: refactor AdminMembers and AdminStaff to utilize useMembers hook for improved member management 2026-01-22 14:47:34 -06:00
kayela
ac879b69b4 feat: Introduce StatusBadge component for consistent status representation
- Added StatusBadge component to standardize the display of user and membership statuses across various admin pages.
- Refactored AdminMembers, AdminPlans, AdminStaff, AdminSubscriptions, AdminUserView, AdminValidations, and MembersDirectory to utilize the new StatusBadge component.
- Removed redundant status badge logic from AdminMembers, AdminStaff, and AdminValidations.
- Updated AdminLayout to include a mobile-friendly sidebar toggle button with Menu icon.
- Created MemberCard component to encapsulate member display logic, improving code reusability.
- Adjusted various components to enhance user experience and maintain consistent styling.
2026-01-22 14:20:02 -06:00
kayela
6c844c0e19 feat: add @tailwindcss/line-clamp dependency and integrate responsive layout adjustments in Admin components for improved UI 2026-01-22 12:07:56 -06:00
7d0c207f1b Merge pull request 'theme-provider' (#18) from theme-provider into dev
Reviewed-on: andika/membership-fe#18
2026-01-21 04:58:57 +00:00
kayela
8ea486a4f4 feat: enhance date formatting in AdminUserView for improved readability and consistency 2026-01-20 22:51:44 -06:00
kayela
264ee860df feat: add member since date handling in CreateMemberDialog and CreateStaffDialog for improved member tracking 2026-01-20 16:16:23 -06:00
kayela
65c3e3b92d fix: update color styles in AdminSidebar, Register, and CSS files for improved UI consistency 2026-01-20 16:02:54 -06:00
kayela
819062d697 fixed spacing in AdminMembers.js 2026-01-20 14:45:05 -06:00
kayela
c73ebfb6c0 feat: implement StatCard component and integrate it into AdminDashboard and AdminMembers for improved stats display 2026-01-20 14:43:17 -06:00
kayela
3822ba8ffb feat: add member since date handling across admin and member views 2026-01-20 12:33:17 -06:00
Koncept Kit
c79db66739 - Details Column - Expandable chevron button for each row- Expandable Transaction Details - Click chevron to show/hide details- Payment Information Section:- Stripe Transaction IDs Section- Copy to Clipboard - One-click copy for all transaction IDs- Update Stripe webhook event permission on Stripe Config page. 2026-01-20 23:52:35 +07:00
Koncept Kit
57cd18ad9d - Add Settings menu for Stripe configuration- In the Member Profile page, Superadmin can assign new Role to the member- Stripe Configuration is now stored with encryption in Database 2026-01-16 19:07:14 +07:00
56dd9eeb77 Merge pull request 'theme-provider' (#16) from theme-provider into dev
Reviewed-on: andika/membership-fe#16
2026-01-16 10:40:04 +00:00
kayela
e831835e6d Merge branch 'dev' into theme-backup 2026-01-14 15:40:45 -06:00
kayela
9287adec01 refactor: update button styles for improved theming and consistency 2026-01-14 13:59:21 -06:00
kayela
0c1202d89a refactor: add scrollbar styles to dialog components for improved usability 2026-01-14 13:43:04 -06:00
kayela
0ebfe71361 refactor: update button styles and text for improved consistency and theming 2026-01-14 13:23:52 -06:00
kayela
a935c0f4dd fix: correct green-forest color value for consistency 2026-01-14 11:08:16 -06:00
kayela
4ccaca192d refactor: update button and badge styles for improved theming and consistency 2026-01-14 11:07:43 -06:00
kayela
4cdccc0323 refactor: update button and badge styles for improved theming and consistency 2026-01-13 23:51:13 -06:00
kayela
21a269998d refactor: restructure styles and components for improved theming and consistency 2026-01-13 22:01:49 -06:00
kayela
e04d39fe17 Refactor color scheme in member-related pages to use brand colors instead of CSS variables for consistency and improved readability 2026-01-13 22:01:33 -06:00
kayela
30d32d8823 Theme refactor 2026-01-13 17:58:04 -06:00
kayela
9c2d516f9d fix typo 2026-01-12 20:37:38 -06:00
kayela
7694532d53 refactor: update styles in MembersDirectory and NewsletterArchive for consistency and improved theming
- Updated color classes to use CSS variables for better maintainability and theming.
- Refactored component styles in MembersDirectory.js to enhance visual consistency.
- Adjusted loading states and empty states in NewsletterArchive.js for improved user experience.
- Added new brand colors to tailwind.config.js for future use.
2026-01-12 20:10:33 -06:00
Koncept Kit
ee0ad176b0 Remove View Public Site on AdminSidebar 2026-01-09 00:23:52 +07:00
kayela
a93e2aa863 Theme provider 2026-01-07 11:01:54 -06:00
Koncept Kit
180eb1ce85 Comment out View Public Site link on the AdminSidebar.js 2026-01-07 15:37:40 +07:00
Koncept Kit
5377a0f465 Security Hardening 2026-01-07 14:03:32 +07:00
Koncept Kit
c54eb23689 Login and Session Fixes 2026-01-07 13:37:20 +07:00
9f7367ceeb Merge pull request 'Merge Kayela works to Dev' (#12) from templates into dev
Reviewed-on: andika/membership-fe#12
2026-01-07 06:18:06 +00:00
kayela
f71931d4a7 Merge branch 'dev' into templates 2026-01-06 12:25:55 -06:00
kayela
97cc5bdedf Add pagination buttons for first and last pages in Members Directory 2026-01-06 12:17:03 -06:00
kayela
8011913c4d Enhance Admin Dashboard and Members Directory with improved layout, pagination, and member details display.
Applied requested changes to UI
2026-01-06 12:05:34 -06:00
Koncept Kit
40a0e3f342 feat(frontend): Comprehensive RBAC implementation across admin pages
**Option 3 Implementation (Latest):**
- InviteStaffDialog: Use /admin/roles/assignable endpoint
- AdminStaff: Enable admin users to see 'Invite Staff' button

**Permission Checks Added (8 admin pages):**
- AdminNewsletters: newsletters.create/edit/delete
- AdminFinancials: financials.create/edit/delete
- AdminBylaws: bylaws.create/edit/delete
- AdminValidations: users.approve, subscriptions.activate
- AdminSubscriptions: subscriptions.export/edit/cancel
- AdminDonations: donations.export
- AdminGallery: gallery.upload/edit/delete
- AdminPlans: subscriptions.plans

**Pattern Established:**
All admin action buttons now wrapped with hasPermission() checks.
UI hides what users can't access, backend enforces rules.

**Files Modified:** 10 files, 100+ permission checks added
2026-01-06 14:45:15 +07:00
kayela
968eaccac2 fixed tabs styling 2026-01-05 14:34:23 -06:00
kayela
11de3d1eed styled chrome scrollbar 2026-01-05 14:16:05 -06:00
kayela
11142ec50e Merge branch 'dev' into templates 2026-01-05 12:51:33 -06:00
Koncept Kit
0249cad261 Improve UX with navigation, attendance management, and calendar fixes
## Quick Wins
- **AdminSidebar**: Move "View Public Site" to clickable logo area
- **Plans**: Fix layout to center single plan, dynamic grid for multiple
- **AdminGallery**: Add empty state message with "Create Event" button

## Event Attendance Enhancement
- **NEW: AdminEventAttendance page** with full-featured table view:
  - Tab filters (All/Yes/No/Maybe RSVPs)
  - Search by name/email
  - Bulk selection with Select All
  - Individual attendance toggle buttons (merged column)
  - CSV export functionality (client requirement)
  - Summary statistics cards
- **AdminEvents**: Navigate to new attendance page instead of dialog
- **App.js**: Add /admin/events/:eventId/attendance route

## Calendar Fixes
- **MemberCalendar**: Add state management for navigation (date/view)
  - Fix non-functional buttons (Today/Back/Next/Month/Week/Day/Agenda)
  - Add onNavigate and onView handlers
- **NEW: MemberCalendar.css**: Extract styles from broken jsx syntax
  - Fix toolbar button styling and interactivity

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 01:02:16 +07:00
Koncept Kit
56711e9136 Revert URL cleanup - backend path is correct
The /membership path in backend URL is correct for development.
Issue is CORS configuration on backend, not URL format.
2026-01-05 15:47:30 +07:00
Koncept Kit
03b76a8e58 Add defensive backend URL validation and auto-cleanup
- Add getApiUrl() function to validate and clean backend URL
- Automatically strip /membership or /api suffix if present
- Log all environment variables on module load for debugging
- Add detailed URL logging in login function
- Provide fallback if REACT_APP_BACKEND_URL is undefined

This fixes the intermittent CORS error caused by incorrect backend URL
2026-01-05 15:42:11 +07:00
Koncept Kit
1acb13ba79 Add comprehensive login diagnostics and retry logic
- Add detailed console logging throughout login flow
- Add 30s timeout to prevent hanging requests
- Defer permission fetching with setTimeout to avoid race conditions
- Add automatic retry for 5xx errors and network failures
- Enhanced error logging with full context for debugging

This addresses intermittent login failures reported by users
2026-01-05 15:00:41 +07:00
Koncept Kit
fa9a1d1d1d Add 404 page and invitation success screen
- Created NotFound component with proper error messaging and navigation
- Added catch-all route (*) in App.js for undefined routes
- Added success state in AcceptInvitation with user info display
- Auto-redirect after 3 seconds with manual continue button option
- Improved UX with animated success indicator
2026-01-05 14:51:39 +07:00
Koncept Kit
48802fe0c6 Fix invitation redirect: admin users now go to /admin instead of /admin/dashboard 2026-01-05 14:15:22 +07:00
Koncept Kit
8c351773ba Fix staff invitation acceptance & add delete/deactivate buttons 2026-01-05 00:11:30 +07:00
Koncept Kit
3511e7a9c8 LOAF Prod 2026-01-04 19:47:49 +07:00
Koncept Kit
33a4d8f4c4 Document Upload Dialogue update 2026-01-02 15:35:30 +07:00
kayela
1d70ac4ec7 update Merge branch 'dev' into templates 2025-12-26 17:39:16 -06:00
Koncept Kit
a6c2475092 Test Preparation 2025-12-26 20:04:08 +07:00
kayela
6d777ed583 Update homepage and links in RegistrationStep4 component 2025-12-24 14:17:30 -06:00
kayela
99d65c917f Merge remote-tracking branch 'origin/dev' into templates 2025-12-24 14:06:12 -06:00
kayela
0f16264656 Remove unused imports from TermsOfService component 2025-12-24 14:04:35 -06:00
Koncept Kit
33fc3a101d Fix runtime error 2025-12-20 18:46:07 +07:00
Koncept Kit
4093c1603e Fix runtime error 2025-12-20 18:40:57 +07:00
035cc896df Merge pull request 'templates' (#5) from templates into dev
Reviewed-on: andika/membership-fe#5
2025-12-20 11:23:07 +00:00
kayela
8ffa97bcd1 Refactor code structure for improved readability and maintainability
Fixed images and styling for Landing page
2025-12-19 15:28:19 -06:00
kayela
b6d25cdab7 Fix max height utility class for responsive layout in Donate component 2025-12-19 13:36:56 -06:00
kayela
f3610282f2 Merge remote-tracking branch 'origin/dev' into templates 2025-12-19 13:29:31 -06:00
kayela
f1dd7fe75b Merge remote-tracking branch 'origin/main' into templates 2025-12-19 13:21:34 -06:00
kayela
37ccfe7767 Update Privacy Policy header to specify "LOAFers, Inc. Website Privacy Policy" and apply Poppins font style 2025-12-19 12:42:29 -06:00
kayela
93cd4c1316 Refactor Terms of Service page layout and content; add comprehensive legal terms sections and improve styling 2025-12-19 12:27:03 -06:00
kayela
a6656b1ff0 Refactor code structure for improved readability and maintainability 2025-12-19 11:59:52 -06:00
kayela
1d4ed96dc9 Refactor BecomeMember, BoardOfDirectors, ContactUs, Donate, MissionValues, and Resources pages for improved layout, styling, and accessibility; update component structure and enhance responsiveness. 2025-12-18 16:34:14 -06:00
kayela
a9bdd1d0a6 Refactor layout and styling for PublicFooter, PublicNavbar, History, Landing, and MissionValues components for improved responsiveness and visual consistency 2025-12-18 12:02:32 -06:00
4848ec3942 Merge pull request 'templates' (#4) from templates into dev
Reviewed-on: andika/membership-fe#4
2025-12-18 10:34:29 +00:00
41d2466cbf Merge pull request '- Profile Picture\' (#3) from main into dev
Reviewed-on: andika/membership-fe#3
2025-12-18 10:29:21 +00:00
Koncept Kit
8c0d9a2a18 - Profile Picture\
Donation Tracking\
Validation Rejection\
Subscription Data Export\
Admin Dashboard Logo\
Admin Navbar Reorganization
2025-12-18 17:04:50 +07:00
kayela
f7fef8572a Landing page fully mobile functional, History page moble and History page fixed. Footer and navbar changes 2025-12-17 17:59:58 -06:00
kayela
23163a7a2b landing page fixes 2025-12-17 13:34:06 -06:00
kayela
4b0517b92c fixed footer 2025-12-17 13:24:38 -06:00
kayela
bebbba1ece fixed all instanced of changed PublicFooter name 2025-12-17 12:36:00 -06:00
kayela
5a46375212 fixed typo error 2025-12-17 12:33:50 -06:00
kayela
d683ec6b5b Copied PublicFooter from Main branch 2025-12-17 12:28:20 -06:00
kayela
03eb349f0e Add PublicFooter-kc component and update imports across pages 2025-12-17 12:11:24 -06:00
kayela
b842130b62 Merge branch 'main' into templates 2025-12-17 12:11:09 -06:00
kayela
eee26cf108 Refactor PublicFooter and PublicNavbar for improved responsiveness and styling; add InfoCard component to Landing page for better content organization 2025-12-16 23:14:20 -06:00
kayela-c
ac850d65d3 added padding when in moble mode 2025-12-13 13:03:57 -06:00
kayela-c
40a8930b93 added side padding to footer 2025-12-13 12:59:06 -06:00
kayela-c
4d80f9aca5 adjusting the padding for the footer and the 3rd column spacing 2025-12-13 12:57:05 -06:00
137 changed files with 17862 additions and 5435 deletions

75
.dockerignore Normal file
View File

@@ -0,0 +1,75 @@
# Git
.git
.gitignore
# Dependencies
node_modules/
# Build output (we build inside Docker)
build/
dist/
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# Testing
coverage/
.nyc_output/
# Environment files (will be passed as build args)
.env
.env.local
.env.development
.env.development.local
.env.test
.env.test.local
.env.production
.env.production.local
*.env
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Docker
Dockerfile
docker-compose*.yml
.docker/
# Documentation
*.md
docs/
# OS files
.DS_Store
Thumbs.db
# Temporary files
tmp/
temp/
*.tmp
# ESLint cache
.eslintcache
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
# Yarn
.yarn-integrity
.pnp.*
# Storybook
storybook-static/
# Design files (if any)
.superdesign/

3
.env.development Normal file
View File

@@ -0,0 +1,3 @@
REACT_APP_BACKEND_URL=http://localhost:8000
REACT_APP_BASENAME=/membership
PUBLIC_URL=/membership

17
.env.example Normal file
View File

@@ -0,0 +1,17 @@
# Server Configuration
PORT=3000
WDS_SOCKET_PORT=443
# Backend API URL
REACT_APP_BACKEND_URL=http://localhost:8000
# App Base Path Configuration
# Examples:
# - For root path: REACT_APP_BASENAME=
# - For subpath: REACT_APP_BASENAME=/membership
# - For production: REACT_APP_BASENAME=/membership
REACT_APP_BASENAME=
# Feature Flags
REACT_APP_ENABLE_VISUAL_EDITS=false
ENABLE_HEALTH_CHECK=false

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
# Environment # Environment
.env .env
.env.production
# dependencies # dependencies
/node_modules /node_modules

49
Dockerfile Normal file
View File

@@ -0,0 +1,49 @@
# Frontend Dockerfile - React with multi-stage build
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package.json yarn.lock ./
# Install dependencies
RUN yarn install --frozen-lockfile
# Copy source code
COPY . .
# Build arguments for environment variables
ARG REACT_APP_BACKEND_URL
ENV REACT_APP_BACKEND_URL=$REACT_APP_BACKEND_URL
# Build the application
RUN yarn build
# Stage 2: Production with Nginx
FROM nginx:alpine AS production
# Copy custom nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy built assets from builder stage
COPY --from=builder /app/build /usr/share/nginx/html
# Create non-root user for security
RUN adduser -D -g '' appuser && \
chown -R appuser:appuser /usr/share/nginx/html && \
chown -R appuser:appuser /var/cache/nginx && \
chown -R appuser:appuser /var/log/nginx && \
touch /var/run/nginx.pid && \
chown -R appuser:appuser /var/run/nginx.pid
# Expose port
EXPOSE 80
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:80/ || exit 1
# Start nginx
CMD ["nginx", "-g", "daemon off;"]

1186
README.md

File diff suppressed because it is too large Load Diff

44
nginx.conf Normal file
View File

@@ -0,0 +1,44 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml application/javascript application/json;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# Handle React Router - serve index.html for all routes
location / {
try_files $uri $uri/ /index.html;
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# Disable access to hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}

View File

@@ -2,7 +2,7 @@
"name": "frontend", "name": "frontend",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"homepage": "/membership", "homepage": "/",
"dependencies": { "dependencies": {
"@fontsource/dm-sans": "^5.2.8", "@fontsource/dm-sans": "^5.2.8",
"@fontsource/fraunces": "^5.2.9", "@fontsource/fraunces": "^5.2.9",
@@ -36,6 +36,7 @@
"@radix-ui/react-tooltip": "^1.2.4", "@radix-ui/react-tooltip": "^1.2.4",
"@stripe/react-stripe-js": "^2.0.0", "@stripe/react-stripe-js": "^2.0.0",
"@stripe/stripe-js": "^2.0.0", "@stripe/stripe-js": "^2.0.0",
"@tailwindcss/line-clamp": "^0.4.4",
"axios": "^1.8.4", "axios": "^1.8.4",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -53,9 +54,11 @@
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-hook-form": "^7.56.2", "react-hook-form": "^7.56.2",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^3.0.1", "react-resizable-panels": "^3.0.1",
"react-router-dom": "^7.5.1", "react-router-dom": "^7.5.1",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"remark-gfm": "^4.0.1",
"sonner": "^2.0.3", "sonner": "^2.0.3",
"tailwind-merge": "^3.2.0", "tailwind-merge": "^3.2.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
@@ -83,6 +86,7 @@
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@craco/craco": "^7.1.0", "@craco/craco": "^7.1.0",
"@eslint/js": "9.23.0", "@eslint/js": "9.23.0",
"@tailwindcss/typography": "^0.5.19",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"eslint": "9.23.0", "eslint": "9.23.0",
"eslint-plugin-import": "2.31.0", "eslint-plugin-import": "2.31.0",

BIN
public/friendships.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

5
public/health.json Normal file
View File

@@ -0,0 +1,5 @@
{
"status": "healthy",
"mode": "production",
"build": "optimized"
}

View File

@@ -25,7 +25,6 @@
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>LOAF - Lesbians Over Age Fifty</title> <title>LOAF - Lesbians Over Age Fifty</title>
<script src="#"></script>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

BIN
public/shooting_star_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

View File

@@ -1,32 +0,0 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Nunito Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', sans-serif;
background-color: #FFFFFF;
color: #422268;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Inter', sans-serif;
font-weight: 600;
}
.inter {
font-family: 'Inter', sans-serif;
}
.nunito-sans {
font-family: 'Nunito Sans', sans-serif;
}
.bg-purple-gradient {
background: linear-gradient(135deg, rgba(100, 76, 159, 0.2) 0%, rgba(72, 40, 110, 0.2) 100%);
}
.bg-soft-mesh {
background: radial-gradient(ellipse at top right, rgba(221, 216, 235, 0.4) 0%, #FFFFFF 50%, #FFFFFF 100%);
}

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { Toaster } from './components/ui/sonner'; import { Toaster } from './components/ui/sonner';
import IdleSessionWarning from './components/IdleSessionWarning';
import Landing from './pages/Landing'; import Landing from './pages/Landing';
import Register from './pages/Register'; import Register from './pages/Register';
import Login from './pages/Login'; import Login from './pages/Login';
@@ -21,12 +22,18 @@ import AdminUserView from './pages/admin/AdminUserView';
import AdminStaff from './pages/admin/AdminStaff'; import AdminStaff from './pages/admin/AdminStaff';
import AdminMembers from './pages/admin/AdminMembers'; import AdminMembers from './pages/admin/AdminMembers';
import AdminPermissions from './pages/admin/AdminPermissions'; import AdminPermissions from './pages/admin/AdminPermissions';
import AdminSettings from './pages/admin/AdminSettings';
import AdminMemberTiers from './pages/admin/AdminMemberTiers';
import AdminRoles from './pages/admin/AdminRoles'; import AdminRoles from './pages/admin/AdminRoles';
import AdminTheme from './pages/admin/AdminTheme';
import AdminEvents from './pages/admin/AdminEvents'; import AdminEvents from './pages/admin/AdminEvents';
import AdminEventAttendance from './pages/admin/AdminEventAttendance';
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 SettingsLayout from './layouts/SettingsLayout';
import { AuthProvider, useAuth } from './context/AuthContext'; import { AuthProvider, useAuth } from './context/AuthContext';
import MemberRoute from './components/MemberRoute'; import MemberRoute from './components/MemberRoute';
import MemberCalendar from './pages/members/MemberCalendar'; import MemberCalendar from './pages/members/MemberCalendar';
@@ -49,6 +56,8 @@ import Resources from './pages/Resources';
import ContactUs from './pages/ContactUs'; import ContactUs from './pages/ContactUs';
import TermsOfService from './pages/TermsOfService'; import TermsOfService from './pages/TermsOfService';
import PrivacyPolicy from './pages/PrivacyPolicy'; import PrivacyPolicy from './pages/PrivacyPolicy';
import AcceptInvitation from './pages/AcceptInvitation';
import NotFound from './pages/NotFound';
const PrivateRoute = ({ children, adminOnly = false }) => { const PrivateRoute = ({ children, adminOnly = false }) => {
const { user, loading } = useAuth(); const { user, loading } = useAuth();
@@ -69,14 +78,19 @@ const PrivateRoute = ({ children, adminOnly = false }) => {
}; };
function App() { function App() {
// Read basename from environment variable (defaults to empty string for root path)
// Set REACT_APP_BASENAME in .env to use a subpath (e.g., "/membership")
const basename = process.env.REACT_APP_BASENAME || '';
return ( return (
<AuthProvider> <AuthProvider>
<BrowserRouter basename="/membership"> <BrowserRouter basename={basename}>
<Routes> <Routes>
<Route path="/" element={<Landing />} /> <Route path="/" element={<Landing />} />
<Route path="/register" element={<Register />} /> <Route path="/register" element={<Register />} />
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route path="/verify-email" element={<VerifyEmail />} /> <Route path="/verify-email" element={<VerifyEmail />} />
<Route path="/accept-invitation" element={<AcceptInvitation />} />
<Route path="/forgot-password" element={<ForgotPassword />} /> <Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/reset-password" element={<ResetPassword />} /> <Route path="/reset-password" element={<ResetPassword />} />
<Route path="/change-password-required" element={ <Route path="/change-password-required" element={
@@ -210,6 +224,13 @@ function App() {
</AdminLayout> </AdminLayout>
</PrivateRoute> </PrivateRoute>
} /> } />
<Route path="/admin/events/:eventId/attendance" element={
<PrivateRoute adminOnly>
<AdminLayout>
<AdminEventAttendance />
</AdminLayout>
</PrivateRoute>
} />
<Route path="/admin/validations" element={ <Route path="/admin/validations" element={
<PrivateRoute adminOnly> <PrivateRoute adminOnly>
<AdminLayout> <AdminLayout>
@@ -231,6 +252,13 @@ 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>
@@ -261,13 +289,28 @@ function App() {
} /> } />
<Route path="/admin/permissions" element={ <Route path="/admin/permissions" element={
<PrivateRoute adminOnly> <PrivateRoute adminOnly>
<AdminLayout> <Navigate to="/admin/settings/permissions" replace />
<AdminRoles />
</AdminLayout>
</PrivateRoute> </PrivateRoute>
} /> } />
<Route path="/admin/settings" element={
<PrivateRoute adminOnly>
<AdminLayout>
<SettingsLayout />
</AdminLayout>
</PrivateRoute>
}>
<Route index element={<Navigate to="stripe" replace />} />
<Route path="stripe" element={<AdminSettings />} />
<Route path="permissions" element={<AdminRoles />} />
<Route path="member-tiers" element={<AdminMemberTiers />} />
<Route path="theme" element={<AdminTheme />} />
</Route>
{/* 404 - Catch all undefined routes */}
<Route path="*" element={<NotFound />} />
</Routes> </Routes>
<Toaster position="top-right" /> <Toaster position="top-right" />
<IdleSessionWarning />
</BrowserRouter> </BrowserRouter>
</AuthProvider> </AuthProvider>
); );

View File

@@ -117,7 +117,7 @@ export default function AddToCalendarButton({
return ( return (
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}> <DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant={variant} size={size} className="gap-2"> <Button variant={variant} size={size} className="bg-[var(--purple-lavender)] text-white hover:bg-[var(--purple-muted)] rounded-full gap-2 dark:hover:bg-brand-lavender dark:hover:text-brand-dark-lavender">
<Calendar className="h-4 w-4" /> <Calendar className="h-4 w-4" />
Add to Calendar Add to Calendar
<ChevronDown className="h-4 w-4" /> <ChevronDown className="h-4 w-4" />
@@ -128,7 +128,7 @@ export default function AddToCalendarButton({
{event && ( {event && (
<> <>
{/* Single Event Export Options */} {/* Single Event Export Options */}
<div className="px-2 py-1.5 text-sm font-semibold text-[#422268]"> <div className="px-2 py-1.5 text-sm font-semibold text-[var(--purple-ink)]">
Add This Event Add This Event
</div> </div>
@@ -177,7 +177,7 @@ export default function AddToCalendarButton({
{showSubscribe && ( {showSubscribe && (
<> <>
{/* Subscription Options */} {/* Subscription Options */}
<div className="px-2 py-1.5 text-sm font-semibold text-[#422268]"> <div className="px-2 py-1.5 text-sm font-semibold text-[var(--purple-ink)]">
Calendar Feeds Calendar Feeds
</div> </div>
@@ -187,7 +187,7 @@ export default function AddToCalendarButton({
> >
<RefreshCw className="h-4 w-4 mr-2" /> <RefreshCw className="h-4 w-4 mr-2" />
Subscribe to My Events Subscribe to My Events
<div className="text-xs text-[#664fa3] mt-0.5"> <div className="text-xs text-brand-purple mt-0.5">
Auto-syncs your RSVP'd events Auto-syncs your RSVP'd events
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
@@ -198,7 +198,7 @@ export default function AddToCalendarButton({
> >
<Download className="h-4 w-4 mr-2" /> <Download className="h-4 w-4 mr-2" />
Download All Events Download All Events
<div className="text-xs text-[#664fa3] mt-0.5"> <div className="text-xs text-brand-purple mt-0.5">
One-time import of all upcoming events One-time import of all upcoming events
</div> </div>
</DropdownMenuItem> </DropdownMenuItem>
@@ -206,7 +206,7 @@ export default function AddToCalendarButton({
)} )}
{!event && !showSubscribe && ( {!event && !showSubscribe && (
<div className="px-2 py-6 text-center text-sm text-[#664fa3]"> <div className="px-2 py-6 text-center text-sm text-brand-purple ">
No event selected No event selected
</div> </div>
)} )}

View File

@@ -1,6 +1,8 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom'; import { Link, useLocation, useNavigate } from 'react-router-dom';
import { useTheme } from 'next-themes';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { useThemeConfig } from '../context/ThemeConfigContext';
import api from '../utils/api'; import api from '../utils/api';
import { Badge } from './ui/badge'; import { Badge } from './ui/badge';
import { import {
@@ -21,17 +23,23 @@ import {
DollarSign, DollarSign,
Scale, Scale,
HardDrive, HardDrive,
Repeat Repeat,
Heart,
Sun,
Moon,
} from 'lucide-react'; } from 'lucide-react';
const AdminSidebar = ({ isOpen, onToggle, isMobile }) => { const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const { user, logout } = useAuth(); const { user, logout } = useAuth();
const { getLogoUrl } = useThemeConfig();
const { theme, setTheme } = useTheme();
const [pendingCount, setPendingCount] = useState(0); const [pendingCount, setPendingCount] = useState(0);
const [storageUsed, setStorageUsed] = useState(0); const [storageUsed, setStorageUsed] = useState(0);
const [storageLimit, setStorageLimit] = useState(0); const [storageLimit, setStorageLimit] = useState(0);
const [storagePercentage, setStoragePercentage] = useState(0); const [storagePercentage, setStoragePercentage] = useState(0);
const isDark = theme === 'dark';
// Fetch pending approvals count // Fetch pending approvals count
useEffect(() => { useEffect(() => {
@@ -39,7 +47,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
try { try {
const response = await api.get('/admin/users'); const response = await api.get('/admin/users');
const pending = response.data.filter(u => const pending = response.data.filter(u =>
['pending_validation', 'pre_validated'].includes(u.status) ['pending_email', 'pending_validation', 'pre_validated', 'payment_pending'].includes(u.status)
); );
setPendingCount(pending.length); setPendingCount(pending.length);
} catch (error) { } catch (error) {
@@ -85,6 +93,10 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
navigate('/login'); navigate('/login');
}; };
const handleThemeToggle = () => {
setTheme(isDark ? 'light' : 'dark');
};
const navItems = [ const navItems = [
{ {
name: 'Dashboard', name: 'Dashboard',
@@ -123,6 +135,12 @@ 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,
@@ -153,10 +171,11 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
path: '/admin/bylaws', path: '/admin/bylaws',
disabled: false disabled: false
}, },
{ {
name: 'Permissions', name: 'Settings',
icon: Shield, icon: Settings,
path: '/admin/permissions', path: '/admin/settings',
disabled: false, disabled: false,
superadminOnly: true superadminOnly: true
} }
@@ -165,11 +184,15 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
// Filter nav items based on user role // Filter nav items based on user role
const filteredNavItems = navItems.filter(item => { const filteredNavItems = navItems.filter(item => {
if (item.superadminOnly && user?.role !== 'superadmin') { if (item.superadminOnly && user?.role !== 'superadmin') {
console.log('Filtering out superadmin-only item:', item.name, 'User role:', user?.role);
return false; return false;
} }
return true; return true;
}); });
// Debug: Log filtered items count
console.log('Total nav items:', navItems.length, 'Filtered items:', filteredNavItems.length, 'User role:', user?.role);
const isActive = (path) => { const isActive = (path) => {
if (path === '/admin') { if (path === '/admin') {
return location.pathname === '/admin'; return location.pathname === '/admin';
@@ -177,43 +200,9 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
return location.pathname.startsWith(path); return location.pathname.startsWith(path);
}; };
return ( const renderNavItem = (item) => {
<> 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);
@@ -229,16 +218,16 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
className={` className={`
flex items-center gap-3 px-4 py-3 rounded-lg transition-all relative flex items-center gap-3 px-4 py-3 rounded-lg transition-all relative
${item.disabled ${item.disabled
? 'opacity-50 cursor-not-allowed text-[#664fa3]' ? 'opacity-50 cursor-not-allowed text-brand-purple '
: active : active
? 'bg-[#ff9e77]/10 text-[#ff9e77]' ? 'bg-[var(--orange-light)]/10 text-[var(--purple-ink)]'
: 'text-[#422268] hover:bg-[#DDD8EB]/20' : 'text-[var(--purple-ink)] hover:bg-[var(--neutral-800)]/20'
} }
`} `}
> >
{/* Active border */} {/* Active border */}
{active && !item.disabled && ( {active && !item.disabled && (
<div className="absolute left-0 top-0 bottom-0 w-1 bg-[#ff9e77] rounded-r" /> <div className="absolute left-0 top-0 bottom-0 w-1 bg-accent rounded-r" />
)} )}
<Icon className="h-5 w-5 flex-shrink-0" /> <Icon className="h-5 w-5 flex-shrink-0" />
@@ -247,12 +236,12 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
<> <>
<span className="flex-1">{item.name}</span> <span className="flex-1">{item.name}</span>
{item.disabled && ( {item.disabled && (
<Badge className="bg-[#DDD8EB] text-[#422268] text-xs px-2 py-0.5"> <Badge className="bg-[var(--neutral-800)] text-[var(--purple-ink)] text-xs px-2 py-0.5">
Soon Soon
</Badge> </Badge>
)} )}
{item.badge > 0 && !item.disabled && ( {item.badge > 0 && !item.disabled && (
<Badge className="bg-[#ff9e77] text-white text-xs px-2 py-0.5"> <Badge className="bg-accent foreground text-xs px-2 py-0.5">
{item.badge} {item.badge}
</Badge> </Badge>
)} )}
@@ -261,7 +250,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
{/* Badge when collapsed */} {/* Badge when collapsed */}
{!isOpen && item.badge > 0 && !item.disabled && ( {!isOpen && item.badge > 0 && !item.disabled && (
<div className="absolute -top-1 -right-1 bg-[#ff9e77] text-white text-xs rounded-full h-5 w-5 flex items-center justify-center font-medium"> <div className="absolute -top-1 -right-1 bg-accent text-white foreground text-xs rounded-full h-5 w-5 flex items-center justify-center font-medium">
{item.badge} {item.badge}
</div> </div>
)} )}
@@ -269,71 +258,219 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
{/* Tooltip when collapsed */} {/* Tooltip when collapsed */}
{!isOpen && ( {!isOpen && (
<div className="absolute left-full ml-2 top-1/2 -translate-y-1/2 px-3 py-2 bg-[#422268] text-white text-sm rounded-lg opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-50"> <div className="absolute left-full ml-2 top-1/2 -translate-y-1/2 px-3 py-2 bg-primary foreground text-sm rounded-lg opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-50">
{item.name} {item.name}
{item.badge > 0 && ` (${item.badge})`} {item.badge > 0 && ` (${item.badge})`}
</div> </div>
)} )}
</div> </div>
); );
})} };
return (
<>
{/* Sidebar */}
<aside
className={`
bg-background border-r border-[var(--neutral-800)] 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-[var(--neutral-800)]">
<Link to="/" className="flex items-center gap-3 group flex-1 min-w-0">
<img
src={getLogoUrl()}
alt="LOAF Logo"
className={`object-contain transition-all duration-200 ${isOpen ? 'h-10 w-10' : 'h-8 w-8'
}`}
/>
{isOpen && (
<div className="flex-1 min-w-0">
<h2 className="text-xl font-semibold text-primary dark:text-brand-light-lavender " style={{ fontFamily: "'Inter', sans-serif" }}>
Admin
</h2>
</div>
)}
</Link>
<button
onClick={onToggle}
className="p-2 rounded-lg hover:bg-[var(--neutral-800)]/20 transition-colors"
aria-label={isOpen ? 'Collapse sidebar' : 'Expand sidebar'}
>
{isMobile ? (
<Menu className="h-5 w-5 text-primary" />
) : isOpen ? (
<ChevronLeft className="h-5 w-5 text-primary" />
) : (
<ChevronRight className="h-5 w-5 text-primary" />
)}
</button>
</div>
{/* Navigation */}
<nav className="flex-1 overflow-y-auto p-4 scrollbar-dashboard scrollbar-x-dashboard">
{/* 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-muted-foreground 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-muted-foreground 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-muted-foreground 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-muted-foreground 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>
{/* SYSTEM Section - Superadmin only */}
{user?.role === 'superadmin' && (
<>
{isOpen && (
<div className="px-4 py-2 mt-6">
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
System
</h3>
</div>
)}
<div className="space-y-1">
{renderNavItem(filteredNavItems.find(item => item.name === 'Permissions'))}
{renderNavItem(filteredNavItems.find(item => item.name === 'Settings'))}
</div>
</>
)}
</nav> </nav>
{/* User Section */} {/* User Section */}
<div className="border-t border-[#ddd8eb] p-4 space-y-2"> <div className="border-t border-[var(--neutral-800)] p-4 space-y-2">
{isOpen && user && ( {isOpen && user && (
<div className="px-4 py-3 mb-2"> <div className="px-4 py-3 mb-2 flex justify-between items-center">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-full bg-[#DDD8EB] flex items-center justify-center text-[#422268] font-semibold"> <div className="h-10 w-10 rounded-full bg-[var(--neutral-800)] flex items-center justify-center text-[var(--purple-ink)] font-semibold">
{user.first_name?.[0]}{user.last_name?.[0]} {user.first_name?.[0]}{user.last_name?.[0]}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-[#422268] truncate" style={{ fontFamily: "'Inter', sans-serif" }}> <p className="text-sm font-medium text-primary dark:text-brand-light-lavender truncate" style={{ fontFamily: "'Inter', sans-serif" }}>
{user.first_name} {user.last_name} {user.first_name} {user.last_name}
</p> </p>
<p className="text-xs text-[#664fa3] capitalize truncate" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-xs text-muted-foreground capitalize truncate" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{user.role} {user.role}
</p> </p>
</div> </div>
</div> </div>
<Link className='dark:text-brand-lavender ' to='/profile'><Settings size={16} />
</Link>
</div> </div>
)} )}
{/* Theme Toggle */}
<div className="relative group">
<button
type="button"
onClick={handleThemeToggle}
aria-pressed={isDark}
aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
className={`
flex items-center gap-3 px-4 py-3 rounded-lg w-full
text-primary dark:text-brand-lavender hover:bg-muted/20 transition-colors
${!isOpen && 'justify-center'}
`}
>
{isDark ? (
<Sun className="h-5 w-5 flex-shrink-0 " />
) : (
<Moon className="h-5 w-5 flex-shrink-0" />
)}
{isOpen && <span >{isDark ? 'Light mode' : 'Dark mode'}</span>}
</button>
{!isOpen && (
<div className="absolute left-full ml-2 top-1/2 -translate-y-1/2 px-3 py-2 bg-primary foreground text-sm rounded-lg opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-50">
{isDark ? 'Light mode' : 'Dark mode'}
</div>
)}
</div>
{/* Storage Usage Widget */} {/* Storage Usage Widget */}
<div className="mb-2"> <div className="mb-2">
{isOpen ? ( {isOpen ? (
<div className="px-4 py-3 bg-[#F8F7FB] rounded-lg"> <div className="px-4 py-3 bg-[var(--lavender-500)] rounded-lg">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-[#422268]">Storage Usage</span> <span className="text-sm font-medium text-primary dark:text-brand-light-lavender ">Storage Usage</span>
<span className="text-xs text-[#664fa3]">{storagePercentage}%</span> <span className="text-xs text-muted-foreground">{storagePercentage}%</span>
</div> </div>
<div className="w-full bg-[#ddd8eb] rounded-full h-2"> <div className="w-full bg-[var(--neutral-800)] rounded-full h-2">
<div <div
className={`h-2 rounded-full transition-all ${ className={`h-2 rounded-full transition-all ${storagePercentage > 90 ? 'bg-red-500' :
storagePercentage > 90 ? 'bg-red-500' :
storagePercentage > 75 ? 'bg-yellow-500' : storagePercentage > 75 ? 'bg-yellow-500' :
'bg-[#81B29A]' 'bg-[var(--green-light)]'
}`} }`}
style={{ width: `${storagePercentage}%` }} style={{ width: `${storagePercentage}%` }}
/> />
</div> </div>
<p className="text-xs text-[#664fa3] mt-1"> <p className="text-xs text-muted-foreground mt-1">
{formatBytes(storageUsed)} / {formatBytes(storageLimit)} {formatBytes(storageUsed)} / {formatBytes(storageLimit)}
</p> </p>
</div> </div>
) : ( ) : (
<div className="flex justify-center"> <div className="flex justify-center">
<div className="relative group"> <div className="relative group">
<HardDrive className={`h-5 w-5 ${ <HardDrive className={`h-5 w-5 ${storagePercentage > 90 ? 'text-red-500' :
storagePercentage > 90 ? 'text-red-500' :
storagePercentage > 75 ? 'text-yellow-500' : storagePercentage > 75 ? 'text-yellow-500' :
'text-[#664fa3]' 'text-muted-foreground'
}`} /> }`} />
{storagePercentage > 75 && ( {storagePercentage > 75 && (
<div className="absolute -top-1 -right-1 bg-red-500 h-2 w-2 rounded-full" /> <div className="absolute -top-1 -right-1 bg-red-500 h-2 w-2 rounded-full" />
)} )}
{/* Tooltip */} {/* Tooltip */}
<div className="absolute left-full ml-2 top-1/2 -translate-y-1/2 px-3 py-2 bg-[#422268] text-white text-sm rounded-lg opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-50"> <div className="absolute left-full ml-2 top-1/2 -translate-y-1/2 px-3 py-2 bg-primary foreground text-sm rounded-lg opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-50">
Storage: {storagePercentage}% Storage: {storagePercentage}%
</div> </div>
</div> </div>
@@ -346,7 +483,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
onClick={handleLogout} onClick={handleLogout}
className={` className={`
flex items-center gap-3 px-4 py-3 rounded-lg w-full flex items-center gap-3 px-4 py-3 rounded-lg w-full
text-[#ff9e77] hover:bg-[#ff9e77]/10 transition-colors text-accent hover:bg-accent/10 transition-colors
${!isOpen && 'justify-center'} ${!isOpen && 'justify-center'}
`} `}
> >
@@ -357,7 +494,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
{/* Logout tooltip when collapsed */} {/* Logout tooltip when collapsed */}
{!isOpen && ( {!isOpen && (
<div className="relative group"> <div className="relative group">
<div className="absolute left-full ml-2 bottom-0 px-3 py-2 bg-[#422268] text-white text-sm rounded-lg opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-50"> <div className="absolute left-full ml-2 bottom-0 px-3 py-2 bg-primary foreground text-sm rounded-lg opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-50">
Logout Logout
</div> </div>
</div> </div>

View File

@@ -55,21 +55,21 @@ export const AttendanceDialog = ({ event, open, onOpenChange, onSuccess }) => {
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto bg-white"> <DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto bg-background scrollbar-dashboard">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-2xl text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <DialogTitle className="text-2xl text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Mark Attendance: {event?.title} Mark Attendance: {event?.title}
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4 mt-4"> <div className="space-y-4 mt-4">
{rsvps.length === 0 ? ( {rsvps.length === 0 ? (
<p className="text-center text-[#664fa3] py-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>No RSVPs yet</p> <p className="text-center text-brand-purple py-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>No RSVPs yet</p>
) : ( ) : (
rsvps.map((rsvp) => ( rsvps.map((rsvp) => (
<div <div
key={rsvp.user_id} key={rsvp.user_id}
className="flex items-center gap-3 p-4 border-2 border-[#ddd8eb] rounded-xl hover:border-[#664fa3] transition-colors" className="flex items-center gap-3 p-4 border-2 border-[var(--neutral-800)] rounded-xl hover:border-brand-purple transition-colors"
> >
<Checkbox <Checkbox
checked={attendance[rsvp.user_id] || false} checked={attendance[rsvp.user_id] || false}
@@ -79,11 +79,11 @@ export const AttendanceDialog = ({ event, open, onOpenChange, onSuccess }) => {
className="w-5 h-5" className="w-5 h-5"
/> />
<div className="flex-1"> <div className="flex-1">
<p className="font-medium text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>{rsvp.user_name}</p> <p className="font-medium text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>{rsvp.user_name}</p>
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{rsvp.user_email}</p> <p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{rsvp.user_email}</p>
</div> </div>
{rsvp.attended && ( {rsvp.attended && (
<span className="text-sm text-[#81B29A] font-medium"> <span className="text-sm text-[var(--green-light)] font-medium">
Attended Attended
</span> </span>
)} )}
@@ -96,14 +96,14 @@ export const AttendanceDialog = ({ event, open, onOpenChange, onSuccess }) => {
<Button <Button
onClick={handleSave} onClick={handleSave}
disabled={loading} disabled={loading}
className="flex-1 bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full" className="flex-1 bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background rounded-full"
> >
{loading ? 'Saving...' : 'Save Attendance'} {loading ? 'Saving...' : 'Save Attendance'}
</Button> </Button>
<Button <Button
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
variant="outline" variant="outline"
className="flex-1 border-2 border-[#ddd8eb] text-[#664fa3] hover:bg-white hover:text-[#422268] rounded-full" className="flex-1 border-2 border-[var(--neutral-800)] text-brand-purple hover:bg-background hover:text-[var(--purple-ink)] rounded-full"
> >
Cancel Cancel
</Button> </Button>

View File

@@ -66,17 +66,17 @@ const ChangePasswordDialog = ({ open, onOpenChange }) => {
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md bg-white"> <DialogContent className="sm:max-w-md bg-background overflow-y-auto max-h-[90vh]">
<DialogHeader> <DialogHeader>
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<div className="inline-flex items-center justify-center w-10 h-10 rounded-full bg-[#f1eef9]"> <div className="inline-flex items-center justify-center w-10 h-10 rounded-full bg-[var(--lavender-300)]">
<Lock className="h-5 w-5 text-[#ff9e77]" /> <Lock className="h-5 w-5 text-[var(--orange-light)]" />
</div> </div>
<DialogTitle className="text-2xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <DialogTitle className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Change Password Change Password
</DialogTitle> </DialogTitle>
</div> </div>
<DialogDescription className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <DialogDescription className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Update your password to keep your account secure. Update your password to keep your account secure.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -92,7 +92,7 @@ const ChangePasswordDialog = ({ open, onOpenChange }) => {
value={formData.currentPassword} value={formData.currentPassword}
onChange={handleInputChange} onChange={handleInputChange}
placeholder="Enter current password" placeholder="Enter current password"
className="h-12 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
/> />
</div> </div>
@@ -106,7 +106,7 @@ const ChangePasswordDialog = ({ open, onOpenChange }) => {
value={formData.newPassword} value={formData.newPassword}
onChange={handleInputChange} onChange={handleInputChange}
placeholder="Enter new password (min. 6 characters)" placeholder="Enter new password (min. 6 characters)"
className="h-12 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
/> />
</div> </div>
@@ -120,23 +120,22 @@ const ChangePasswordDialog = ({ open, onOpenChange }) => {
value={formData.confirmPassword} value={formData.confirmPassword}
onChange={handleInputChange} onChange={handleInputChange}
placeholder="Re-enter new password" placeholder="Re-enter new password"
className="h-12 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
/> />
</div> </div>
<DialogFooter className="mt-6"> <DialogFooter className="mt-6">
<Button <Button
type="button" type="button"
variant="outline"
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
className="rounded-full px-6" className="btn-outline mr-33"
> >
Cancel Cancel
</Button> </Button>
<Button <Button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-6 disabled:opacity-50" className=" btn-primary"
> >
{loading ? 'Changing...' : 'Change Password'} {loading ? 'Changing...' : 'Change Password'}
</Button> </Button>

View File

@@ -0,0 +1,149 @@
import React, { useState, useEffect } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog';
import { Button } from './ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { Label } from './ui/label';
import { AlertCircle, Shield } from 'lucide-react';
import api from '../utils/api';
import { toast } from 'sonner';
export default function ChangeRoleDialog({ open, onClose, user, onSuccess }) {
const [roles, setRoles] = useState([]);
const [selectedRole, setSelectedRole] = useState('');
const [selectedRoleId, setSelectedRoleId] = useState(null);
const [loadingRoles, setLoadingRoles] = useState(false);
const [submitting, setSubmitting] = useState(false);
useEffect(() => {
if (open) {
fetchRoles();
// Pre-select current role
setSelectedRole(user.role);
setSelectedRoleId(user.role_id);
}
}, [open, user]);
const fetchRoles = async () => {
setLoadingRoles(true);
try {
// Reuse existing endpoint that returns assignable roles based on privilege
const response = await api.get('/admin/roles/assignable');
// Map API response to format expected by Select component
const mappedRoles = response.data.map(role => ({
value: role.code,
label: role.name,
id: role.id,
description: role.description
}));
setRoles(mappedRoles);
} catch (error) {
console.error('Failed to fetch assignable roles:', error);
toast.error('Failed to load roles. Please try again.');
} finally {
setLoadingRoles(false);
}
};
const handleSubmit = async () => {
if (!selectedRole) {
toast.error('Please select a role');
return;
}
// Don't submit if role hasn't changed
if (selectedRole === user.role && selectedRoleId === user.role_id) {
toast.info('The selected role is the same as current role');
return;
}
setSubmitting(true);
try {
await api.put(`/admin/users/${user.id}/role`, {
role: selectedRole,
role_id: selectedRoleId
});
toast.success(`Role changed to ${selectedRole}`);
onSuccess();
onClose();
} catch (error) {
const message = error.response?.data?.detail || 'Failed to change role';
toast.error(message);
} finally {
setSubmitting(false);
}
};
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[500px] overflow-y-auto max-h-[90vh]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Shield className="h-5 w-5 text-[#664fa3]" />
Change User Role
</DialogTitle>
<DialogDescription>
Change role for {user.first_name} {user.last_name} ({user.email})
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
{/* Current Role Display */}
<div className="p-3 bg-[#f1eef9] rounded-lg border border-[#DDD8EB]">
<p className="text-sm text-gray-600">Current Role</p>
<p className="font-semibold text-[#664fa3] capitalize">{user.role}</p>
</div>
{/* Role Selection */}
<div className="space-y-2">
<Label htmlFor="role">New Role</Label>
<Select value={selectedRole} onValueChange={setSelectedRole} disabled={loadingRoles}>
<SelectTrigger>
<SelectValue placeholder={loadingRoles ? "Loading roles..." : "Select role"} />
</SelectTrigger>
<SelectContent>
{roles.map((role) => (
<SelectItem key={role.value} value={role.value}>
<span className="capitalize">{role.label}</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Warning for privileged roles */}
{(selectedRole === 'admin' || selectedRole === 'superadmin') && (
<div className="flex items-start gap-2 p-3 bg-amber-50 border border-amber-200 rounded-lg">
<AlertCircle className="h-5 w-5 text-amber-600 flex-shrink-0 mt-0.5" />
<div className="text-sm">
<p className="font-semibold text-amber-900">Admin Access Warning</p>
<p className="text-amber-700">
This user will gain full administrative access to the system.
</p>
</div>
</div>
)}
</div>
<div className="flex justify-end gap-3">
<Button
variant="outline"
onClick={onClose}
disabled={submitting}
className="border-2 border-gray-300 rounded-full"
>
Cancel
</Button>
<Button
onClick={handleSubmit}
disabled={submitting || loadingRoles}
className="bg-[#664fa3] hover:bg-[#7d5ec2] text-white rounded-full"
>
{submitting ? 'Changing Role...' : 'Change Role'}
</Button>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -38,8 +38,8 @@ const ConfirmationDialog = ({
const variants = { const variants = {
warning: { warning: {
icon: AlertTriangle, icon: AlertTriangle,
iconColor: 'text-[#ff9e77]', iconColor: 'text-[var(--orange-light)]',
confirmButtonClass: 'bg-[#ff9e77] text-white hover:bg-[#e88d66] rounded-full px-6', confirmButtonClass: 'bg-[var(--orange-light)] text-white hover:bg-[var(--orange-sand)] rounded-full px-6',
}, },
danger: { danger: {
icon: AlertTriangle, icon: AlertTriangle,
@@ -48,13 +48,13 @@ const ConfirmationDialog = ({
}, },
info: { info: {
icon: Info, icon: Info,
iconColor: 'text-[#664fa3]', iconColor: 'text-brand-purple ',
confirmButtonClass: 'bg-[#664fa3] text-white hover:bg-[#553d8a] rounded-full px-6', confirmButtonClass: 'bg-brand-purple text-white hover:bg-[var(--purple-plum)] rounded-full px-6',
}, },
success: { success: {
icon: CheckCircle, icon: CheckCircle,
iconColor: 'text-[#81B29A]', iconColor: 'text-[var(--green-light)]',
confirmButtonClass: 'bg-[#81B29A] text-white hover:bg-[#6fa188] rounded-full px-6', confirmButtonClass: 'bg-[var(--green-light)] text-white hover:bg-[var(--green-pastel)] rounded-full px-6',
}, },
}; };
@@ -63,21 +63,21 @@ const ConfirmationDialog = ({
return ( return (
<AlertDialog open={open} onOpenChange={onOpenChange}> <AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent className="bg-white rounded-2xl border border-[#ddd8eb] p-0 overflow-hidden max-w-md"> <AlertDialogContent className="bg-background rounded-2xl border border-[var(--neutral-800)] p-0 overflow-hidden max-w-md">
<AlertDialogHeader className="p-6 pb-4"> <AlertDialogHeader className="p-6 pb-4">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className={`p-3 rounded-full bg-[#F8F7FB] ${config.iconColor}`}> <div className={`p-3 rounded-full bg-[var(--lavender-500)] ${config.iconColor}`}>
<Icon className="h-6 w-6" /> <Icon className="h-6 w-6" />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<AlertDialogTitle <AlertDialogTitle
className="text-xl font-semibold text-[#422268] mb-2" className="text-xl font-semibold text-[var(--purple-ink)] mb-2"
style={{ fontFamily: "'Inter', sans-serif" }} style={{ fontFamily: "'Inter', sans-serif" }}
> >
{title} {title}
</AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription <AlertDialogDescription
className="text-[#664fa3] text-sm leading-relaxed" className="text-brand-purple text-sm leading-relaxed"
style={{ fontFamily: "'Nunito Sans', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
> >
{description} {description}
@@ -85,9 +85,9 @@ const ConfirmationDialog = ({
</div> </div>
</div> </div>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter className="p-6 pt-4 bg-[#F8F7FB] flex-row gap-3 justify-end"> <AlertDialogFooter className="p-6 pt-4 bg-[var(--lavender-500)] flex-row gap-3 justify-end">
<AlertDialogCancel <AlertDialogCancel
className="border-2 border-[#ddd8eb] text-[#664fa3] hover:bg-white rounded-full px-6" className="border-2 border-[var(--neutral-800)] text-brand-purple hover:bg-background rounded-full px-6"
disabled={loading} disabled={loading}
> >
{cancelText} {cancelText}

View File

@@ -31,6 +31,7 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
}); });
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
const getTodayDate = () => new Date().toISOString().slice(0, 10);
const handleChange = (field, value) => { const handleChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value })); setFormData(prev => ({ ...prev, [field]: value }));
@@ -84,8 +85,8 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
if (payload.date_of_birth === '') { if (payload.date_of_birth === '') {
delete payload.date_of_birth; delete payload.date_of_birth;
} }
if (payload.member_since === '') { if (!payload.member_since) {
delete payload.member_since; payload.member_since = getTodayDate();
} }
await api.post('/admin/users/create', payload); await api.post('/admin/users/create', payload);
@@ -119,13 +120,13 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[700px] rounded-2xl max-h-[90vh] overflow-y-auto"> <DialogContent className="sm:max-w-[700px] rounded-2xl max-h-[90vh] overflow-y-auto scrollbar-dashboard">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-2xl text-[#422268] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}> <DialogTitle className="text-2xl text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<UserPlus className="h-6 w-6" /> <UserPlus className="h-6 w-6" />
Create Member Create Member
</DialogTitle> </DialogTitle>
<DialogDescription className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <DialogDescription className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Create a new member account with direct login access. Member will be created immediately. Create a new member account with direct login access. Member will be created immediately.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -135,7 +136,7 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
{/* Email & Password Row */} {/* Email & Password Row */}
<div className="grid md:grid-cols-2 gap-4"> <div className="grid md:grid-cols-2 gap-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="email" className="text-[#422268]"> <Label htmlFor="email" className="text-[var(--purple-ink)]">
Email <span className="text-red-500">*</span> Email <span className="text-red-500">*</span>
</Label> </Label>
<Input <Input
@@ -143,7 +144,7 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
type="email" type="email"
value={formData.email} value={formData.email}
onChange={(e) => handleChange('email', e.target.value)} onChange={(e) => handleChange('email', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="member@example.com" placeholder="member@example.com"
/> />
{errors.email && ( {errors.email && (
@@ -152,7 +153,7 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="password" className="text-[#422268]"> <Label htmlFor="password" className="text-[var(--purple-ink)]">
Password <span className="text-red-500">*</span> Password <span className="text-red-500">*</span>
</Label> </Label>
<Input <Input
@@ -160,7 +161,7 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
type="password" type="password"
value={formData.password} value={formData.password}
onChange={(e) => handleChange('password', e.target.value)} onChange={(e) => handleChange('password', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="Minimum 8 characters" placeholder="Minimum 8 characters"
/> />
{errors.password && ( {errors.password && (
@@ -172,15 +173,15 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
{/* Name Row */} {/* Name Row */}
<div className="grid md:grid-cols-2 gap-4"> <div className="grid md:grid-cols-2 gap-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="first_name" className="text-[#422268]"> <Label htmlFor="first_name" className="text-[var(--purple-ink)]">
First Name <span className="text-red-500">*</span> First Name <span className="text-red-500">*</span>
</Label> </Label>
<Input <Input
id="first_name" id="first_name"
value={formData.first_name} value={formData.first_name}
onChange={(e) => handleChange('first_name', e.target.value)} onChange={(e) => handleChange('first_name', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="John" placeholder="Jane"
/> />
{errors.first_name && ( {errors.first_name && (
<p className="text-sm text-red-500">{errors.first_name}</p> <p className="text-sm text-red-500">{errors.first_name}</p>
@@ -188,14 +189,14 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="last_name" className="text-[#422268]"> <Label htmlFor="last_name" className="text-[var(--purple-ink)]">
Last Name <span className="text-red-500">*</span> Last Name <span className="text-red-500">*</span>
</Label> </Label>
<Input <Input
id="last_name" id="last_name"
value={formData.last_name} value={formData.last_name}
onChange={(e) => handleChange('last_name', e.target.value)} onChange={(e) => handleChange('last_name', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="Doe" placeholder="Doe"
/> />
{errors.last_name && ( {errors.last_name && (
@@ -206,7 +207,7 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
{/* Phone */} {/* Phone */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="phone" className="text-[#422268]"> <Label htmlFor="phone" className="text-[var(--purple-ink)]">
Phone <span className="text-red-500">*</span> Phone <span className="text-red-500">*</span>
</Label> </Label>
<Input <Input
@@ -214,7 +215,7 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
type="tel" type="tel"
value={formData.phone} value={formData.phone}
onChange={(e) => handleChange('phone', e.target.value)} onChange={(e) => handleChange('phone', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="(555) 123-4567" placeholder="(555) 123-4567"
/> />
{errors.phone && ( {errors.phone && (
@@ -224,14 +225,14 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
{/* Address */} {/* Address */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="address" className="text-[#422268]"> <Label htmlFor="address" className="text-[var(--purple-ink)]">
Address Address
</Label> </Label>
<Input <Input
id="address" id="address"
value={formData.address} value={formData.address}
onChange={(e) => handleChange('address', e.target.value)} onChange={(e) => handleChange('address', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="123 Main St" placeholder="123 Main St"
/> />
</div> </div>
@@ -239,35 +240,35 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
{/* City, State, Zipcode Row */} {/* City, State, Zipcode Row */}
<div className="grid md:grid-cols-3 gap-4"> <div className="grid md:grid-cols-3 gap-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="city" className="text-[#422268]">City</Label> <Label htmlFor="city" className="text-[var(--purple-ink)]">City</Label>
<Input <Input
id="city" id="city"
value={formData.city} value={formData.city}
onChange={(e) => handleChange('city', e.target.value)} onChange={(e) => handleChange('city', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="San Francisco" placeholder="San Francisco"
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="state" className="text-[#422268]">State</Label> <Label htmlFor="state" className="text-[var(--purple-ink)]">State</Label>
<Input <Input
id="state" id="state"
value={formData.state} value={formData.state}
onChange={(e) => handleChange('state', e.target.value)} onChange={(e) => handleChange('state', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="CA" placeholder="CA"
maxLength={2} maxLength={2}
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="zipcode" className="text-[#422268]">Zipcode</Label> <Label htmlFor="zipcode" className="text-[var(--purple-ink)]">Zipcode</Label>
<Input <Input
id="zipcode" id="zipcode"
value={formData.zipcode} value={formData.zipcode}
onChange={(e) => handleChange('zipcode', e.target.value)} onChange={(e) => handleChange('zipcode', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="94102" placeholder="94102"
/> />
</div> </div>
@@ -276,24 +277,24 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
{/* Dates Row */} {/* Dates Row */}
<div className="grid md:grid-cols-2 gap-4"> <div className="grid md:grid-cols-2 gap-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="date_of_birth" className="text-[#422268]">Date of Birth</Label> <Label htmlFor="date_of_birth" className="text-[var(--purple-ink)]">Date of Birth</Label>
<Input <Input
id="date_of_birth" id="date_of_birth"
type="date" type="date"
value={formData.date_of_birth} value={formData.date_of_birth}
onChange={(e) => handleChange('date_of_birth', e.target.value)} onChange={(e) => handleChange('date_of_birth', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="member_since" className="text-[#422268]">Member Since</Label> <Label htmlFor="member_since" className="text-[var(--purple-ink)]">Member Since</Label>
<Input <Input
id="member_since" id="member_since"
type="date" type="date"
value={formData.member_since} value={formData.member_since}
onChange={(e) => handleChange('member_since', e.target.value)} onChange={(e) => handleChange('member_since', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
/> />
</div> </div>
</div> </div>
@@ -311,7 +312,7 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
</Button> </Button>
<Button <Button
type="submit" type="submit"
className="rounded-xl bg-[#81B29A] hover:bg-[#6DA085] text-white" className="rounded-xl bg-[var(--green-light)] hover:bg-[var(--green-fern)] text-white"
disabled={loading} disabled={loading}
> >
{loading ? ( {loading ? (

View File

@@ -22,10 +22,12 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
first_name: '', first_name: '',
last_name: '', last_name: '',
phone: '', phone: '',
member_since: '',
role: 'admin' role: 'admin'
}); });
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
const getTodayDate = () => new Date().toISOString().slice(0, 10);
const handleChange = (field, value) => { const handleChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value })); setFormData(prev => ({ ...prev, [field]: value }));
@@ -74,7 +76,11 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
setLoading(true); setLoading(true);
try { try {
await api.post('/admin/users/create', formData); const payload = { ...formData };
if (!payload.member_since) {
payload.member_since = getTodayDate();
}
await api.post('/admin/users/create', payload);
toast.success('Staff member created successfully'); toast.success('Staff member created successfully');
// Reset form // Reset form
@@ -84,6 +90,7 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
first_name: '', first_name: '',
last_name: '', last_name: '',
phone: '', phone: '',
member_since: '',
role: 'admin' role: 'admin'
}); });
@@ -99,13 +106,13 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[600px] rounded-2xl"> <DialogContent className="sm:max-w-[600px] rounded-2xl overflow-y-auto max-h-[90vh]">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-2xl text-[#422268] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}> <DialogTitle className="text-2xl text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<UserPlus className="h-6 w-6" /> <UserPlus className="h-6 w-6" />
Create Staff Member Create Staff Member
</DialogTitle> </DialogTitle>
<DialogDescription className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <DialogDescription className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Create a new staff account with direct login access. User will be created immediately. Create a new staff account with direct login access. User will be created immediately.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -114,7 +121,7 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
<div className="grid gap-6 py-4"> <div className="grid gap-6 py-4">
{/* Email */} {/* Email */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="email" className="text-[#422268]"> <Label htmlFor="email" className="text-[var(--purple-ink)]">
Email <span className="text-red-500">*</span> Email <span className="text-red-500">*</span>
</Label> </Label>
<Input <Input
@@ -122,7 +129,7 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
type="email" type="email"
value={formData.email} value={formData.email}
onChange={(e) => handleChange('email', e.target.value)} onChange={(e) => handleChange('email', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="staff@example.com" placeholder="staff@example.com"
/> />
{errors.email && ( {errors.email && (
@@ -132,7 +139,7 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
{/* Password */} {/* Password */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="password" className="text-[#422268]"> <Label htmlFor="password" className="text-[var(--purple-ink)]">
Password <span className="text-red-500">*</span> Password <span className="text-red-500">*</span>
</Label> </Label>
<Input <Input
@@ -140,7 +147,7 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
type="password" type="password"
value={formData.password} value={formData.password}
onChange={(e) => handleChange('password', e.target.value)} onChange={(e) => handleChange('password', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="Minimum 8 characters" placeholder="Minimum 8 characters"
/> />
{errors.password && ( {errors.password && (
@@ -150,15 +157,15 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
{/* First Name */} {/* First Name */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="first_name" className="text-[#422268]"> <Label htmlFor="first_name" className="text-[var(--purple-ink)]">
First Name <span className="text-red-500">*</span> First Name <span className="text-red-500">*</span>
</Label> </Label>
<Input <Input
id="first_name" id="first_name"
value={formData.first_name} value={formData.first_name}
onChange={(e) => handleChange('first_name', e.target.value)} onChange={(e) => handleChange('first_name', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="John" placeholder="Jane"
/> />
{errors.first_name && ( {errors.first_name && (
<p className="text-sm text-red-500">{errors.first_name}</p> <p className="text-sm text-red-500">{errors.first_name}</p>
@@ -167,14 +174,14 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
{/* Last Name */} {/* Last Name */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="last_name" className="text-[#422268]"> <Label htmlFor="last_name" className="text-[var(--purple-ink)]">
Last Name <span className="text-red-500">*</span> Last Name <span className="text-red-500">*</span>
</Label> </Label>
<Input <Input
id="last_name" id="last_name"
value={formData.last_name} value={formData.last_name}
onChange={(e) => handleChange('last_name', e.target.value)} onChange={(e) => handleChange('last_name', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="Doe" placeholder="Doe"
/> />
{errors.last_name && ( {errors.last_name && (
@@ -184,7 +191,7 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
{/* Phone */} {/* Phone */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="phone" className="text-[#422268]"> <Label htmlFor="phone" className="text-[var(--purple-ink)]">
Phone <span className="text-red-500">*</span> Phone <span className="text-red-500">*</span>
</Label> </Label>
<Input <Input
@@ -192,7 +199,7 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
type="tel" type="tel"
value={formData.phone} value={formData.phone}
onChange={(e) => handleChange('phone', e.target.value)} onChange={(e) => handleChange('phone', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="(555) 123-4567" placeholder="(555) 123-4567"
/> />
{errors.phone && ( {errors.phone && (
@@ -200,13 +207,27 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
)} )}
</div> </div>
{/* Member Since */}
<div className="grid gap-2">
<Label htmlFor="member_since" className="text-[var(--purple-ink)]">
Member Since
</Label>
<Input
id="member_since"
type="date"
value={formData.member_since}
onChange={(e) => handleChange('member_since', e.target.value)}
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
/>
</div>
{/* Role */} {/* Role */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="role" className="text-[#422268]"> <Label htmlFor="role" className="text-[var(--purple-ink)]">
Role <span className="text-red-500">*</span> Role <span className="text-red-500">*</span>
</Label> </Label>
<Select value={formData.role} onValueChange={(value) => handleChange('role', value)}> <Select value={formData.role} onValueChange={(value) => handleChange('role', value)}>
<SelectTrigger className="rounded-xl border-2 border-[#ddd8eb]"> <SelectTrigger className="rounded-xl border-2 border-[var(--neutral-800)]">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -229,7 +250,7 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
</Button> </Button>
<Button <Button
type="submit" type="submit"
className="rounded-xl bg-[#81B29A] hover:bg-[#6DA085] text-white" className="rounded-xl bg-[var(--green-light)] hover:bg-[var(--green-fern)] text-white"
disabled={loading} disabled={loading}
> >
{loading ? ( {loading ? (

View File

@@ -0,0 +1,576 @@
import React, { useState, useEffect } from 'react';
import api from '../utils/api';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from './ui/dialog';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Label } from './ui/label';
import { Textarea } from './ui/textarea';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from './ui/select';
import { Card } from './ui/card';
import { toast } from 'sonner';
import { Loader2, Repeat, Search, Calendar, Heart, X, User } from 'lucide-react';
const CreateSubscriptionDialog = ({ open, onOpenChange, onSuccess }) => {
// Search state
const [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState([]);
const [selectedUser, setSelectedUser] = useState(null);
const [searchLoading, setSearchLoading] = useState(false);
const [allUsers, setAllUsers] = useState([]);
// Plan state
const [plans, setPlans] = useState([]);
const [selectedPlan, setSelectedPlan] = useState(null);
const [useCustomPeriod, setUseCustomPeriod] = useState(false);
// Form state
const [formData, setFormData] = useState({
plan_id: '',
amount: '',
payment_date: new Date().toISOString().split('T')[0],
payment_method: 'cash',
custom_period_start: new Date().toISOString().split('T')[0],
custom_period_end: '',
notes: ''
});
const [loading, setLoading] = useState(false);
// Fetch users and plans when dialog opens
useEffect(() => {
const fetchData = async () => {
if (!open) return;
try {
const [usersResponse, plansResponse] = await Promise.all([
api.get('/admin/users'),
api.get('/admin/subscriptions/plans')
]);
setAllUsers(usersResponse.data);
setPlans(plansResponse.data.filter(p => p.active));
} catch (error) {
toast.error('Failed to load data');
}
};
fetchData();
}, [open]);
// Filter users based on search query
useEffect(() => {
if (!searchQuery.trim()) {
setSearchResults([]);
return;
}
setSearchLoading(true);
const query = searchQuery.toLowerCase();
const filtered = allUsers.filter(user =>
user.first_name?.toLowerCase().includes(query) ||
user.last_name?.toLowerCase().includes(query) ||
user.email?.toLowerCase().includes(query)
).slice(0, 10); // Limit to 10 results
setSearchResults(filtered);
setSearchLoading(false);
}, [searchQuery, allUsers]);
// Update amount when plan changes
useEffect(() => {
if (selectedPlan && !formData.amount) {
const suggestedAmount = (selectedPlan.suggested_price_cents || selectedPlan.minimum_price_cents || selectedPlan.price_cents) / 100;
setFormData(prev => ({
...prev,
amount: suggestedAmount.toFixed(2)
}));
}
}, [selectedPlan]);
// Calculate donation breakdown
const getAmountBreakdown = () => {
if (!selectedPlan || !formData.amount) return null;
const totalCents = Math.round(parseFloat(formData.amount) * 100);
const minimumCents = selectedPlan.minimum_price_cents || selectedPlan.price_cents || 3000;
const donationCents = Math.max(0, totalCents - minimumCents);
return {
total: totalCents,
base: minimumCents,
donation: donationCents
};
};
const formatPrice = (cents) => {
return `$${(cents / 100).toFixed(2)}`;
};
const breakdown = getAmountBreakdown();
const handleSelectUser = (user) => {
setSelectedUser(user);
setSearchQuery('');
setSearchResults([]);
};
const handleClearUser = () => {
setSelectedUser(null);
setFormData({
plan_id: '',
amount: '',
payment_date: new Date().toISOString().split('T')[0],
payment_method: 'cash',
custom_period_start: new Date().toISOString().split('T')[0],
custom_period_end: '',
notes: ''
});
setSelectedPlan(null);
setUseCustomPeriod(false);
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!selectedUser) {
toast.error('Please select a user');
return;
}
if (!formData.plan_id) {
toast.error('Please select a subscription plan');
return;
}
if (!formData.amount || parseFloat(formData.amount) <= 0) {
toast.error('Please enter a valid payment amount');
return;
}
// Validate minimum amount
const amountCents = Math.round(parseFloat(formData.amount) * 100);
const minimumCents = selectedPlan.minimum_price_cents || selectedPlan.price_cents || 3000;
if (amountCents < minimumCents) {
toast.error(`Amount must be at least ${formatPrice(minimumCents)}`);
return;
}
if (useCustomPeriod && (!formData.custom_period_start || !formData.custom_period_end)) {
toast.error('Please specify both start and end dates for custom period');
return;
}
setLoading(true);
try {
const payload = {
plan_id: formData.plan_id,
amount_cents: amountCents,
payment_date: new Date(formData.payment_date).toISOString(),
payment_method: formData.payment_method,
override_plan_dates: useCustomPeriod,
notes: formData.notes || null
};
if (useCustomPeriod) {
payload.custom_period_start = new Date(formData.custom_period_start).toISOString();
payload.custom_period_end = new Date(formData.custom_period_end).toISOString();
}
await api.post(`/admin/users/${selectedUser.id}/activate-payment`, payload);
toast.success(`Subscription created for ${selectedUser.first_name} ${selectedUser.last_name}!`);
// Reset form
handleClearUser();
onOpenChange(false);
if (onSuccess) onSuccess();
} catch (error) {
const errorMessage = error.response?.data?.detail || 'Failed to create subscription';
toast.error(errorMessage);
} finally {
setLoading(false);
}
};
const handleClose = () => {
handleClearUser();
setSearchQuery('');
setSearchResults([]);
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-[700px] rounded-2xl max-h-[90vh] overflow-y-auto scrollbar-dashboard">
<DialogHeader>
<DialogTitle className="text-2xl text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<Repeat className="h-6 w-6" />
Create Subscription
</DialogTitle>
<DialogDescription className="text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Search for an existing member and create a subscription with manual payment processing.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="grid gap-6 py-4">
{/* User Search Section */}
{!selectedUser ? (
<div className="space-y-3">
<Label className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
Search Member
</Label>
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-brand-purple" />
<Input
placeholder="Search by name or email..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
/>
{searchLoading && (
<Loader2 className="absolute right-3 top-1/2 transform -translate-y-1/2 h-4 w-4 animate-spin text-brand-purple" />
)}
</div>
{/* Search Results */}
{searchResults.length > 0 && (
<Card className="border-2 border-[var(--neutral-800)] rounded-xl overflow-hidden">
<div className="max-h-60 overflow-y-auto">
{searchResults.map((user) => (
<button
key={user.id}
type="button"
onClick={() => handleSelectUser(user)}
className="w-full p-3 text-left hover:bg-[var(--lavender-400)] transition-colors border-b border-[var(--neutral-800)] last:border-b-0"
>
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-full bg-[var(--neutral-800)]/20 flex items-center justify-center">
<User className="h-5 w-5 text-brand-purple" />
</div>
<div>
<p className="font-medium text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
{user.first_name} {user.last_name}
</p>
<p className="text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{user.email}
</p>
</div>
</div>
</button>
))}
</div>
</Card>
)}
{searchQuery && !searchLoading && searchResults.length === 0 && (
<p className="text-sm text-brand-purple text-center py-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
No members found matching "{searchQuery}"
</p>
)}
</div>
) : (
/* Selected User Card */
<Card className="p-4 bg-[var(--lavender-400)] border-2 border-[var(--neutral-800)] rounded-xl">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="h-12 w-12 rounded-full bg-[var(--neutral-800)]/20 flex items-center justify-center">
<User className="h-6 w-6 text-brand-purple" />
</div>
<div>
<p className="font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
{selectedUser.first_name} {selectedUser.last_name}
</p>
<p className="text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{selectedUser.email}
</p>
</div>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={handleClearUser}
className="text-brand-purple hover:bg-[var(--neutral-800)]/20"
>
<X className="h-4 w-4" />
</Button>
</div>
</Card>
)}
{/* Payment Form - Only show when user is selected */}
{selectedUser && (
<>
{/* Plan Selection */}
<div className="space-y-2">
<Label htmlFor="plan_id" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
Subscription Plan
</Label>
<Select
value={formData.plan_id}
onValueChange={(value) => {
const plan = plans.find(p => p.id === value);
setSelectedPlan(plan);
const suggestedAmount = plan ? (plan.suggested_price_cents || plan.minimum_price_cents || plan.price_cents) / 100 : '';
setFormData({
...formData,
plan_id: value,
amount: suggestedAmount ? suggestedAmount.toFixed(2) : ''
});
}}
>
<SelectTrigger className="rounded-xl border-2 border-[var(--neutral-800)]">
<SelectValue placeholder="Select subscription plan" />
</SelectTrigger>
<SelectContent>
{plans.map(plan => {
const minPrice = (plan.minimum_price_cents || plan.price_cents) / 100;
const sugPrice = plan.suggested_price_cents ? (plan.suggested_price_cents / 100) : null;
return (
<SelectItem key={plan.id} value={plan.id}>
{plan.name} - ${minPrice.toFixed(2)}{sugPrice && sugPrice > minPrice ? ` (Suggested: $${sugPrice.toFixed(2)})` : ''}/{plan.billing_cycle}
</SelectItem>
);
})}
</SelectContent>
</Select>
{selectedPlan && (
<p className="text-xs text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{selectedPlan.description || `${selectedPlan.billing_cycle} subscription`}
</p>
)}
</div>
{/* Payment Amount */}
<div className="space-y-2">
<Label htmlFor="amount" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
Payment Amount ($)
</Label>
<Input
id="amount"
type="number"
step="0.01"
min="0"
placeholder="Enter amount"
value={formData.amount}
onChange={(e) => setFormData({ ...formData, amount: e.target.value })}
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
required
/>
{selectedPlan && (
<p className="text-xs text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Minimum: {formatPrice(selectedPlan.minimum_price_cents || selectedPlan.price_cents || 3000)}
</p>
)}
</div>
{/* Amount Breakdown */}
{breakdown && breakdown.total >= breakdown.base && (
<Card className="p-4 bg-[var(--lavender-400)] border border-[var(--neutral-800)]">
<div className="space-y-2 text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<div className="flex justify-between text-[var(--purple-ink)]">
<span>Membership Fee:</span>
<span className="font-semibold">{formatPrice(breakdown.base)}</span>
</div>
{breakdown.donation > 0 && (
<div className="flex justify-between text-[var(--orange-light)]">
<span className="flex items-center gap-1">
<Heart className="h-4 w-4" />
Additional Donation:
</span>
<span className="font-semibold">{formatPrice(breakdown.donation)}</span>
</div>
)}
<div className="flex justify-between text-[var(--purple-ink)] font-bold text-base pt-2 border-t border-[var(--neutral-800)]">
<span>Total:</span>
<span>{formatPrice(breakdown.total)}</span>
</div>
</div>
</Card>
)}
{/* Payment Date */}
<div className="space-y-2">
<Label htmlFor="payment_date" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
Payment Date
</Label>
<div className="relative">
<Calendar className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-brand-purple" />
<Input
id="payment_date"
type="date"
value={formData.payment_date}
onChange={(e) => setFormData({ ...formData, payment_date: e.target.value })}
className="pl-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
required
/>
</div>
</div>
{/* Payment Method */}
<div className="space-y-2">
<Label htmlFor="payment_method" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
Payment Method
</Label>
<Select
value={formData.payment_method}
onValueChange={(value) => setFormData({ ...formData, payment_method: value })}
>
<SelectTrigger className="rounded-xl border-2 border-[var(--neutral-800)]">
<SelectValue placeholder="Select payment method" />
</SelectTrigger>
<SelectContent>
<SelectItem value="cash">Cash</SelectItem>
<SelectItem value="bank_transfer">Bank Transfer</SelectItem>
<SelectItem value="check">Check</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
</div>
{/* Subscription Period */}
<div className="space-y-3">
<Label className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
Subscription Period
</Label>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="use_custom_period"
checked={useCustomPeriod}
onChange={(e) => setUseCustomPeriod(e.target.checked)}
className="rounded border-[var(--neutral-800)]"
/>
<Label htmlFor="use_custom_period" className="text-sm text-brand-purple font-normal cursor-pointer" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Use custom dates instead of plan's billing cycle
</Label>
</div>
{useCustomPeriod ? (
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="custom_period_start" className="text-sm text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Start Date
</Label>
<Input
id="custom_period_start"
type="date"
value={formData.custom_period_start}
onChange={(e) => setFormData({ ...formData, custom_period_start: e.target.value })}
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
required={useCustomPeriod}
/>
</div>
<div className="space-y-2">
<Label htmlFor="custom_period_end" className="text-sm text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
End Date
</Label>
<Input
id="custom_period_end"
type="date"
value={formData.custom_period_end}
onChange={(e) => setFormData({ ...formData, custom_period_end: e.target.value })}
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
required={useCustomPeriod}
/>
</div>
</div>
) : (
selectedPlan && (
<div className="text-sm text-brand-purple bg-[var(--lavender-300)] p-3 rounded-lg space-y-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{selectedPlan.custom_cycle_enabled ? (
<>
<p>
<span className="font-medium text-[var(--purple-ink)]">Plan uses custom billing cycle:</span>
<br />
{(() => {
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const startMonth = months[(selectedPlan.custom_cycle_start_month || 1) - 1];
const endMonth = months[(selectedPlan.custom_cycle_end_month || 12) - 1];
return `${startMonth} ${selectedPlan.custom_cycle_start_day} - ${endMonth} ${selectedPlan.custom_cycle_end_day} (recurring annually)`;
})()}
</p>
<p className="text-xs">
Subscription will end on the upcoming cycle end date based on today's date.
</p>
</>
) : (
<p>
Will use plan's billing cycle: <span className="font-medium">{selectedPlan.billing_cycle}</span>
<br />
Starts today, ends {selectedPlan.billing_cycle === 'monthly' ? '30 days' :
selectedPlan.billing_cycle === 'quarterly' ? '90 days' :
selectedPlan.billing_cycle === 'yearly' ? '1 year' :
selectedPlan.billing_cycle === 'lifetime' ? 'lifetime' : '1 year'} from now
</p>
)}
</div>
)
)}
</div>
{/* Notes */}
<div className="space-y-2">
<Label htmlFor="notes" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
Notes (Optional)
</Label>
<Textarea
id="notes"
placeholder="Additional notes about the payment..."
value={formData.notes}
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple min-h-[100px]"
/>
</div>
</>
)}
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={handleClose}
className="rounded-xl"
disabled={loading}
>
Cancel
</Button>
<Button
type="submit"
className="rounded-xl bg-[var(--green-light)] hover:bg-[var(--green-fern)] text-white"
disabled={loading || !selectedUser}
>
{loading ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Creating...
</>
) : (
<>
<Repeat className="h-4 w-4 mr-2" />
Create Subscription
</>
)}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
};
export default CreateSubscriptionDialog;

View File

@@ -0,0 +1,232 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import logger from '../utils/logger';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from './ui/dialog';
import { Button } from './ui/button';
import { AlertTriangle, RefreshCw } from 'lucide-react';
/**
* IdleSessionWarning Component
*
* Monitors user activity and warns before session expiration
* - Warns 1 minute before JWT expiry (at 29 minutes if JWT is 30 min)
* - Auto-logout on expiration
* - "Stay Logged In" extends session
*/
const IdleSessionWarning = () => {
const { user, logout, refreshUser } = useAuth();
const navigate = useNavigate();
// Configuration
const SESSION_DURATION = 30 * 60 * 1000; // 30 minutes in milliseconds
const WARNING_BEFORE_EXPIRY = 1 * 60 * 1000; // Warn 1 minute before expiry
const WARNING_TIME = SESSION_DURATION - WARNING_BEFORE_EXPIRY; // 29 minutes
const [showWarning, setShowWarning] = useState(false);
const [timeRemaining, setTimeRemaining] = useState(60); // seconds
const [isExtending, setIsExtending] = useState(false);
const activityTimeoutRef = useRef(null);
const warningTimeoutRef = useRef(null);
const countdownIntervalRef = useRef(null);
const lastActivityRef = useRef(Date.now());
// Reset activity timer
const resetActivityTimer = useCallback(() => {
lastActivityRef.current = Date.now();
// Clear existing timers
if (activityTimeoutRef.current) {
clearTimeout(activityTimeoutRef.current);
}
if (warningTimeoutRef.current) {
clearTimeout(warningTimeoutRef.current);
}
if (countdownIntervalRef.current) {
clearInterval(countdownIntervalRef.current);
}
// Hide warning if showing
if (showWarning) {
setShowWarning(false);
}
// Set new warning timer
warningTimeoutRef.current = setTimeout(() => {
// Show warning
setShowWarning(true);
setTimeRemaining(60); // 60 seconds until logout
// Start countdown
countdownIntervalRef.current = setInterval(() => {
setTimeRemaining((prev) => {
if (prev <= 1) {
// Time's up - logout
handleSessionExpired();
return 0;
}
return prev - 1;
});
}, 1000);
// Set auto-logout timer
activityTimeoutRef.current = setTimeout(() => {
handleSessionExpired();
}, WARNING_BEFORE_EXPIRY);
}, WARNING_TIME);
}, [showWarning]);
// Handle session expiration
const handleSessionExpired = useCallback(() => {
// Clear all timers
if (activityTimeoutRef.current) clearTimeout(activityTimeoutRef.current);
if (warningTimeoutRef.current) clearTimeout(warningTimeoutRef.current);
if (countdownIntervalRef.current) clearInterval(countdownIntervalRef.current);
setShowWarning(false);
logout();
navigate('/login', {
state: { message: 'Your session has expired due to inactivity. Please log in again.' }
});
}, [logout, navigate]);
// Handle "Stay Logged In" button
const handleExtendSession = async () => {
setIsExtending(true);
try {
// Refresh user data to get new token
await refreshUser();
// Reset activity timer
resetActivityTimer();
logger.log('[IdleSessionWarning] Session extended successfully');
} catch (error) {
logger.error('[IdleSessionWarning] Failed to extend session:', error);
// If refresh fails, logout
handleSessionExpired();
} finally {
setIsExtending(false);
}
};
// Track user activity
useEffect(() => {
if (!user) return;
const activityEvents = [
'mousedown',
'mousemove',
'keypress',
'scroll',
'touchstart',
'click'
];
// Throttle activity detection to avoid too many resets
let throttleTimeout = null;
const handleActivity = () => {
if (throttleTimeout) return;
throttleTimeout = setTimeout(() => {
resetActivityTimer();
throttleTimeout = null;
}, 1000); // Throttle to once per second
};
// Add event listeners
activityEvents.forEach(event => {
document.addEventListener(event, handleActivity, { passive: true });
});
// Initialize timer
resetActivityTimer();
// Cleanup
return () => {
activityEvents.forEach(event => {
document.removeEventListener(event, handleActivity);
});
if (activityTimeoutRef.current) clearTimeout(activityTimeoutRef.current);
if (warningTimeoutRef.current) clearTimeout(warningTimeoutRef.current);
if (countdownIntervalRef.current) clearInterval(countdownIntervalRef.current);
if (throttleTimeout) clearTimeout(throttleTimeout);
};
}, [user, resetActivityTimer]);
// Don't render if user is not logged in
if (!user) return null;
return (
<Dialog open={showWarning} onOpenChange={(open) => {
if (!open) {
// Prevent closing dialog by clicking outside
// User must click a button
return;
}
}}>
<DialogContent
className="max-w-md"
onPointerDownOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => e.preventDefault()}
>
<DialogHeader>
<div className="flex items-center gap-3 mb-2">
<div className="bg-[#ff9e77]/10 p-3 rounded-full">
<AlertTriangle className="h-6 w-6 text-[#ff9e77]" />
</div>
<DialogTitle className="text-[#422268]">
Session About to Expire
</DialogTitle>
</div>
<DialogDescription className="text-[#664fa3]">
Your session will expire in <strong className="text-[#422268] text-lg">{timeRemaining}</strong> seconds due to inactivity.
<div className="mt-4 p-4 bg-[#f1eef9] rounded-lg border border-[#ddd8eb]">
<p className="text-sm text-[#422268]">
Click <strong>"Stay Logged In"</strong> to continue your session, or you will be automatically logged out.
</p>
</div>
</DialogDescription>
</DialogHeader>
<DialogFooter className="flex-col sm:flex-row gap-3 mt-4">
<Button
variant="outline"
onClick={handleSessionExpired}
className="border-[#ddd8eb] text-[#664fa3] hover:bg-[#f1eef9]"
>
Log Out Now
</Button>
<Button
onClick={handleExtendSession}
disabled={isExtending}
className="bg-[#664fa3] hover:bg-[#422268] text-white"
>
{isExtending ? (
<>
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
Extending...
</>
) : (
'Stay Logged In'
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default IdleSessionWarning;

View File

@@ -138,13 +138,13 @@ const ImportMembersDialog = ({ open, onOpenChange, onSuccess }) => {
return ( return (
<Dialog open={open} onOpenChange={handleClose}> <Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-[800px] rounded-2xl max-h-[90vh] overflow-y-auto"> <DialogContent className="sm:max-w-[800px] rounded-2xl max-h-[90vh] overflow-y-auto scrollbar-dashboard">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-2xl text-[#422268] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}> <DialogTitle className="text-2xl text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<Upload className="h-6 w-6" /> <Upload className="h-6 w-6" />
{importResult ? 'Import Results' : 'Import Members from CSV'} {importResult ? 'Import Results' : 'Import Members from CSV'}
</DialogTitle> </DialogTitle>
<DialogDescription className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <DialogDescription className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{importResult {importResult
? 'Review the import results below' ? 'Review the import results below'
: 'Upload a CSV file to bulk import members. Ensure the CSV has the required columns.'} : 'Upload a CSV file to bulk import members. Ensure the CSV has the required columns.'}
@@ -155,8 +155,8 @@ const ImportMembersDialog = ({ open, onOpenChange, onSuccess }) => {
// Upload Form // Upload Form
<div className="grid gap-6 py-4"> <div className="grid gap-6 py-4">
{/* CSV Format Instructions */} {/* CSV Format Instructions */}
<Alert className="border-[#664fa3] bg-[#F9F8FB]"> <Alert className="border-brand-purple bg-[var(--lavender-700)]">
<AlertDescription className="text-sm text-[#422268]"> <AlertDescription className="text-sm text-[var(--purple-ink)]">
<strong>Required columns:</strong> Email, First Name, Last Name, Phone, Role <strong>Required columns:</strong> Email, First Name, Last Name, Phone, Role
<br /> <br />
<strong>Optional columns:</strong> Status, Address, City, State, Zipcode, Date of Birth, Member Since <strong>Optional columns:</strong> Status, Address, City, State, Zipcode, Date of Birth, Member Since
@@ -167,10 +167,9 @@ const ImportMembersDialog = ({ open, onOpenChange, onSuccess }) => {
{/* File Upload Area */} {/* File Upload Area */}
<div <div
className={`border-2 border-dashed rounded-2xl p-12 text-center transition-colors ${ className={`border-2 border-dashed rounded-2xl p-12 text-center transition-colors ${dragActive
dragActive ? 'border-brand-purple bg-[var(--lavender-700)]'
? 'border-[#664fa3] bg-[#F9F8FB]' : 'border-[var(--neutral-800)] hover:border-brand-purple hover:bg-[var(--lavender-700)]'
: 'border-[#ddd8eb] hover:border-[#664fa3] hover:bg-[#F9F8FB]'
}`} }`}
onDragEnter={handleDrag} onDragEnter={handleDrag}
onDragLeave={handleDrag} onDragLeave={handleDrag}
@@ -179,12 +178,12 @@ const ImportMembersDialog = ({ open, onOpenChange, onSuccess }) => {
> >
{file ? ( {file ? (
<div className="flex flex-col items-center gap-4"> <div className="flex flex-col items-center gap-4">
<FileUp className="h-16 w-16 text-[#81B29A]" /> <FileUp className="h-16 w-16 text-[var(--green-light)]" />
<div> <div>
<p className="text-lg font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <p className="text-lg font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
{file.name} {file.name}
</p> </p>
<p className="text-sm text-[#664fa3]"> <p className="text-sm text-brand-purple ">
{(file.size / 1024).toFixed(2)} KB {(file.size / 1024).toFixed(2)} KB
</p> </p>
</div> </div>
@@ -199,12 +198,12 @@ const ImportMembersDialog = ({ open, onOpenChange, onSuccess }) => {
</div> </div>
) : ( ) : (
<div className="flex flex-col items-center gap-4"> <div className="flex flex-col items-center gap-4">
<Upload className="h-16 w-16 text-[#ddd8eb]" /> <Upload className="h-16 w-16 text-[var(--neutral-800)]" />
<div> <div>
<p className="text-lg font-semibold text-[#422268] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}> <p className="text-lg font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
Drag and drop your CSV file here Drag and drop your CSV file here
</p> </p>
<p className="text-sm text-[#664fa3] mb-4">or</p> <p className="text-sm text-brand-purple mb-4">or</p>
<Label htmlFor="file-upload"> <Label htmlFor="file-upload">
<Button variant="outline" className="rounded-xl cursor-pointer" asChild> <Button variant="outline" className="rounded-xl cursor-pointer" asChild>
<span>Browse Files</span> <span>Browse Files</span>
@@ -223,14 +222,14 @@ const ImportMembersDialog = ({ open, onOpenChange, onSuccess }) => {
</div> </div>
{/* Options */} {/* Options */}
<div className="flex items-center gap-3 p-4 bg-[#F9F8FB] rounded-xl"> <div className="flex items-center gap-3 p-4 bg-[var(--lavender-700)] rounded-xl">
<Checkbox <Checkbox
checked={updateExisting} checked={updateExisting}
onCheckedChange={setUpdateExisting} onCheckedChange={setUpdateExisting}
id="update-existing" id="update-existing"
className="h-5 w-5 border-2 border-[#664fa3] data-[state=checked]:bg-[#664fa3]" className="h-5 w-5 border-2 border-brand-purple data-[state=checked]:bg-brand-purple "
/> />
<Label htmlFor="update-existing" className="text-[#422268] cursor-pointer"> <Label htmlFor="update-existing" className="text-[var(--purple-ink)] cursor-pointer">
Update existing members (if email already exists) Update existing members (if email already exists)
</Label> </Label>
</div> </div>
@@ -240,9 +239,9 @@ const ImportMembersDialog = ({ open, onOpenChange, onSuccess }) => {
<div className="grid gap-6 py-4"> <div className="grid gap-6 py-4">
{/* Summary Cards */} {/* Summary Cards */}
<div className="grid md:grid-cols-4 gap-4"> <div className="grid md:grid-cols-4 gap-4">
<div className="p-4 bg-white rounded-xl border border-[#ddd8eb] text-center"> <div className="p-4 bg-background rounded-xl border border-[var(--neutral-800)] text-center">
<p className="text-sm text-[#664fa3] mb-1">Total Rows</p> <p className="text-sm text-brand-purple mb-1">Total Rows</p>
<p className="text-2xl font-semibold text-[#422268]">{importResult.total_rows}</p> <p className="text-2xl font-semibold text-[var(--purple-ink)]">{importResult.total_rows}</p>
</div> </div>
<div className="p-4 bg-green-50 rounded-xl border border-green-200 text-center"> <div className="p-4 bg-green-50 rounded-xl border border-green-200 text-center">
<p className="text-sm text-green-700 mb-1">Successful</p> <p className="text-sm text-green-700 mb-1">Successful</p>
@@ -252,7 +251,7 @@ const ImportMembersDialog = ({ open, onOpenChange, onSuccess }) => {
<p className="text-sm text-red-700 mb-1">Failed</p> <p className="text-sm text-red-700 mb-1">Failed</p>
<p className="text-2xl font-semibold text-red-600">{importResult.failed_rows}</p> <p className="text-2xl font-semibold text-red-600">{importResult.failed_rows}</p>
</div> </div>
<div className="p-4 bg-white rounded-xl border border-[#ddd8eb] flex items-center justify-center gap-2"> <div className="p-4 bg-background rounded-xl border border-[var(--neutral-800)] flex items-center justify-center gap-2">
{getStatusIcon(importResult.status)} {getStatusIcon(importResult.status)}
{getStatusBadge(importResult.status)} {getStatusBadge(importResult.status)}
</div> </div>
@@ -261,23 +260,23 @@ const ImportMembersDialog = ({ open, onOpenChange, onSuccess }) => {
{/* Errors Table */} {/* Errors Table */}
{importResult.errors && importResult.errors.length > 0 && ( {importResult.errors && importResult.errors.length > 0 && (
<div> <div>
<h3 className="text-lg font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Errors ({importResult.errors.length} {importResult.errors.length === 10 ? '- showing first 10' : ''}) Errors ({importResult.errors.length} {importResult.errors.length === 10 ? '- showing first 10' : ''})
</h3> </h3>
<div className="border border-[#ddd8eb] rounded-xl overflow-hidden"> <div className="border border-[var(--neutral-800)] rounded-xl overflow-hidden">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="bg-[#DDD8EB] hover:bg-[#DDD8EB]"> <TableRow className="bg-[var(--neutral-800)] hover:bg-[var(--neutral-800)]">
<TableHead className="text-[#422268] font-semibold">Row</TableHead> <TableHead className="text-[var(--purple-ink)] font-semibold">Row</TableHead>
<TableHead className="text-[#422268] font-semibold">Email</TableHead> <TableHead className="text-[var(--purple-ink)] font-semibold">Email</TableHead>
<TableHead className="text-[#422268] font-semibold">Error</TableHead> <TableHead className="text-[var(--purple-ink)] font-semibold">Error</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{importResult.errors.map((error, idx) => ( {importResult.errors.map((error, idx) => (
<TableRow key={idx} className="hover:bg-[#F9F8FB]"> <TableRow key={idx} className="hover:bg-[var(--lavender-700)]">
<TableCell className="font-medium text-[#422268]">{error.row}</TableCell> <TableCell className="font-medium text-[var(--purple-ink)]">{error.row}</TableCell>
<TableCell className="text-[#664fa3]">{error.email}</TableCell> <TableCell className="text-brand-purple ">{error.email}</TableCell>
<TableCell className="text-red-600 text-sm">{error.error}</TableCell> <TableCell className="text-red-600 text-sm">{error.error}</TableCell>
</TableRow> </TableRow>
))} ))}
@@ -302,7 +301,7 @@ const ImportMembersDialog = ({ open, onOpenChange, onSuccess }) => {
</Button> </Button>
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
className="rounded-xl bg-[#81B29A] hover:bg-[#6DA085] text-white" className="rounded-xl bg-[var(--green-light)] hover:bg-[var(--green-fern)] text-white"
disabled={loading || !file} disabled={loading || !file}
> >
{loading ? ( {loading ? (
@@ -321,7 +320,7 @@ const ImportMembersDialog = ({ open, onOpenChange, onSuccess }) => {
) : ( ) : (
<Button <Button
onClick={handleClose} onClick={handleClose}
className="rounded-xl bg-[#81B29A] hover:bg-[#6DA085] text-white" className="rounded-xl bg-[var(--green-light)] hover:bg-[var(--green-fern)] text-white"
> >
Done Done
</Button> </Button>

View File

@@ -0,0 +1,281 @@
import React, { useState, useEffect } from 'react';
import api from '../utils/api';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from './ui/dialog';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Label } from './ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { toast } from 'sonner';
import { Loader2, Mail, Copy, Check } from 'lucide-react';
const InviteMemberDialog = ({ open, onOpenChange, onSuccess }) => {
const [formData, setFormData] = useState({
email: '',
first_name: '',
last_name: '',
phone: '',
role: 'admin'
});
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState({});
const [invitationUrl, setInvitationUrl] = useState(null);
const [copied, setCopied] = useState(false);
const [roles, setRoles] = useState([]);
const [loadingRoles, setLoadingRoles] = useState(false);
// Fetch roles when dialog opens
useEffect(() => {
if (open) {
fetchRoles();
}
}, [open]);
const fetchRoles = async () => {
setLoadingRoles(true);
try {
// New endpoint returns roles based on user's permission level
// Superadmin: all roles
// Admin: admin, finance, and non-elevated custom roles
const response = await api.get('/admin/roles/assignable');
setRoles(response.data);
} catch (error) {
console.error('Failed to fetch assignable roles:', error);
toast.error('Failed to load roles. Please try again.');
} finally {
setLoadingRoles(false);
}
};
const handleChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
// Clear error when user starts typing
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: null }));
}
};
const validate = () => {
const newErrors = {};
if (!formData.email) {
newErrors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = 'Invalid email format';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validate()) {
return;
}
setLoading(true);
try {
const response = await api.post('/admin/users/invite', formData);
toast.success('Invitation sent successfully');
// Show invitation URL
setInvitationUrl(response.data.invitation_url);
// Don't close dialog yet - show invitation URL first
if (onSuccess) onSuccess();
} catch (error) {
const errorMessage = error.response?.data?.detail || 'Failed to send invitation';
toast.error(errorMessage);
} finally {
setLoading(false);
}
};
const copyToClipboard = () => {
navigator.clipboard.writeText(invitationUrl);
setCopied(true);
toast.success('Invitation link copied to clipboard');
setTimeout(() => setCopied(false), 2000);
};
const handleClose = () => {
// Reset form
setFormData({
email: '',
first_name: '',
last_name: '',
phone: '',
role: 'admin'
});
setInvitationUrl(null);
setCopied(false);
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-[600px] rounded-2xl overflow-y-auto max-h-[90vh]">
<DialogHeader>
<DialogTitle className="text-2xl text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<Mail className="h-6 w-6" />
{invitationUrl ? 'Invitation Sent' : 'Invite Member'}
</DialogTitle>
<DialogDescription className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{invitationUrl
? 'The invitation has been sent via email. You can also copy the link below.'
: 'Send an email invitation to join as member. They will set their own password.'}
</DialogDescription>
</DialogHeader>
{invitationUrl ? (
// Show invitation URL after successful send
<div className="py-4">
<Label className="text-[var(--purple-ink)] mb-2 block">Invitation Link (expires in 7 days)</Label>
<div className="flex gap-2">
<Input
value={invitationUrl}
readOnly
className="rounded-xl border-2 border-[var(--neutral-800)] bg-gray-50"
/>
<Button
onClick={copyToClipboard}
className="rounded-xl bg-brand-purple hover:bg-[var(--purple-ink)] text-white flex-shrink-0"
>
{copied ? (
<>
<Check className="h-4 w-4 mr-2" />
Copied
</>
) : (
<>
<Copy className="h-4 w-4 mr-2" />
Copy
</>
)}
</Button>
</div>
</div>
) : (
// Show invitation form
<form onSubmit={handleSubmit}>
<div className="grid gap-6 py-4">
{/* Email */}
<div className="grid gap-2">
<Label htmlFor="email" className="text-[var(--purple-ink)]">
Email <span className="text-red-500">*</span>
</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleChange('email', e.target.value)}
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="member@example.com"
/>
{errors.email && (
<p className="text-sm text-red-500">{errors.email}</p>
)}
</div>
{/* First Name (Optional) */}
<div className="grid gap-2">
<Label htmlFor="first_name" className="text-[var(--purple-ink)]">
First Name <span className="text-gray-400">(Optional)</span>
</Label>
<Input
id="first_name"
value={formData.first_name}
onChange={(e) => handleChange('first_name', e.target.value)}
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="Jane"
/>
</div>
{/* Last Name (Optional) */}
<div className="grid gap-2">
<Label htmlFor="last_name" className="text-[var(--purple-ink)]">
Last Name <span className="text-gray-400">(Optional)</span>
</Label>
<Input
id="last_name"
value={formData.last_name}
onChange={(e) => handleChange('last_name', e.target.value)}
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="Doe"
/>
</div>
{/* Phone (Optional) */}
<div className="grid gap-2">
<Label htmlFor="phone" className="text-[var(--purple-ink)]">
Phone <span className="text-gray-400">(Optional)</span>
</Label>
<Input
id="phone"
type="tel"
value={formData.phone}
onChange={(e) => handleChange('phone', e.target.value)}
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="(555) 123-4567"
/>
</div>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={handleClose}
className="rounded-xl"
disabled={loading}
>
Cancel
</Button>
<Button
type="submit"
className="rounded-xl bg-[var(--green-light)] hover:bg-[var(--green-fern)] text-white"
disabled={loading}
>
{loading ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Sending...
</>
) : (
<>
<Mail className="h-4 w-4 mr-2" />
Send Invitation
</>
)}
</Button>
</DialogFooter>
</form>
)}
{invitationUrl && (
<DialogFooter>
<Button
onClick={handleClose}
className="rounded-xl bg-[var(--green-light)] hover:bg-[var(--green-fern)] text-white"
>
Done
</Button>
</DialogFooter>
)}
</DialogContent>
</Dialog>
);
};
export default InviteMemberDialog;

View File

@@ -40,15 +40,14 @@ const InviteStaffDialog = ({ open, onOpenChange, onSuccess }) => {
const fetchRoles = async () => { const fetchRoles = async () => {
setLoadingRoles(true); setLoadingRoles(true);
try { try {
const response = await api.get('/admin/roles'); // New endpoint returns roles based on user's permission level
// Filter to show only admin-type roles (not guest or member) // Superadmin: all roles
const staffRoles = response.data.filter(role => // Admin: admin, finance, and non-elevated custom roles
['admin', 'superadmin', 'finance'].includes(role.code) || !role.is_system_role const response = await api.get('/admin/roles/assignable');
); setRoles(response.data);
setRoles(staffRoles);
} catch (error) { } catch (error) {
console.error('Failed to fetch roles:', error); console.error('Failed to fetch assignable roles:', error);
toast.error('Failed to load roles'); toast.error('Failed to load roles. Please try again.');
} finally { } finally {
setLoadingRoles(false); setLoadingRoles(false);
} }
@@ -124,13 +123,13 @@ const InviteStaffDialog = ({ open, onOpenChange, onSuccess }) => {
return ( return (
<Dialog open={open} onOpenChange={handleClose}> <Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-[600px] rounded-2xl"> <DialogContent className="sm:max-w-[600px] rounded-2xl overflow-y-auto max-h-[90vh]">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-2xl text-[#422268] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}> <DialogTitle className="text-2xl text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<Mail className="h-6 w-6" /> <Mail className="h-6 w-6" />
{invitationUrl ? 'Invitation Sent' : 'Invite Staff Member'} {invitationUrl ? 'Invitation Sent' : 'Invite Staff Member'}
</DialogTitle> </DialogTitle>
<DialogDescription className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <DialogDescription className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{invitationUrl {invitationUrl
? 'The invitation has been sent via email. You can also copy the link below.' ? 'The invitation has been sent via email. You can also copy the link below.'
: 'Send an email invitation to join as staff. They will set their own password.'} : 'Send an email invitation to join as staff. They will set their own password.'}
@@ -140,16 +139,16 @@ const InviteStaffDialog = ({ open, onOpenChange, onSuccess }) => {
{invitationUrl ? ( {invitationUrl ? (
// Show invitation URL after successful send // Show invitation URL after successful send
<div className="py-4"> <div className="py-4">
<Label className="text-[#422268] mb-2 block">Invitation Link (expires in 7 days)</Label> <Label className="text-[var(--purple-ink)] mb-2 block">Invitation Link (expires in 7 days)</Label>
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
value={invitationUrl} value={invitationUrl}
readOnly readOnly
className="rounded-xl border-2 border-[#ddd8eb] bg-gray-50" className="rounded-xl border-2 border-[var(--neutral-800)] bg-gray-50"
/> />
<Button <Button
onClick={copyToClipboard} onClick={copyToClipboard}
className="rounded-xl bg-[#664fa3] hover:bg-[#422268] text-white flex-shrink-0" className="rounded-xl bg-brand-purple hover:bg-[var(--purple-ink)] text-white flex-shrink-0"
> >
{copied ? ( {copied ? (
<> <>
@@ -171,7 +170,7 @@ const InviteStaffDialog = ({ open, onOpenChange, onSuccess }) => {
<div className="grid gap-6 py-4"> <div className="grid gap-6 py-4">
{/* Email */} {/* Email */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="email" className="text-[#422268]"> <Label htmlFor="email" className="text-[var(--purple-ink)]">
Email <span className="text-red-500">*</span> Email <span className="text-red-500">*</span>
</Label> </Label>
<Input <Input
@@ -179,7 +178,7 @@ const InviteStaffDialog = ({ open, onOpenChange, onSuccess }) => {
type="email" type="email"
value={formData.email} value={formData.email}
onChange={(e) => handleChange('email', e.target.value)} onChange={(e) => handleChange('email', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="staff@example.com" placeholder="staff@example.com"
/> />
{errors.email && ( {errors.email && (
@@ -189,35 +188,35 @@ const InviteStaffDialog = ({ open, onOpenChange, onSuccess }) => {
{/* First Name (Optional) */} {/* First Name (Optional) */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="first_name" className="text-[#422268]"> <Label htmlFor="first_name" className="text-[var(--purple-ink)]">
First Name <span className="text-gray-400">(Optional)</span> First Name <span className="text-gray-400">(Optional)</span>
</Label> </Label>
<Input <Input
id="first_name" id="first_name"
value={formData.first_name} value={formData.first_name}
onChange={(e) => handleChange('first_name', e.target.value)} onChange={(e) => handleChange('first_name', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="John" placeholder="Jane"
/> />
</div> </div>
{/* Last Name (Optional) */} {/* Last Name (Optional) */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="last_name" className="text-[#422268]"> <Label htmlFor="last_name" className="text-[var(--purple-ink)]">
Last Name <span className="text-gray-400">(Optional)</span> Last Name <span className="text-gray-400">(Optional)</span>
</Label> </Label>
<Input <Input
id="last_name" id="last_name"
value={formData.last_name} value={formData.last_name}
onChange={(e) => handleChange('last_name', e.target.value)} onChange={(e) => handleChange('last_name', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="Doe" placeholder="Doe"
/> />
</div> </div>
{/* Phone (Optional) */} {/* Phone (Optional) */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="phone" className="text-[#422268]"> <Label htmlFor="phone" className="text-[var(--purple-ink)]">
Phone <span className="text-gray-400">(Optional)</span> Phone <span className="text-gray-400">(Optional)</span>
</Label> </Label>
<Input <Input
@@ -225,14 +224,14 @@ const InviteStaffDialog = ({ open, onOpenChange, onSuccess }) => {
type="tel" type="tel"
value={formData.phone} value={formData.phone}
onChange={(e) => handleChange('phone', e.target.value)} onChange={(e) => handleChange('phone', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="(555) 123-4567" placeholder="(555) 123-4567"
/> />
</div> </div>
{/* Role */} {/* Role */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="role" className="text-[#422268]"> <Label htmlFor="role" className="text-[var(--purple-ink)]">
Role <span className="text-red-500">*</span> Role <span className="text-red-500">*</span>
</Label> </Label>
<Select <Select
@@ -240,7 +239,7 @@ const InviteStaffDialog = ({ open, onOpenChange, onSuccess }) => {
onValueChange={(value) => handleChange('role', value)} onValueChange={(value) => handleChange('role', value)}
disabled={loadingRoles} disabled={loadingRoles}
> >
<SelectTrigger className="rounded-xl border-2 border-[#ddd8eb]"> <SelectTrigger className="rounded-xl border-2 border-[var(--neutral-800)]">
<SelectValue placeholder={loadingRoles ? "Loading roles..." : "Select a role"} /> <SelectValue placeholder={loadingRoles ? "Loading roles..." : "Select a role"} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -276,7 +275,7 @@ const InviteStaffDialog = ({ open, onOpenChange, onSuccess }) => {
</Button> </Button>
<Button <Button
type="submit" type="submit"
className="rounded-xl bg-[#81B29A] hover:bg-[#6DA085] text-white" className="rounded-xl bg-[var(--green-light)] hover:bg-[var(--green-fern)] text-white"
disabled={loading} disabled={loading}
> >
{loading ? ( {loading ? (
@@ -299,7 +298,7 @@ const InviteStaffDialog = ({ open, onOpenChange, onSuccess }) => {
<DialogFooter> <DialogFooter>
<Button <Button
onClick={handleClose} onClick={handleClose}
className="rounded-xl bg-[#81B29A] hover:bg-[#6DA085] text-white" className="rounded-xl bg-[var(--green-light)] hover:bg-[var(--green-fern)] text-white"
> >
Done Done
</Button> </Button>

View File

@@ -0,0 +1,19 @@
// src/components/MemberBadge.js
import React from 'react';
import { Badge } from './ui/badge';
import { getTierForMember } from '../utils/member-tiers';
import { getTierIcon } from '../config/memberTierIcons';
const MemberBadge = ({ memberSince, tiers }) => {
const tier = getTierForMember(memberSince, tiers);
const Icon = getTierIcon(tier.iconKey);
return (
<Badge className={`px-3 py-2 rounded-md text-sm flex items-center gap-2 border hover:text-white ${tier.badgeClass}`}>
<Icon className="size-6" />
{tier.label}
</Badge>
);
};
export default MemberBadge;

View File

@@ -0,0 +1,187 @@
import React from 'react'
import { Card } from './ui/card';
import { Button } from './ui/button';
import { Heart, Calendar, Mail, Phone, MapPin, Facebook, Instagram, Twitter, Linkedin, UserCircle } from 'lucide-react';
import MemberBadge from './MemberBadge';
// Helper function to get initials
const getInitials = (firstName, lastName) => {
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
};
// Helper function to ensure social media URLs have proper protocol
const getSocialMediaLink = (url) => {
if (!url) return null;
if (!url.startsWith('http://') && !url.startsWith('https://')) {
return `https://${url}`;
}
return url;
};
const MemberCard = ({ member, onViewProfile, tiers }) => {
const memberSince = member.member_since || member.created_at;
return (
<Card className="p-6 bg-background rounded-3xl border border-[var(--neutral-800)] hover:shadow-lg transition-all h-full">
{/* Member Tier Badge */}
<div className='flex justify-end items-center mb-2'>
<MemberBadge memberSince={memberSince} tiers={tiers} />
</div>
<div className="flex justify-center mb-4">
{member.profile_photo_url ? (
<img
src={member.profile_photo_url}
alt={`${member.first_name} ${member.last_name}`}
className="w-32 h-32 rounded-full object-cover border-4 border-[var(--neutral-800)]"
/>
) : (
<div className="w-32 h-32 rounded-full bg-[var(--neutral-800)] border-4 border-[var(--neutral-800)] flex items-center justify-center">
<span className="text-4xl font-semibold text-brand-purple " style={{ fontFamily: "'Inter', sans-serif" }}>
{getInitials(member.first_name, member.last_name)}
</span>
</div>
)}
</div>
{/* Name */}
<h3 className="text-2xl font-semibold text-[var(--purple-ink)] text-center mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
{member.first_name} {member.last_name}
</h3>
{/* Partner Name */}
{member.directory_partner_name && (
<div className="flex items-center justify-center gap-2 mb-4">
<Heart className="h-4 w-4 text-[var(--orange-light)]" />
<span className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Partner: {member.directory_partner_name}
</span>
</div>
)}
{/* Bio */}
{member.directory_bio && (
<p className="text-brand-purple text-center mb-4 line-clamp-3" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{member.directory_bio}
</p>
)}
{/* Member Since */}
{memberSince && (
<div className="flex items-center justify-center gap-2 mb-4">
<Calendar className="h-4 w-4 text-brand-purple " />
<span className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Member since {new Date(memberSince).toLocaleDateString('en-US', {
month: 'long',
year: 'numeric'
})}
</span>
</div>
)}
{/* Contact Information */}
<div className="space-y-3 mb-4">
{member.directory_email && (
<div className="flex items-center gap-2 text-sm">
<Mail className="h-4 w-4 text-brand-purple flex-shrink-0" />
<a
href={`mailto:${member.directory_email}`}
className="text-brand-purple hover:text-[var(--purple-ink)] truncate"
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
{member.directory_email}
</a>
</div>
)}
{member.directory_phone && (
<div className="flex items-center gap-2 text-sm">
<Phone className="h-4 w-4 text-brand-purple flex-shrink-0" />
<a
href={`tel:${member.directory_phone}`}
className="text-brand-purple hover:text-[var(--purple-ink)]"
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
{member.directory_phone}
</a>
</div>
)}
{member.directory_address && (
<div className="flex items-start gap-2 text-sm">
<MapPin className="h-4 w-4 text-brand-purple flex-shrink-0 mt-0.5" />
<span className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{member.directory_address}
</span>
</div>
)}
</div>
{/* Social Media Links */}
{(member.social_media_facebook || member.social_media_instagram || member.social_media_twitter || member.social_media_linkedin) && (
<div className="pt-4 border-t border-[var(--neutral-800)]">
<div className="flex justify-center gap-3">
{member.social_media_facebook && (
<a
href={getSocialMediaLink(member.social_media_facebook)}
target="_blank"
rel="noopener noreferrer"
className="p-2 rounded-lg bg-[var(--lavender-500)] hover:bg-[var(--neutral-800)] transition-colors"
title="Facebook"
>
<Facebook className="h-5 w-5 text-[var(--blue-facebook)]" />
</a>
)}
{member.social_media_instagram && (
<a
href={getSocialMediaLink(member.social_media_instagram)}
target="_blank"
rel="noopener noreferrer"
className="p-2 rounded-lg bg-[var(--lavender-500)] hover:bg-[var(--neutral-800)] transition-colors"
title="Instagram"
>
<Instagram className="h-5 w-5 text-[var(--red-instagram)]" />
</a>
)}
{member.social_media_twitter && (
<a
href={getSocialMediaLink(member.social_media_twitter)}
target="_blank"
rel="noopener noreferrer"
className="p-2 rounded-lg bg-[var(--lavender-500)] hover:bg-[var(--neutral-800)] transition-colors"
title="Twitter/X"
>
<Twitter className="h-5 w-5 text-[var(--blue-twitter)]" />
</a>
)}
{member.social_media_linkedin && (
<a
href={getSocialMediaLink(member.social_media_linkedin)}
target="_blank"
rel="noopener noreferrer"
className="p-2 rounded-lg bg-[var(--lavender-500)] hover:bg-[var(--neutral-800)] transition-colors"
title="LinkedIn"
>
<Linkedin className="h-5 w-5 text-[var(--blue-linkedin)]" />
</a>
)}
</div>
</div>
)}
{/* View Profile Button */}
<div className="pt-4 mt-4 border-t border-[var(--neutral-800)]">
<Button
onClick={() => onViewProfile?.(member.id)}
className="w-full bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-brand-purple hover:text-white rounded-full py-5"
>
<UserCircle className="h-4 w-4 mr-2" />
View Full Profile
</Button>
</div>
</Card>
);
};
export default MemberCard

View File

@@ -4,7 +4,7 @@ import { Calendar, Users, User, BookOpen, FileText, DollarSign, Scale } from 'lu
const MemberFooter = () => { const MemberFooter = () => {
return ( return (
<footer className="bg-[#422268] text-white mt-auto"> <footer className="bg-brand-dark-lavender text-white mt-auto">
<div className="max-w-7xl mx-auto px-6 py-12"> <div className="max-w-7xl mx-auto px-6 py-12">
<div className="grid md:grid-cols-4 gap-8"> <div className="grid md:grid-cols-4 gap-8">
{/* Logo & About */} {/* Logo & About */}
@@ -89,12 +89,12 @@ const MemberFooter = () => {
</Link> </Link>
</li> </li>
<li> <li>
<a href="/#contact" className="text-gray-300 hover:text-white transition-colors"> <a href="/membership/contact-us" className="text-gray-300 hover:text-white transition-colors">
Contact Us Contact Us
</a> </a>
</li> </li>
<li> <li>
<a href="/#donate" className="text-gray-300 hover:text-white transition-colors"> <a href="/membership/donate" className="text-gray-300 hover:text-white transition-colors">
Donate Donate
</a> </a>
</li> </li>
@@ -104,14 +104,14 @@ const MemberFooter = () => {
</div> </div>
{/* Bottom Bar */} {/* Bottom Bar */}
<div className="border-t border-[#664fa3]"> <div className="border-t border-[var(--purple-lavender)]">
<div className="max-w-7xl mx-auto px-6 py-4"> <div className="max-w-7xl mx-auto px-6 py-4">
<div className="flex flex-col md:flex-row justify-between items-center gap-4 text-sm text-gray-400" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <div className="flex flex-col md:flex-row justify-between items-center gap-4 text-sm text-gray-300" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<div className="flex gap-6"> <div className="flex gap-6">
<a href="/#terms" className="hover:text-white transition-colors">Terms of Service</a> <a href="/membership/terms-of-service" className="hover:text-white transition-colors">Terms of Service</a>
<a href="/#privacy" className="hover:text-white transition-colors">Privacy Policy</a> <a href="/membership/privacy-policy" className="hover:text-white transition-colors">Privacy Policy</a>
</div> </div>
<p>© 2025 LOAF. All rights reserved.</p> <p>© {new Date().getFullYear()} LOAF. All rights reserved.</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -19,8 +19,8 @@ const MemberRoute = ({ children }) => {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen flex items-center justify-center bg-[#FDFCF8]"> <div className="min-h-screen flex items-center justify-center bg-[var(--neutral-200:)]">
<p className="text-[#6B708D]">Loading...</p> <p className="text-[var(--slate-muted)]">Loading...</p>
</div> </div>
); );
} }

View File

@@ -1,6 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { useThemeConfig } from '../context/ThemeConfigContext';
import { Button } from './ui/button'; import { Button } from './ui/button';
import { ChevronDown, Menu, X } from 'lucide-react'; import { ChevronDown, Menu, X } from 'lucide-react';
import { import {
@@ -12,11 +13,12 @@ import {
const Navbar = () => { const Navbar = () => {
const { user, logout } = useAuth(); const { user, logout } = useAuth();
const { getLogoUrl } = useThemeConfig();
const navigate = useNavigate(); const navigate = useNavigate();
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
// LOAF logo (local) // Get logo URL from theme config (with fallback to default)
const loafLogo = `${process.env.PUBLIC_URL}/loaf-logo.png`; const loafLogo = getLogoUrl();
const handleLogout = () => { const handleLogout = () => {
logout(); logout();
@@ -26,7 +28,7 @@ const Navbar = () => {
return ( return (
<> <>
{/* Top Header - Member Actions (Desktop Only) */} {/* Top Header - Member Actions (Desktop Only) */}
<header className="hidden lg:flex bg-gradient-to-r from-[#644c9f] to-[#48286e] px-4 sm:px-8 md:px-16 py-4 justify-end items-center gap-4 sm:gap-6"> <header className="hidden lg:flex bg-gradient-to-r from-[var(--purple-amethyst)] to-[var(--purple-deep)] px-4 sm:px-8 md:px-16 py-4 justify-end items-center gap-4 sm:gap-6">
{user && ( {user && (
<span className="text-white text-base font-medium" style={{ fontFamily: "'Poppins', sans-serif" }}> <span className="text-white text-base font-medium" style={{ fontFamily: "'Poppins', sans-serif" }}>
Welcome, {user.first_name} Welcome, {user.first_name}
@@ -39,7 +41,7 @@ const Navbar = () => {
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}
data-testid="admin-nav-button" data-testid="admin-nav-button"
> >
Admin Panel Dashboard
</button> </button>
</Link> </Link>
)} )}
@@ -53,7 +55,7 @@ const Navbar = () => {
</button> </button>
<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-[var(--orange-light)] hover:bg-[var(--orange-coral)] text-[var(--purple-deep)] rounded-[25px] px-[54px] py-[10px] text-[16.5px] font-semibold h-[41px]"
style={{ fontFamily: "'Montserrat', sans-serif" }} style={{ fontFamily: "'Montserrat', sans-serif" }}
> >
Donate Donate
@@ -62,7 +64,7 @@ const Navbar = () => {
</header> </header>
{/* Main Header - Member Navigation */} {/* Main Header - Member Navigation */}
<header className="bg-[#664fa3] px-4 sm:px-8 md:px-16 py-2 flex justify-between items-center"> <header className="bg-[var(--purple-lavender)] px-4 sm:px-8 md:px-16 py-2 flex justify-between items-center">
<Link to="/dashboard"> <Link to="/dashboard">
<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>
@@ -84,21 +86,21 @@ const Navbar = () => {
<ChevronDown className="h-4 w-4" /> <ChevronDown className="h-4 w-4" />
</button> </button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="bg-white min-w-[220px]"> <DropdownMenuContent align="start" className="bg-background 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-[var(--purple-deep)] hover:bg-[var(--lavender-300)] cursor-pointer"
style={{ fontFamily: "'Poppins', sans-serif" }}> style={{ fontFamily: "'Poppins', 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-[var(--purple-deep)] hover:bg-[var(--lavender-300)] cursor-pointer"
style={{ fontFamily: "'Poppins', sans-serif" }}> style={{ fontFamily: "'Poppins', 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-[var(--purple-deep)] hover:bg-[var(--lavender-300)] cursor-pointer"
style={{ fontFamily: "'Poppins', sans-serif" }}> style={{ fontFamily: "'Poppins', sans-serif" }}>
Board of Directors Board of Directors
</Link> </Link>
@@ -110,7 +112,7 @@ const Navbar = () => {
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity" className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}
> >
Dashboard My Profile
</Link> </Link>
<Link <Link
to="/events" to="/events"
@@ -141,27 +143,42 @@ const Navbar = () => {
> >
Gallery Gallery
</Link> </Link>
<Link <DropdownMenu>
to="/members/newsletters" <DropdownMenuTrigger asChild>
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity" <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"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}>
>
Documents Documents
<ChevronDown className="h-4 w-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="bg-background min-w-[220px]">
<DropdownMenuItem asChild>
<Link to="/members/newsletters" className="w-full px-3 py-2 text-[var(--purple-deep)] hover:bg-[var(--lavender-300)] cursor-pointer"
style={{ fontFamily: "'Poppins', sans-serif" }}>
Newsletters
</Link> </Link>
<Link </DropdownMenuItem>
to="/profile" <DropdownMenuItem asChild>
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity" <Link to="/members/financials" className="w-full px-3 py-2 text-[var(--purple-deep)] hover:bg-[var(--lavender-300)] cursor-pointer"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}>
data-testid="profile-nav-button" Financials
>
Profile
</Link> </Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/members/bylaws" className="w-full px-3 py-2 text-[var(--purple-deep)] hover:bg-[var(--lavender-300)] cursor-pointer"
style={{ fontFamily: "'Poppins', sans-serif" }}>
Bylaws
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</nav> </nav>
{/* Mobile Hamburger Button */} {/* Mobile Hamburger Button */}
<button <button
onClick={() => setIsMobileMenuOpen(true)} onClick={() => setIsMobileMenuOpen(true)}
className="lg:hidden p-2 text-white hover:bg-white/10 rounded-lg transition-colors" className="lg:hidden p-2 text-white hover:bg-background/10 rounded-lg transition-colors"
aria-label="Open menu" aria-label="Open menu"
> >
<Menu className="h-6 w-6" /> <Menu className="h-6 w-6" />
@@ -178,7 +195,7 @@ const Navbar = () => {
/> />
{/* Drawer */} {/* Drawer */}
<div className="fixed right-0 top-0 h-full w-[280px] bg-gradient-to-b from-[#664fa3] to-[#48286e] shadow-2xl flex flex-col"> <div className="fixed right-0 top-0 h-full w-[280px] bg-gradient-to-b from-[var(--purple-lavender)] to-[var(--purple-deep)] shadow-2xl flex flex-col">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between p-6 border-b border-white/20"> <div className="flex items-center justify-between p-6 border-b border-white/20">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -189,7 +206,7 @@ const Navbar = () => {
</div> </div>
<button <button
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className="p-2 text-white hover:bg-white/10 rounded-lg transition-colors" className="p-2 text-white hover:bg-background/10 rounded-lg transition-colors"
aria-label="Close menu" aria-label="Close menu"
> >
<X className="h-6 w-6" /> <X className="h-6 w-6" />
@@ -209,12 +226,12 @@ const Navbar = () => {
)} )}
{/* Navigation Links */} {/* Navigation Links */}
<nav className="flex-1 overflow-y-auto py-6 px-4"> <nav className="flex-1 overflow-y-auto scrollbar-dashboard py-6 px-4">
<div className="space-y-2"> <div className="space-y-2">
<Link <Link
to="/" to="/"
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className="block px-4 py-3 text-white text-base font-medium hover:bg-white/10 rounded-lg transition-colors" className="block px-4 py-3 text-white text-base font-medium hover:bg-background/10 rounded-lg transition-colors"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}
> >
Home Home
@@ -228,7 +245,7 @@ const Navbar = () => {
<Link <Link
to="/about/history" to="/about/history"
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className="block px-6 py-2 text-white text-base hover:bg-white/10 rounded-lg transition-colors" className="block px-6 py-2 text-white text-base hover:bg-background/10 rounded-lg transition-colors"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}
> >
History History
@@ -236,7 +253,7 @@ const Navbar = () => {
<Link <Link
to="/about/mission-values" to="/about/mission-values"
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className="block px-6 py-2 text-white text-base hover:bg-white/10 rounded-lg transition-colors" className="block px-6 py-2 text-white text-base hover:bg-background/10 rounded-lg transition-colors"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}
> >
Mission and Values Mission and Values
@@ -244,7 +261,7 @@ const Navbar = () => {
<Link <Link
to="/about/board" to="/about/board"
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className="block px-6 py-2 text-white text-base hover:bg-white/10 rounded-lg transition-colors" className="block px-6 py-2 text-white text-base hover:bg-background/10 rounded-lg transition-colors"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}
> >
Board of Directors Board of Directors
@@ -254,7 +271,7 @@ const Navbar = () => {
<Link <Link
to="/dashboard" to="/dashboard"
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className="block px-4 py-3 text-white text-base font-medium hover:bg-white/10 rounded-lg transition-colors" className="block px-4 py-3 text-white text-base font-medium hover:bg-background/10 rounded-lg transition-colors"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}
> >
Dashboard Dashboard
@@ -263,7 +280,7 @@ const Navbar = () => {
<Link <Link
to="/events" to="/events"
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className="block px-4 py-3 text-white text-base font-medium hover:bg-white/10 rounded-lg transition-colors" className="block px-4 py-3 text-white text-base font-medium hover:bg-background/10 rounded-lg transition-colors"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}
data-testid="mobile-events-nav-button" data-testid="mobile-events-nav-button"
> >
@@ -273,7 +290,7 @@ const Navbar = () => {
<Link <Link
to="/members/calendar" to="/members/calendar"
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className="block px-4 py-3 text-white text-base font-medium hover:bg-white/10 rounded-lg transition-colors" className="block px-4 py-3 text-white text-base font-medium hover:bg-background/10 rounded-lg transition-colors"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}
> >
Calendar Calendar
@@ -282,7 +299,7 @@ const Navbar = () => {
<Link <Link
to="/members/directory" to="/members/directory"
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className="block px-4 py-3 text-white text-base font-medium hover:bg-white/10 rounded-lg transition-colors" className="block px-4 py-3 text-white text-base font-medium hover:bg-background/10 rounded-lg transition-colors"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}
> >
Directory Directory
@@ -291,25 +308,47 @@ const Navbar = () => {
<Link <Link
to="/members/gallery" to="/members/gallery"
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className="block px-4 py-3 text-white text-base font-medium hover:bg-white/10 rounded-lg transition-colors" className="block px-4 py-3 text-white text-base font-medium hover:bg-background/10 rounded-lg transition-colors"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}
> >
Gallery Gallery
</Link> </Link>
{/* Documents Section */}
<div className="space-y-1">
<p className="px-4 py-2 text-white/70 text-sm font-semibold uppercase tracking-wider" style={{ fontFamily: "'Poppins', sans-serif" }}>
Documents
</p>
<Link <Link
to="/members/newsletters" to="/members/newsletters"
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className="block px-4 py-3 text-white text-base font-medium hover:bg-white/10 rounded-lg transition-colors" className="block px-6 py-2 text-white text-base hover:bg-background/10 rounded-lg transition-colors"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}
> >
Documents Newsletters
</Link> </Link>
<Link
to="/members/financials"
onClick={() => setIsMobileMenuOpen(false)}
className="block px-6 py-2 text-white text-base hover:bg-background/10 rounded-lg transition-colors"
style={{ fontFamily: "'Poppins', sans-serif" }}
>
Financials
</Link>
<Link
to="/members/bylaws"
onClick={() => setIsMobileMenuOpen(false)}
className="block px-6 py-2 text-white text-base hover:bg-background/10 rounded-lg transition-colors"
style={{ fontFamily: "'Poppins', sans-serif" }}
>
Bylaws
</Link>
</div>
<Link <Link
to="/profile" to="/profile"
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className="block px-4 py-3 text-white text-base font-medium hover:bg-white/10 rounded-lg transition-colors" className="block px-4 py-3 text-white text-base font-medium hover:bg-background/10 rounded-lg transition-colors"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}
data-testid="mobile-profile-nav-button" data-testid="mobile-profile-nav-button"
> >
@@ -326,10 +365,10 @@ const Navbar = () => {
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
> >
<Button <Button
className="w-full bg-white/20 hover:bg-white/30 text-white rounded-lg" className="w-full bg-background/20 hover:bg-background/30 text-white rounded-lg"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}
> >
Admin Panel Dashboard
</Button> </Button>
</Link> </Link>
)} )}
@@ -339,7 +378,7 @@ const Navbar = () => {
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
> >
<Button <Button
className="w-full bg-[#ff9e77] hover:bg-[#ff8c64] text-[#48286e] rounded-lg font-semibold" className="w-full bg-[var(--orange-light)] hover:bg-[var(--orange-coral)] text-[var(--purple-deep)] rounded-lg font-semibold"
style={{ fontFamily: "'Montserrat', sans-serif" }} style={{ fontFamily: "'Montserrat', sans-serif" }}
> >
Donate Donate
@@ -352,7 +391,7 @@ const Navbar = () => {
handleLogout(); handleLogout();
}} }}
variant="outline" variant="outline"
className="w-full border-2 border-white/30 text-white hover:bg-white/10 rounded-lg" className="w-full border-2 border-white/30 text-white hover:bg-background/10 rounded-lg"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}
data-testid="mobile-logout-button" data-testid="mobile-logout-button"
> >

View File

@@ -156,13 +156,13 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
if (!user) return null; if (!user) return null;
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange} className="">
<DialogContent className="sm:max-w-[600px] bg-white rounded-2xl"> <DialogContent className="sm:max-w-[600px] bg-background rounded-2xl overflow-y-auto max-h-[90vh] p-6">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-2xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <DialogTitle className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Activate Manual Payment Activate Manual Payment
</DialogTitle> </DialogTitle>
<DialogDescription className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <DialogDescription className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Record offline payment for {user.first_name} {user.last_name} ({user.email}) Record offline payment for {user.first_name} {user.last_name} ({user.email})
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -170,7 +170,7 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
<form onSubmit={handleSubmit} className="space-y-6 py-4"> <form onSubmit={handleSubmit} className="space-y-6 py-4">
{/* Subscription Plan Selection */} {/* Subscription Plan Selection */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="plan_id" className="text-[#422268] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}> <Label htmlFor="plan_id" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
Subscription Plan Subscription Plan
</Label> </Label>
<Select <Select
@@ -187,7 +187,7 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
}); });
}} }}
> >
<SelectTrigger className="rounded-xl border-2 border-[#ddd8eb]"> <SelectTrigger className="rounded-xl border-2 border-[var(--neutral-800)]">
<SelectValue placeholder="Select subscription plan" /> <SelectValue placeholder="Select subscription plan" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -203,7 +203,7 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
</SelectContent> </SelectContent>
</Select> </Select>
{selectedPlan && ( {selectedPlan && (
<p className="text-xs text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-xs text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{selectedPlan.description || `${selectedPlan.billing_cycle} subscription`} {selectedPlan.description || `${selectedPlan.billing_cycle} subscription`}
</p> </p>
)} )}
@@ -211,7 +211,7 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
{/* Payment Amount */} {/* Payment Amount */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="amount" className="text-[#422268] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}> <Label htmlFor="amount" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
Payment Amount ($) Payment Amount ($)
</Label> </Label>
<Input <Input
@@ -222,11 +222,11 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
placeholder="Enter amount" placeholder="Enter amount"
value={formData.amount} value={formData.amount}
onChange={(e) => setFormData({ ...formData, amount: e.target.value })} onChange={(e) => setFormData({ ...formData, amount: e.target.value })}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
required required
/> />
{selectedPlan && ( {selectedPlan && (
<p className="text-xs text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-xs text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Minimum: {formatPrice(selectedPlan.minimum_price_cents || selectedPlan.price_cents || 3000)} Minimum: {formatPrice(selectedPlan.minimum_price_cents || selectedPlan.price_cents || 3000)}
</p> </p>
)} )}
@@ -234,14 +234,14 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
{/* Breakdown Display */} {/* Breakdown Display */}
{breakdown && breakdown.total >= breakdown.base && ( {breakdown && breakdown.total >= breakdown.base && (
<Card className="p-4 bg-[#f9f5ff] border border-[#DDD8EB]"> <Card className="p-4 bg-[var(--lavender-400)] border border-[var(--neutral-800)]">
<div className="space-y-2 text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <div className="space-y-2 text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<div className="flex justify-between text-[#422268]"> <div className="flex justify-between text-[var(--purple-ink)]">
<span>Membership Fee:</span> <span>Membership Fee:</span>
<span className="font-semibold">{formatPrice(breakdown.base)}</span> <span className="font-semibold">{formatPrice(breakdown.base)}</span>
</div> </div>
{breakdown.donation > 0 && ( {breakdown.donation > 0 && (
<div className="flex justify-between text-[#ff9e77]"> <div className="flex justify-between text-[var(--orange-light)]">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Heart className="h-4 w-4" /> <Heart className="h-4 w-4" />
Additional Donation: Additional Donation:
@@ -249,7 +249,7 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
<span className="font-semibold">{formatPrice(breakdown.donation)}</span> <span className="font-semibold">{formatPrice(breakdown.donation)}</span>
</div> </div>
)} )}
<div className="flex justify-between text-[#422268] font-bold text-base pt-2 border-t border-[#DDD8EB]"> <div className="flex justify-between text-[var(--purple-ink)] font-bold text-base pt-2 border-t border-[var(--neutral-800)]">
<span>Total:</span> <span>Total:</span>
<span>{formatPrice(breakdown.total)}</span> <span>{formatPrice(breakdown.total)}</span>
</div> </div>
@@ -259,17 +259,17 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
{/* Payment Date */} {/* Payment Date */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="payment_date" className="text-[#422268] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}> <Label htmlFor="payment_date" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
Payment Date Payment Date
</Label> </Label>
<div className="relative"> <div className="relative">
<Calendar className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-[#664fa3]" /> <Calendar className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-brand-purple " />
<Input <Input
id="payment_date" id="payment_date"
type="date" type="date"
value={formData.payment_date} value={formData.payment_date}
onChange={(e) => setFormData({ ...formData, payment_date: e.target.value })} onChange={(e) => setFormData({ ...formData, payment_date: e.target.value })}
className="pl-12 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="pl-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
required required
/> />
</div> </div>
@@ -277,14 +277,14 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
{/* Payment Method */} {/* Payment Method */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="payment_method" className="text-[#422268] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}> <Label htmlFor="payment_method" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
Payment Method Payment Method
</Label> </Label>
<Select <Select
value={formData.payment_method} value={formData.payment_method}
onValueChange={(value) => setFormData({ ...formData, payment_method: value })} onValueChange={(value) => setFormData({ ...formData, payment_method: value })}
> >
<SelectTrigger className="rounded-xl border-2 border-[#ddd8eb]"> <SelectTrigger className="rounded-xl border-2 border-[var(--neutral-800)]">
<SelectValue placeholder="Select payment method" /> <SelectValue placeholder="Select payment method" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -298,7 +298,7 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
{/* Subscription Period */} {/* Subscription Period */}
<div className="space-y-3"> <div className="space-y-3">
<Label className="text-[#422268] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>Subscription Period</Label> <Label className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>Subscription Period</Label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
@@ -306,9 +306,9 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
id="use_custom_period" id="use_custom_period"
checked={useCustomPeriod} checked={useCustomPeriod}
onChange={(e) => setUseCustomPeriod(e.target.checked)} onChange={(e) => setUseCustomPeriod(e.target.checked)}
className="rounded border-[#ddd8eb]" className="rounded border-[var(--neutral-800)]"
/> />
<Label htmlFor="use_custom_period" className="text-sm text-[#664fa3] font-normal cursor-pointer" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <Label htmlFor="use_custom_period" className="text-sm text-brand-purple font-normal cursor-pointer" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Use custom dates instead of plan's billing cycle Use custom dates instead of plan's billing cycle
</Label> </Label>
</div> </div>
@@ -316,7 +316,7 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
{useCustomPeriod ? ( {useCustomPeriod ? (
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="custom_period_start" className="text-sm text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <Label htmlFor="custom_period_start" className="text-sm text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Start Date Start Date
</Label> </Label>
<Input <Input
@@ -324,12 +324,12 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
type="date" type="date"
value={formData.custom_period_start} value={formData.custom_period_start}
onChange={(e) => setFormData({ ...formData, custom_period_start: e.target.value })} onChange={(e) => setFormData({ ...formData, custom_period_start: e.target.value })}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
required={useCustomPeriod} required={useCustomPeriod}
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="custom_period_end" className="text-sm text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <Label htmlFor="custom_period_end" className="text-sm text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
End Date End Date
</Label> </Label>
<Input <Input
@@ -337,18 +337,18 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
type="date" type="date"
value={formData.custom_period_end} value={formData.custom_period_end}
onChange={(e) => setFormData({ ...formData, custom_period_end: e.target.value })} onChange={(e) => setFormData({ ...formData, custom_period_end: e.target.value })}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
required={useCustomPeriod} required={useCustomPeriod}
/> />
</div> </div>
</div> </div>
) : ( ) : (
selectedPlan && ( selectedPlan && (
<div className="text-sm text-[#664fa3] bg-[#f1eef9] p-3 rounded-lg space-y-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <div className="text-sm text-brand-purple bg-[var(--lavender-300)] p-3 rounded-lg space-y-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{selectedPlan.custom_cycle_enabled ? ( {selectedPlan.custom_cycle_enabled ? (
<> <>
<p> <p>
<span className="font-medium text-[#422268]">Plan uses custom billing cycle:</span> <span className="font-medium text-[var(--purple-ink)]">Plan uses custom billing cycle:</span>
<br /> <br />
{(() => { {(() => {
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
@@ -378,7 +378,7 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
{/* Notes */} {/* Notes */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="notes" className="text-[#422268] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}> <Label htmlFor="notes" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
Notes (Optional) Notes (Optional)
</Label> </Label>
<Textarea <Textarea
@@ -386,7 +386,7 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
placeholder="Additional notes about the payment..." placeholder="Additional notes about the payment..."
value={formData.notes} value={formData.notes}
onChange={(e) => setFormData({ ...formData, notes: e.target.value })} onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3] min-h-[100px]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple min-h-[100px]"
/> />
</div> </div>
@@ -395,14 +395,14 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
type="button" type="button"
variant="outline" variant="outline"
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
className="rounded-full border-2 border-[#ddd8eb]" className="rounded-full border-2 border-[var(--neutral-800)]"
> >
Cancel Cancel
</Button> </Button>
<Button <Button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="bg-[#81B29A] text-white hover:bg-[#6FA087] rounded-full" className="bg-[var(--green-light)] text-white hover:bg-[var(--green-mint)] rounded-full"
> >
{loading ? 'Activating...' : 'Activate Payment'} {loading ? 'Activating...' : 'Activate Payment'}
</Button> </Button>

View File

@@ -73,9 +73,9 @@ const PendingInvitationsTable = () => {
const getRoleBadge = (role) => { const getRoleBadge = (role) => {
const config = { const config = {
superadmin: { label: 'Superadmin', className: 'bg-[#664fa3] text-white' }, superadmin: { label: 'Superadmin', className: 'bg-brand-purple text-white' },
admin: { label: 'Admin', className: 'bg-[#81B29A] text-white' }, admin: { label: 'Admin', className: 'bg-[var(--green-light)] text-white' },
member: { label: 'Member', className: 'bg-[#DDD8EB] text-[#422268]' } member: { label: 'Member', className: 'bg-[var(--neutral-800)] text-[var(--purple-ink)]' }
}; };
const roleConfig = config[role] || { label: role, className: 'bg-gray-500 text-white' }; const roleConfig = config[role] || { label: role, className: 'bg-gray-500 text-white' };
@@ -111,7 +111,7 @@ const PendingInvitationsTable = () => {
if (loading) { if (loading) {
return ( return (
<div className="text-center py-8"> <div className="text-center py-8">
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Loading invitations... Loading invitations...
</p> </p>
</div> </div>
@@ -120,12 +120,12 @@ const PendingInvitationsTable = () => {
if (invitations.length === 0) { if (invitations.length === 0) {
return ( return (
<Card className="p-12 bg-white rounded-2xl border border-[#ddd8eb] text-center"> <Card className="p-12 bg-background rounded-2xl border border-[var(--neutral-800)] text-center">
<Mail className="h-16 w-16 text-[#ddd8eb] mx-auto mb-4" /> <Mail className="h-16 w-16 text-[var(--neutral-800)] mx-auto mb-4" />
<h3 className="text-xl font-semibold text-[#422268] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}> <h3 className="text-xl font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
No Pending Invitations No Pending Invitations
</h3> </h3>
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
All invitations have been accepted or expired All invitations have been accepted or expired
</p> </p>
</Card> </Card>
@@ -134,37 +134,37 @@ const PendingInvitationsTable = () => {
return ( return (
<> <>
<Card className="bg-white rounded-2xl border border-[#ddd8eb] overflow-hidden"> <Card className="bg-background rounded-2xl border border-[var(--neutral-800)] overflow-hidden">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="bg-[#DDD8EB] hover:bg-[#DDD8EB]"> <TableRow className="bg-[var(--neutral-800)] hover:bg-[var(--neutral-800)]">
<TableHead className="text-[#422268] font-semibold">Email</TableHead> <TableHead className="text-[var(--purple-ink)] font-semibold">Email</TableHead>
<TableHead className="text-[#422268] font-semibold">Name</TableHead> <TableHead className="text-[var(--purple-ink)] font-semibold">Name</TableHead>
<TableHead className="text-[#422268] font-semibold">Role</TableHead> <TableHead className="text-[var(--purple-ink)] font-semibold">Role</TableHead>
<TableHead className="text-[#422268] font-semibold">Invited</TableHead> <TableHead className="text-[var(--purple-ink)] font-semibold">Invited</TableHead>
<TableHead className="text-[#422268] font-semibold">Expires</TableHead> <TableHead className="text-[var(--purple-ink)] font-semibold">Expires</TableHead>
<TableHead className="text-[#422268] font-semibold text-right">Actions</TableHead> <TableHead className="text-[var(--purple-ink)] font-semibold text-right">Actions</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{invitations.map((invitation) => ( {invitations.map((invitation) => (
<TableRow key={invitation.id} className="hover:bg-[#F9F8FB]"> <TableRow key={invitation.id} className="hover:bg-[var(--lavender-700)]">
<TableCell className="font-medium text-[#422268]"> <TableCell className="font-medium text-[var(--purple-ink)]">
{invitation.email} {invitation.email}
</TableCell> </TableCell>
<TableCell className="text-[#664fa3]"> <TableCell className="text-brand-purple ">
{invitation.first_name && invitation.last_name {invitation.first_name && invitation.last_name
? `${invitation.first_name} ${invitation.last_name}` ? `${invitation.first_name} ${invitation.last_name}`
: '-'} : '-'}
</TableCell> </TableCell>
<TableCell>{getRoleBadge(invitation.role)}</TableCell> <TableCell>{getRoleBadge(invitation.role)}</TableCell>
<TableCell className="text-[#664fa3]"> <TableCell className="text-brand-purple ">
{new Date(invitation.invited_at).toLocaleDateString()} {new Date(invitation.invited_at).toLocaleDateString()}
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Clock className={`h-4 w-4 ${isExpiringSoon(invitation.expires_at) ? 'text-orange-500' : 'text-[#664fa3]'}`} /> <Clock className={`h-4 w-4 ${isExpiringSoon(invitation.expires_at) ? 'text-orange-500' : 'text-brand-purple '}`} />
<span className={`text-sm ${isExpiringSoon(invitation.expires_at) ? 'text-orange-500 font-semibold' : 'text-[#664fa3]'}`}> <span className={`text-sm ${isExpiringSoon(invitation.expires_at) ? 'text-orange-500 font-semibold' : 'text-brand-purple '}`}>
{formatDate(invitation.expires_at)} {formatDate(invitation.expires_at)}
</span> </span>
</div> </div>
@@ -176,7 +176,7 @@ const PendingInvitationsTable = () => {
size="sm" size="sm"
onClick={() => handleResend(invitation.id)} onClick={() => handleResend(invitation.id)}
disabled={resending === invitation.id} disabled={resending === invitation.id}
className="rounded-xl border-[#81B29A] text-[#81B29A] hover:bg-[#81B29A] hover:text-white" className="rounded-xl border-[var(--green-light)] text-[var(--green-light)] hover:bg-[var(--green-light)] hover:text-white"
> >
{resending === invitation.id ? ( {resending === invitation.id ? (
'Resending...' 'Resending...'
@@ -208,10 +208,10 @@ const PendingInvitationsTable = () => {
<AlertDialog open={revokeDialog.open} onOpenChange={(open) => setRevokeDialog({ open, invitation: null })}> <AlertDialog open={revokeDialog.open} onOpenChange={(open) => setRevokeDialog({ open, invitation: null })}>
<AlertDialogContent className="rounded-2xl"> <AlertDialogContent className="rounded-2xl">
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle className="text-2xl text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <AlertDialogTitle className="text-2xl text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Revoke Invitation Revoke Invitation
</AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <AlertDialogDescription className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Are you sure you want to revoke the invitation for{' '} Are you sure you want to revoke the invitation for{' '}
<span className="font-semibold">{revokeDialog.invitation?.email}</span>? <span className="font-semibold">{revokeDialog.invitation?.email}</span>?
This action cannot be undone. This action cannot be undone.

View File

@@ -159,12 +159,12 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto"> <DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto scrollbar-dashboard">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-2xl text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <DialogTitle className="text-2xl text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
{plan ? 'Edit Plan' : 'Create New Plan'} {plan ? 'Edit Plan' : 'Create New Plan'}
</DialogTitle> </DialogTitle>
<DialogDescription className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <DialogDescription className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{plan ? 'Update plan details below' : 'Enter plan details to create a new subscription plan'} {plan ? 'Update plan details below' : 'Enter plan details to create a new subscription plan'}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -197,8 +197,8 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
</div> </div>
{/* Dynamic Pricing */} {/* Dynamic Pricing */}
<div className="border-2 border-[#DDD8EB] rounded-lg p-4 space-y-4"> <div className="border-2 border-[var(--neutral-800)] rounded-lg p-4 space-y-4">
<h3 className="font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <h3 className="font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Dynamic Pricing Dynamic Pricing
</h3> </h3>
@@ -216,7 +216,7 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
required required
className="mt-2" className="mt-2"
/> />
<p className="text-xs text-[#664fa3] mt-1">Minimum $30</p> <p className="text-xs text-brand-purple mt-1">Minimum $30</p>
</div> </div>
<div> <div>
@@ -232,7 +232,7 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
required required
className="mt-2" className="mt-2"
/> />
<p className="text-xs text-[#664fa3] mt-1">Pre-filled amount</p> <p className="text-xs text-brand-purple mt-1">Pre-filled amount</p>
</div> </div>
</div> </div>
@@ -240,7 +240,7 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
<div className="flex items-center justify-between pt-2"> <div className="flex items-center justify-between pt-2">
<div> <div>
<Label htmlFor="allow_donation">Allow Donations</Label> <Label htmlFor="allow_donation">Allow Donations</Label>
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Members can pay more than minimum Members can pay more than minimum
</p> </p>
</div> </div>
@@ -252,7 +252,7 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
onChange={(e) => setFormData({ ...formData, allow_donation: e.target.checked })} onChange={(e) => setFormData({ ...formData, allow_donation: e.target.checked })}
className="sr-only peer" className="sr-only peer"
/> />
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[#664fa3]/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[#81B29A]"></div> <div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand-purple /20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-background after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--green-light)]"></div>
</label> </label>
</div> </div>
</div> </div>
@@ -279,11 +279,11 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
{/* Custom Billing Cycle Dates */} {/* Custom Billing Cycle Dates */}
{formData.billing_cycle === 'custom' && ( {formData.billing_cycle === 'custom' && (
<div className="border-2 border-[#DDD8EB] rounded-lg p-4 space-y-4"> <div className="border-2 border-[var(--neutral-800)] rounded-lg p-4 space-y-4">
<h3 className="font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <h3 className="font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Custom Billing Period Custom Billing Period
</h3> </h3>
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Set recurring date range (e.g., Jan 1 - Dec 31 for calendar year) Set recurring date range (e.g., Jan 1 - Dec 31 for calendar year)
</p> </p>
@@ -349,8 +349,8 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
</div> </div>
</div> </div>
<div className="bg-[#f9f5ff] border border-[#DDD8EB] rounded p-3"> <div className="bg-[var(--lavender-400)] border border-[var(--neutral-800)] rounded p-3">
<p className="text-sm text-[#422268]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-sm text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<strong>Example:</strong> Jan 1 - Dec 31 for calendar year, or Jul 1 - Jun 30 for fiscal year <strong>Example:</strong> Jan 1 - Dec 31 for calendar year, or Jul 1 - Jun 30 for fiscal year
</p> </p>
</div> </div>
@@ -361,7 +361,7 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<Label htmlFor="active">Active Status</Label> <Label htmlFor="active">Active Status</Label>
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Inactive plans won't appear for new subscriptions Inactive plans won't appear for new subscriptions
</p> </p>
</div> </div>
@@ -373,7 +373,7 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
onChange={(e) => setFormData({ ...formData, active: e.target.checked })} onChange={(e) => setFormData({ ...formData, active: e.target.checked })}
className="sr-only peer" className="sr-only peer"
/> />
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[#664fa3]/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[#81B29A]"></div> <div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand-purple /20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-background after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--green-light)]"></div>
</label> </label>
</div> </div>
@@ -389,7 +389,7 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
<Button <Button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="bg-[#DDD8EB] text-[#422268] hover:bg-white" className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background"
> >
{loading ? ( {loading ? (
<> <>

View File

@@ -1,44 +1,48 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Button } from './ui/button'; import { Button } from './ui/button';
import { useThemeConfig } from '../context/ThemeConfigContext';
const PublicFooter = () => { const PublicFooter = () => {
const loafLogo = `${process.env.PUBLIC_URL}/loaf-logo.png`; const { getLogoUrl } = useThemeConfig();
const loafLogo = getLogoUrl();
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-[var(--purple-amethyst)] 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-[var(--neutral-800)] 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-[var(--neutral-800)] 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-[var(--neutral-800)] 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-[var(--neutral-800)] 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-[var(--neutral-800)] 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-[var(--neutral-800)] 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-[var(--orange-light)] hover:bg-[var(--orange-coral)] text-[var(--purple-deep)] 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-[var(--neutral-800)] 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 +51,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-[var(--purple-deep)] to-[var(--purple-amethyst)] 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-[var(--neutral-500)] 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-[var(--neutral-500)] 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-[var(--neutral-500)] 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. © {new Date().getFullYear()} 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-[var(--neutral-500)] 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>

View File

@@ -1,7 +1,8 @@
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 { useThemeConfig } from '../context/ThemeConfigContext';
import { ChevronDown, Menu, X } from 'lucide-react'; import { ChevronDown, Menu, X } from 'lucide-react';
import { import {
DropdownMenu, DropdownMenu,
@@ -12,11 +13,27 @@ import {
const PublicNavbar = () => { const PublicNavbar = () => {
const { user, logout } = useAuth(); const { user, logout } = useAuth();
const { getLogoUrl } = useThemeConfig();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
// LOAF logo (local) // Helper function to check if a path is active
const loafLogo = `${process.env.PUBLIC_URL}/loaf-logo.png`; 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');
};
// Get logo URL from theme config (with fallback to default)
const loafLogo = getLogoUrl();
const handleAuthAction = () => { const handleAuthAction = () => {
if (user) { if (user) {
@@ -27,14 +44,61 @@ 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-[var(--orange-light)] hover:text-[var(--orange-coral)] `;
}
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-[var(--orange-light)] hover:bg-[var(--orange-coral)] text-[var(--purple-deep)]`;
}
return `${baseClasses} text-white hover:bg-[var(--purple-deep)]`;
};
// 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-[var(--orange-light)] hover:bg-[var(--orange-coral)] text-[var(--purple-deep)]`;
}
return `${baseClasses} text-[var(--neutral-800)] hover:bg-[var(--purple-deep)] 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-50'>
<header className="bg-gradient-to-r flex-wrap from-[var(--purple-amethyst)] to-[var(--purple-deep)] 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 items-center'>
{user && (
<span
className="text-white text-base font-medium"
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
Welcome, {user.first_name}
</span>
)}
{(user?.role === 'admin' || user?.role === 'superadmin') && (
<Link
to="/admin"
className="text-white text-base font-medium hover:opacity-80 transition-opacity"
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
Dashboard
</Link>
)}
<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 +106,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-[var(--orange-light)] hover:bg-[var(--orange-coral)] text-[var(--purple-deep)] 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 +123,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-brand-purple 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>
@@ -66,45 +131,48 @@ const PublicNavbar = () => {
{/* Mobile Menu Button */} {/* Mobile Menu Button */}
<button <button
onClick={() => setIsMobileMenuOpen(true)} onClick={() => setIsMobileMenuOpen(true)}
className="lg:hidden p-2 text-white hover:bg-[#48286e] rounded-md transition-colors" className="lg:hidden p-2 text-white hover:bg-[var(--purple-deep)] 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-[var(--orange-light)] hover:text-[var(--orange-coral)]"
: "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>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="bg-white min-w-[220px]"> <DropdownMenuContent align="start" className="bg-background 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-[var(--purple-deep)] hover:bg-[var(--lavender-300)] 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-[var(--purple-deep)] hover:bg-[var(--lavender-300)] 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-[var(--purple-deep)] hover:bg-[var(--lavender-300)] 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 +180,102 @@ 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 ? 'My Profile' : '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>
)} )}
{user && (
<>
<Link <Link
to="/events"
className={getDesktopLinkClasses('/events')}
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
Events
</Link>
<Link
to="/members/calendar"
className={getDesktopLinkClasses('/members/calendar')}
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
Calendar
</Link>
<Link
to="/members/directory"
className={getDesktopLinkClasses('/members/directory')}
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
Directory
</Link>
<Link
to="/members/gallery"
className={getDesktopLinkClasses('/members/gallery')}
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
Gallery
</Link>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className={`${location.pathname.startsWith('/members/newsletters') || location.pathname.startsWith('/members/financials') || location.pathname.startsWith('/members/bylaws')
? "text-[var(--orange-light)] hover:text-[var(--orange-coral)]"
: "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" }}>
Documents
<ChevronDown className="h-4 w-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="bg-background min-w-[220px]">
<DropdownMenuItem asChild>
<Link to="/members/newsletters" className="w-full px-3 py-2 text-[var(--purple-deep)] hover:bg-[var(--lavender-300)] cursor-pointer"
style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Newsletters
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/members/financials" className="w-full px-3 py-2 text-[var(--purple-deep)] hover:bg-[var(--lavender-300)] cursor-pointer"
style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Financials
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/members/bylaws" className="w-full px-3 py-2 text-[var(--purple-deep)] hover:bg-[var(--lavender-300)] cursor-pointer"
style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Bylaws
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
)}
{/* <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">
@@ -153,58 +286,73 @@ const PublicNavbar = () => {
/> />
{/* Drawer */} {/* Drawer */}
<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-brand-purple shadow-xl overflow-y-auto scrollbar-dashboard">
{/* 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-[var(--purple-deep)]">
<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
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className="p-2 text-white hover:bg-[#48286e] rounded-md transition-colors" className="p-2 text-white hover:bg-[var(--purple-deep)] rounded-md transition-colors"
aria-label="Close menu" aria-label="Close menu"
> >
<X className="h-5 w-5" /> <X className="h-5 w-5" />
</button> </button>
</div> </div>
{/* User Info */}
{user && (
<div className="px-6 py-4 border-b border-[var(--purple-deep)]">
<p className="text-white text-sm opacity-90" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Welcome,
</p>
<p className="text-white font-semibold text-base" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{user.first_name} {user.last_name}
</p>
</div>
)}
{/* Navigation Links */} {/* Navigation Links */}
<nav className="flex flex-col p-6 space-y-4"> <nav className="flex flex-col p-6 space-y-4">
<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-[var(--orange-light)]' : '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,28 +361,102 @@ 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 ? 'My Profile' : 'Become a Member'}
</Link> </Link>
{!user && ( {!user && (
<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>
)} )}
{user && (
<>
<Link
to="/events"
onClick={() => setIsMobileMenuOpen(false)}
className={getMobileLinkClasses('/events')}
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
Events
</Link>
<Link
to="/members/calendar"
onClick={() => setIsMobileMenuOpen(false)}
className={getMobileLinkClasses('/members/calendar')}
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
Calendar
</Link>
<Link
to="/members/directory"
onClick={() => setIsMobileMenuOpen(false)}
className={getMobileLinkClasses('/members/directory')}
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
Directory
</Link>
<Link
to="/members/gallery"
onClick={() => setIsMobileMenuOpen(false)}
className={getMobileLinkClasses('/members/gallery')}
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
Gallery
</Link>
{/* Documents Section */}
<div className="space-y-2">
<p
className={`text-base font-semibold px-4 py-2 rounded-md ${location.pathname.startsWith('/members/newsletters') || location.pathname.startsWith('/members/financials') || location.pathname.startsWith('/members/bylaws') ? 'text-[var(--orange-light)]' : 'text-white'}`}
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
Documents
</p>
<Link
to="/members/newsletters"
onClick={() => setIsMobileMenuOpen(false)}
className={getMobileSubLinkClasses('/members/newsletters')}
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
Newsletters
</Link>
<Link
to="/members/financials"
onClick={() => setIsMobileMenuOpen(false)}
className={getMobileSubLinkClasses('/members/financials')}
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
Financials
</Link>
<Link
to="/members/bylaws"
onClick={() => setIsMobileMenuOpen(false)}
className={getMobileSubLinkClasses('/members/bylaws')}
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
Bylaws
</Link>
</div>
</>
)}
<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,21 +464,31 @@ 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>
{/* Auth Actions */} {/* Auth Actions */}
<div className="pt-4 border-t border-[#48286e] space-y-2"> <div className="pt-4 border-t border-[var(--purple-deep)] space-y-2">
{(user?.role === 'admin' || user?.role === 'superadmin') && (
<Link
to="/admin"
onClick={() => setIsMobileMenuOpen(false)}
className="block text-white text-base font-medium hover:bg-[var(--purple-deep)] px-4 py-3 rounded-md transition-colors"
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
Dashboard
</Link>
)}
<button <button
onClick={() => { onClick={() => {
handleAuthAction(); handleAuthAction();
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-[var(--purple-deep)] 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>
@@ -264,8 +496,8 @@ const PublicNavbar = () => {
<Link <Link
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-[var(--purple-deep)] px-4 py-3 rounded-md transition-colors"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
> >
Register Register
</Link> </Link>
@@ -275,7 +507,7 @@ const PublicNavbar = () => {
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className="block w-full" className="block w-full"
> >
<Button className="w-full bg-[#ff9e77] hover:bg-[#ff8c64] text-[#48286e] rounded-[25px] px-6 py-3 text-base font-semibold"> <Button className="w-full bg-[var(--orange-light)] hover:bg-[var(--orange-coral)] text-[var(--purple-deep)] rounded-[25px] px-6 py-3 text-base font-semibold">
Donate Donate
</Button> </Button>
</Link> </Link>

View File

@@ -0,0 +1,107 @@
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-[var(--neutral-800)]">
<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-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Reject Application
</DialogTitle>
</div>
<DialogDescription className="text-brand-purple " 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-[var(--lavender-400)] border border-[var(--neutral-800)] rounded-lg p-4">
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<strong>Applicant:</strong> {user?.email}
</p>
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<strong>Status:</strong> {user?.status}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="reason" className="text-[var(--purple-ink)] 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-[var(--neutral-800)] focus:border-red-500 min-h-[120px]"
disabled={loading}
/>
{error && (
<p className="text-sm text-red-500">{error}</p>
)}
<p className="text-xs text-brand-purple " 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-[var(--neutral-800)] text-brand-purple hover:bg-[var(--lavender-300)] rounded-full px-6"
disabled={loading}
>
<X className="h-4 w-4 mr-2" />
Cancel
</Button>
<Button
type="button"
onClick={handleSubmit}
className="bg-red-600 text-white hover:bg-red-700 rounded-full px-6"
disabled={loading}
>
<AlertTriangle className="h-4 w-4 mr-2" />
{loading ? 'Rejecting...' : 'Confirm Rejection'}
</Button>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,45 @@
import React from 'react';
import { NavLink, useLocation } from 'react-router-dom';
import { CreditCard, Shield, Star, Palette } from 'lucide-react';
const settingsItems = [
{ label: 'Stripe', path: '/admin/settings/stripe', icon: CreditCard },
{ label: 'Permissions', path: '/admin/settings/permissions', icon: Shield },
{ label: 'Member Tiers', path: '/admin/settings/member-tiers', icon: Star },
{ label: 'Theme', path: '/admin/settings/theme', icon: Palette },
];
const SettingsTabs = () => {
const location = useLocation();
return (
<div className="w-full border-b border-border">
<nav className="flex gap-1 overflow-x-auto pb-px -mb-px" aria-label="Settings tabs">
{settingsItems.map((item) => {
const Icon = item.icon;
const isActive = location.pathname === item.path;
return (
<NavLink
key={item.label}
to={item.path}
className={`
flex items-center gap-2 px-4 py-3 text-sm font-medium whitespace-nowrap
border-b-2 transition-all duration-200
${isActive
? 'border-primary text-primary bg-primary/5'
: 'border-transparent text-muted-foreground hover:text-foreground hover:border-border'
}
`}
>
<Icon className={`h-4 w-4 ${isActive ? 'text-primary' : ''}`} />
<span>{item.label}</span>
</NavLink>
);
})}
</nav>
</div>
);
};
export default SettingsTabs;

View File

@@ -0,0 +1,66 @@
import React from "react";
import { Card } from "./ui/card";
export const StatCard = ({
title,
value,
icon: Icon,
iconBgClass,
dataTestId,
}) => {
const valueString = value == null ? "" : String(value);
const digitCount =
valueString.replace(/\D/g, "").length || valueString.length;
const getValueFontSize = () => {
switch (true) {
case digitCount <= 2:
// 3.75rem for 3 or fewer digits
return "3.75rem";
case digitCount <= 6:
// Scale down for more digits
return "clamp(2rem, 5cqi, 3rem)";
case digitCount <= 9:
return "clamp(1.5rem, 4cqi, 2.5rem)";
default:
return "clamp(1.25rem, 3cqi, 2rem)";
}
};
const valueFontSize = getValueFontSize();
return (
<Card
className="p-6 flex flex-col justify-between bg-background rounded-2xl border border-[var(--neutral-800)]"
data-testid={dataTestId}
>
<div className="flex items-start gap-4 mb-4 justify-between">
<div
className="space-y-8 "
style={{
containerType: "inline-size",
maxWidth: "200px",
width: "100%",
}}
>
<p
className="font-semibold text-[var(--purple-ink)] mb-1"
style={{ fontSize: valueFontSize, lineHeight: 1 }}
>
{value}
</p>
</div>
<div className={`${iconBgClass} px-3 py-2 rounded-lg `}>
<Icon className="size-[valueFontSize]" />
</div>
</div>
<p
className="text-sm text-brand-purple "
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
{title}
</p>
</Card>
);
};

View File

@@ -0,0 +1,46 @@
import React from 'react';
import { Badge } from './ui/badge';
const STATUS_BADGE_CONFIG = {
//status-based badges
pending_email: { label: 'Pending Email', variant: 'orange2' },
pending_validation: { label: 'Pending Validation', variant: 'gray' },
payment_pending: { label: 'Payment Pending', variant: 'orange' },
active: { label: 'Active', variant: 'green' },
inactive: { label: 'Inactive', variant: 'gray2' },
canceled: { label: 'Canceled', variant: 'red' },
expired: { label: 'Expired', variant: 'red2' },
abandoned: { label: 'Abandoned', variant: 'gray3' },
rejected: { label: 'Rejected', className: 'bg-red-100 text-red-700' },
//role-based badges
finance: { label: 'Finance Manager', variant: 'purple' },
guest: { label: 'Guest', variant: 'gray' },
member: { label: 'Member', variant: 'purple' },
superadmin: { label: 'Superadmin', variant: 'purple' },
admin: { label: 'Admin', variant: 'purple' },
moderator: { label: 'Moderator', variant: 'bg-[var(--neutral-800)] text-[var(--purple-ink)]' },
staff: { label: 'Staff', variant: 'gray' },
media: { label: 'Media', variant: 'gray2' },
//donation badges
pending: { label: 'Payment Pending', variant: 'orange' },
completed: { label: 'Completed', variant: 'green' },
failed: { label: 'Failed', className: 'bg-red-100 text-red-700' }
};
//todo: make shield icon dynamic based on status
const StatusBadge = ({ status }) => {
const statusConfig = STATUS_BADGE_CONFIG[status] || STATUS_BADGE_CONFIG.inactive;
return (
<Badge variant={statusConfig.variant} className=" px-3 py-1 rounded-md text-sm">
{/* <Shield className="h-3 w-3 mr-1 inline" /> */}
{statusConfig.label}
</Badge>
);
};
export default StatusBadge;

View File

@@ -0,0 +1,252 @@
import React, { useState } from 'react';
import { Card } from './ui/card';
import { Badge } from './ui/badge';
import { Button } from './ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
import { Receipt, CreditCard, Heart, Calendar, ExternalLink, DollarSign } from 'lucide-react';
/**
* TransactionHistory Component
* Displays user transaction history including subscriptions and donations
*
* @param {Object} props
* @param {Array} props.subscriptions - List of subscription transactions
* @param {Array} props.donations - List of donation transactions
* @param {number} props.totalSubscriptionCents - Total subscription amount in cents
* @param {number} props.totalDonationCents - Total donation amount in cents
* @param {boolean} props.loading - Loading state
* @param {boolean} props.isAdmin - Whether viewing as admin (shows extra fields)
*/
const TransactionHistory = ({
subscriptions = [],
donations = [],
totalSubscriptionCents = 0,
totalDonationCents = 0,
loading = false,
isAdmin = false
}) => {
const [activeTab, setActiveTab] = useState('all');
const formatAmount = (cents) => {
if (!cents) return '$0.00';
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'
});
};
const getStatusBadgeClass = (status) => {
switch (status?.toLowerCase()) {
case 'active':
case 'completed':
return 'bg-green-100 text-green-800 border-green-200';
case 'pending':
return 'bg-yellow-100 text-yellow-800 border-yellow-200';
case 'cancelled':
case 'failed':
case 'expired':
return 'bg-red-100 text-red-800 border-red-200';
default:
return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
const allTransactions = [
...subscriptions.map(s => ({ ...s, sortDate: s.created_at })),
...donations.map(d => ({ ...d, sortDate: d.created_at }))
].sort((a, b) => new Date(b.sortDate) - new Date(a.sortDate));
const TransactionRow = ({ transaction }) => {
const isSubscription = transaction.type === 'subscription';
return (
<div className="flex flex-col sm:flex-row sm:items-center justify-between p-4 border-b border-[var(--neutral-800)] last:border-b-0 hover:bg-[var(--lavender-500)] transition-colors">
<div className="flex items-start gap-3 mb-2 sm:mb-0">
<div className={`p-2 rounded-lg ${isSubscription ? 'bg-[var(--purple-lavender)] bg-opacity-20' : 'bg-[var(--orange-light)] bg-opacity-20'}`}>
{isSubscription ? (
<CreditCard className="h-5 w-5 text-white" />
) : (
<Heart className="h-5 w-5 text-white" />
)}
</div>
<div className="flex-1">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-medium text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{transaction.description}
</span>
<Badge className={`text-xs ${getStatusBadgeClass(transaction.status)}`}>
{transaction.status}
</Badge>
</div>
<div className="flex items-center gap-2 text-sm text-brand-purple mt-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<Calendar className="h-3 w-3" />
<span>{formatDate(transaction.payment_completed_at || transaction.created_at)}</span>
{transaction.card_brand && transaction.card_last4 && (
<>
<span className="text-[var(--neutral-800)]"></span>
<span>{transaction.card_brand} ****{transaction.card_last4}</span>
</>
)}
{isSubscription && transaction.billing_cycle && (
<>
<span className="text-[var(--neutral-800)]"></span>
<span className="capitalize">{transaction.billing_cycle}</span>
</>
)}
</div>
{isAdmin && transaction.manual_payment && (
<div className="text-xs text-[var(--orange-light)] mt-1">
Manual Payment {transaction.manual_payment_notes && `- ${transaction.manual_payment_notes}`}
</div>
)}
</div>
</div>
<div className="flex items-center gap-3 pl-10 sm:pl-0">
<div className="text-right">
<div className="font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
{formatAmount(transaction.amount_cents)}
</div>
{isSubscription && transaction.donation_cents > 0 && (
<div className="text-xs text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
(incl. {formatAmount(transaction.donation_cents)} donation)
</div>
)}
</div>
{transaction.stripe_receipt_url && (
<a
href={transaction.stripe_receipt_url}
target="_blank"
rel="noopener noreferrer"
className="p-2 text-brand-purple hover:text-[var(--purple-ink)] hover:bg-[var(--lavender-300)] rounded-lg transition-colors"
title="View Receipt"
>
<ExternalLink className="h-4 w-4" />
</a>
)}
</div>
</div>
);
};
const EmptyState = ({ type }) => (
<div className="py-12 text-center">
<div className="mx-auto w-16 h-16 bg-[var(--lavender-300)] rounded-full flex items-center justify-center mb-4">
{type === 'subscription' ? (
<CreditCard className="h-8 w-8 text-brand-purple" />
) : type === 'donation' ? (
<Heart className="h-8 w-8 text-brand-purple" />
) : (
<Receipt className="h-8 w-8 text-brand-purple" />
)}
</div>
<p className="text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{type === 'subscription'
? 'No subscription payments yet'
: type === 'donation'
? 'No donations yet'
: 'No transactions yet'}
</p>
</div>
);
if (loading) {
return (
<Card className="p-8 bg-background rounded-2xl border border-[var(--neutral-800)]">
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[var(--purple-lavender)]"></div>
</div>
</Card>
);
}
return (
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<Receipt className="h-6 w-6 text-brand-purple" />
Transaction History
</h2>
</div>
{/* Summary Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6">
<div className="p-4 bg-[var(--lavender-500)] rounded-xl border border-[var(--neutral-800)]">
<div className="flex items-center gap-2 text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<CreditCard className="h-4 w-4" />
<span className="text-sm">Total Subscriptions</span>
</div>
<div className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
{formatAmount(totalSubscriptionCents)}
</div>
<div className="text-xs text-brand-purple mt-1">{subscriptions.length} payment(s)</div>
</div>
<div className="p-4 bg-[var(--lavender-500)] rounded-xl border border-[var(--neutral-800)]">
<div className="flex items-center gap-2 text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<Heart className="h-4 w-4" />
<span className="text-sm">Total Donations</span>
</div>
<div className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
{formatAmount(totalDonationCents)}
</div>
<div className="text-xs text-brand-purple mt-1">{donations.length} donation(s)</div>
</div>
</div>
{/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-3 mb-4">
<TabsTrigger value="all" className="data-[state=active]:bg-[var(--purple-lavender)] data-[state=active]:text-white">
All ({allTransactions.length})
</TabsTrigger>
<TabsTrigger value="subscriptions" className="data-[state=active]:bg-[var(--purple-lavender)] data-[state=active]:text-white">
Subscriptions ({subscriptions.length})
</TabsTrigger>
<TabsTrigger value="donations" className="data-[state=active]:bg-[var(--purple-lavender)] data-[state=active]:text-white">
Donations ({donations.length})
</TabsTrigger>
</TabsList>
<div className="border border-[var(--neutral-800)] rounded-xl overflow-hidden">
<TabsContent value="all" className="m-0">
{allTransactions.length > 0 ? (
allTransactions.map((transaction) => (
<TransactionRow key={transaction.id} transaction={transaction} />
))
) : (
<EmptyState type="all" />
)}
</TabsContent>
<TabsContent value="subscriptions" className="m-0">
{subscriptions.length > 0 ? (
subscriptions.map((transaction) => (
<TransactionRow key={transaction.id} transaction={transaction} />
))
) : (
<EmptyState type="subscription" />
)}
</TabsContent>
<TabsContent value="donations" className="m-0">
{donations.length > 0 ? (
donations.map((transaction) => (
<TransactionRow key={transaction.id} transaction={transaction} />
))
) : (
<EmptyState type="donation" />
)}
</TabsContent>
</div>
</Tabs>
</Card>
);
};
export default TransactionHistory;

View File

@@ -0,0 +1,539 @@
import React, { useEffect, useRef, useState } from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from './ui/dialog';
import { Button } from './ui/button';
import { Card } from './ui/card';
import { Checkbox } from './ui/checkbox';
import { Input } from './ui/input';
import { Label } from './ui/label';
import { Textarea } from './ui/textarea';
import { User, Mail, Phone, Calendar, UserCheck, Clock, FileText } from 'lucide-react';
import StatusBadge from './StatusBadge';
import api from '../utils/api';
import { toast } from 'sonner';
const ViewRegistrationDialog = ({ open, onOpenChange, user }) => {
if (!user) return null;
const [formData, setFormData] = useState(null);
const [isSaving, setIsSaving] = useState(false);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const autoSaveTimeoutRef = useRef(null);
const pendingSaveRef = useRef(false);
const leadSourceOptions = [
'Current member',
'Friend',
'OutSmart Magazine',
'Search engine (Google etc.)',
"I've known about LOAF for a long time",
'Other'
];
const volunteerOptions = [
'Welcoming new members at events',
'Sending out birthday cards',
'Care Team Calls',
'Sharing ideas for events',
'Researching grants',
'Applying for grants',
'Assisting with TeatherLOAFers',
'Assisting with ActiveLOAFers',
'Assisting with weekday Lunch Bunch',
'Uploading Photos to the Website',
'Assisting with eNewsletter',
'Other administrative task'
];
useEffect(() => {
if (!open || !user) return;
const nextFormData = {
lead_sources: Array.isArray(user.lead_sources) ? user.lead_sources : [],
partner_first_name: user.partner_first_name || '',
partner_last_name: user.partner_last_name || '',
partner_is_member: Boolean(user.partner_is_member),
partner_plan_to_become_member: Boolean(user.partner_plan_to_become_member),
newsletter_publish_name: Boolean(user.newsletter_publish_name),
newsletter_publish_photo: Boolean(user.newsletter_publish_photo),
newsletter_publish_birthday: Boolean(user.newsletter_publish_birthday),
newsletter_publish_none: Boolean(user.newsletter_publish_none),
referred_by_member_name: user.referred_by_member_name || '',
volunteer_interests: Array.isArray(user.volunteer_interests) ? user.volunteer_interests : [],
scholarship_requested: Boolean(user.scholarship_requested),
scholarship_reason: user.scholarship_reason || ''
};
setFormData(nextFormData);
setHasUnsavedChanges(false);
}, [open, user]);
useEffect(() => {
return () => {
if (autoSaveTimeoutRef.current) {
clearTimeout(autoSaveTimeoutRef.current);
}
};
}, []);
const formatDate = (dateString) => {
if (!dateString) return '—';
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
};
const formatDateTime = (dateString) => {
if (!dateString) return '—';
return new Date(dateString).toLocaleString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
const formatPhoneNumber = (phone) => {
if (!phone) return '—';
const cleaned = phone.replace(/\D/g, '');
if (cleaned.length === 10) {
return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6)}`;
}
return phone;
};
const saveProfile = async (showToast = true) => {
if (!formData) return;
if (isSaving) {
pendingSaveRef.current = true;
return;
}
setIsSaving(true);
try {
await api.put('/users/profile', {
lead_sources: formData.lead_sources,
partner_first_name: formData.partner_first_name,
partner_last_name: formData.partner_last_name,
partner_is_member: formData.partner_is_member,
partner_plan_to_become_member: formData.partner_plan_to_become_member,
newsletter_publish_name: formData.newsletter_publish_name,
newsletter_publish_photo: formData.newsletter_publish_photo,
newsletter_publish_birthday: formData.newsletter_publish_birthday,
newsletter_publish_none: formData.newsletter_publish_none,
referred_by_member_name: formData.referred_by_member_name,
volunteer_interests: formData.volunteer_interests,
scholarship_requested: formData.scholarship_requested,
scholarship_reason: formData.scholarship_reason
});
setHasUnsavedChanges(false);
if (showToast) {
toast.success('Registration details saved');
}
} catch (error) {
if (showToast) {
toast.error(error.response?.data?.detail || 'Failed to save registration details');
}
} finally {
setIsSaving(false);
if (pendingSaveRef.current) {
pendingSaveRef.current = false;
saveProfile(showToast);
}
}
};
const scheduleAutoSave = () => {
setHasUnsavedChanges(true);
if (autoSaveTimeoutRef.current) {
clearTimeout(autoSaveTimeoutRef.current);
}
autoSaveTimeoutRef.current = setTimeout(() => {
saveProfile(false);
}, 800);
};
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => {
const next = { ...prev, [name]: value };
return next;
});
scheduleAutoSave();
};
const handleCheckboxChange = (name, checked) => {
setFormData(prev => ({ ...prev, [name]: checked }));
scheduleAutoSave();
};
const handleLeadSourceChange = (source) => {
setFormData(prev => {
const sources = prev.lead_sources.includes(source)
? prev.lead_sources.filter((item) => item !== source)
: [...prev.lead_sources, source];
return { ...prev, lead_sources: sources };
});
scheduleAutoSave();
};
const handleVolunteerChange = (option) => {
setFormData(prev => {
const interests = prev.volunteer_interests.includes(option)
? prev.volunteer_interests.filter((item) => item !== option)
: [...prev.volunteer_interests, option];
return { ...prev, volunteer_interests: interests };
});
scheduleAutoSave();
};
const InfoRow = ({ icon: Icon, label, value }) => (
<div className="flex items-start gap-3 py-3 border-b border-[var(--neutral-800)] last:border-b-0">
<div className="h-10 w-10 rounded-lg bg-[var(--lavender-400)] flex items-center justify-center flex-shrink-0">
<Icon className="h-5 w-5 text-brand-purple" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{label}
</p>
<p className="font-medium text-[var(--purple-ink)] break-words" style={{ fontFamily: "'Inter', sans-serif" }}>
{value || '—'}
</p>
</div>
</div>
);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[600px] rounded-2xl max-h-[90vh] overflow-y-auto scrollbar-dashboard">
<DialogHeader>
<DialogTitle className="text-2xl text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<FileText className="h-6 w-6" />
Registration Details
</DialogTitle>
<DialogDescription className="text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
View the registration information for this member application.
</DialogDescription>
</DialogHeader>
<div className="py-4 space-y-4">
{/* User Header Card */}
<Card className="p-4 bg-[var(--lavender-400)] border-2 border-[var(--neutral-800)] rounded-xl">
<div className="flex items-center gap-4">
<div className="h-16 w-16 rounded-full bg-[var(--neutral-800)]/20 flex items-center justify-center">
<User className="h-8 w-8 text-brand-purple" />
</div>
<div className="flex-1">
<p className="text-xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
{user.first_name} {user.last_name}
</p>
<p className="text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{user.email}
</p>
<div className="mt-2">
<StatusBadge status={user.status} />
</div>
</div>
</div>
</Card>
{/* Contact Information */}
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
Contact Information
</h3>
<InfoRow icon={Mail} label="Email Address" value={user.email} />
<InfoRow icon={Phone} label="Phone Number" value={formatPhoneNumber(user.phone)} />
</Card>
{/* Registration Details */}
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
Registration Details
</h3>
<InfoRow icon={Calendar} label="Registration Date" value={formatDate(user.created_at)} />
<InfoRow icon={UserCheck} label="Referred By" value={formData?.referred_by_member_name} />
<InfoRow icon={Clock} label="Email Verification Expires" value={formatDateTime(user.email_verification_expires_at)} />
</Card>
{formData && (
<>
{/* How Did You Hear About Us */}
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
How Did You Hear About Us? *
</h3>
<div className="space-y-3">
{leadSourceOptions.map((source) => (
<div key={source} className="flex items-center space-x-2">
<Checkbox
id={`lead_${source}`}
checked={formData.lead_sources.includes(source)}
onCheckedChange={() => handleLeadSourceChange(source)}
/>
<Label htmlFor={`lead_${source}`} className="text-base cursor-pointer">
{source}
</Label>
</div>
))}
</div>
</Card>
{/* Partner Information */}
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
Partner Information (Optional)
</h3>
<div className="grid md:grid-cols-2 gap-4 mb-4">
<div>
<Label htmlFor="partner_first_name">Partner First Name</Label>
<Input
id="partner_first_name"
name="partner_first_name"
value={formData.partner_first_name}
onChange={handleInputChange}
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
/>
</div>
<div>
<Label htmlFor="partner_last_name">Partner Last Name</Label>
<Input
id="partner_last_name"
name="partner_last_name"
value={formData.partner_last_name}
onChange={handleInputChange}
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
/>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="partner_is_member"
checked={formData.partner_is_member}
onCheckedChange={(checked) => handleCheckboxChange('partner_is_member', checked)}
/>
<Label htmlFor="partner_is_member" className="text-base cursor-pointer">
Is your partner already a member?
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="partner_plan_to_become_member"
checked={formData.partner_plan_to_become_member}
onCheckedChange={(checked) => handleCheckboxChange('partner_plan_to_become_member', checked)}
/>
<Label htmlFor="partner_plan_to_become_member" className="text-base cursor-pointer">
Does your partner plan to become a member?
</Label>
</div>
</div>
</Card>
{/* Newsletter Preferences */}
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
Newsletter Publication Preferences *
</h3>
<p className="text-brand-purple mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Please check what information may be published in LOAF Newsletter
</p>
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="newsletter_publish_name"
checked={formData.newsletter_publish_name}
onCheckedChange={(checked) => handleCheckboxChange('newsletter_publish_name', checked)}
/>
<Label htmlFor="newsletter_publish_name" className="text-base cursor-pointer">
Name
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="newsletter_publish_photo"
checked={formData.newsletter_publish_photo}
onCheckedChange={(checked) => handleCheckboxChange('newsletter_publish_photo', checked)}
/>
<Label htmlFor="newsletter_publish_photo" className="text-base cursor-pointer">
Photo (added later in profile)
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="newsletter_publish_birthday"
checked={formData.newsletter_publish_birthday}
onCheckedChange={(checked) => handleCheckboxChange('newsletter_publish_birthday', checked)}
/>
<Label htmlFor="newsletter_publish_birthday" className="text-base cursor-pointer">
Birthday
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="newsletter_publish_none"
checked={formData.newsletter_publish_none}
onCheckedChange={(checked) => handleCheckboxChange('newsletter_publish_none', checked)}
/>
<Label htmlFor="newsletter_publish_none" className="text-base cursor-pointer">
Do not publish any of my information
</Label>
</div>
</div>
</Card>
{/* Referral */}
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
Referral
</h3>
<div>
<Label htmlFor="referred_by_member_name">Name of a LOAF Member who already knows you</Label>
<Input
id="referred_by_member_name"
name="referred_by_member_name"
value={formData.referred_by_member_name}
onChange={handleInputChange}
placeholder="Enter member name or email"
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
/>
<p className="text-sm text-brand-purple mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
If referred by a current member, you may skip the event attendance requirement.
</p>
</div>
</Card>
{/* Volunteer Interests */}
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
Volunteer Interests (Optional)
</h3>
<p className="text-brand-purple mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
I may at some time be interested in volunteering with LOAF in the following ways (training is provided)
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{volunteerOptions.map((option) => (
<div key={option} className="flex items-center space-x-2">
<Checkbox
id={`volunteer_${option}`}
checked={formData.volunteer_interests.includes(option)}
onCheckedChange={() => handleVolunteerChange(option)}
/>
<Label htmlFor={`volunteer_${option}`} className="text-base cursor-pointer">
{option}
</Label>
</div>
))}
</div>
</Card>
{/* Scholarship Request */}
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
<div className="flex items-center space-x-2">
<Checkbox
id="scholarship_requested"
checked={formData.scholarship_requested}
onCheckedChange={(checked) => handleCheckboxChange('scholarship_requested', checked)}
/>
<Label htmlFor="scholarship_requested" className="text-base cursor-pointer font-semibold">
I am requesting for scholarship
</Label>
</div>
<p className="text-sm text-brand-purple mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Scholarship information is kept confidential
</p>
{formData.scholarship_requested && (
<div className="mt-4">
<Label htmlFor="scholarship_reason">Please explain your situation *</Label>
<Textarea
id="scholarship_reason"
name="scholarship_reason"
value={formData.scholarship_reason}
onChange={handleInputChange}
placeholder="Tell us why you're requesting a scholarship..."
rows={4}
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
/>
</div>
)}
</Card>
</>
)}
{/* Additional Information (if available) */}
{(user.address || user.city || user.state || user.zip_code) && (
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
Address
</h3>
<div className="text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{user.address && <p>{user.address}</p>}
{(user.city || user.state || user.zip_code) && (
<p>
{[user.city, user.state, user.zip_code].filter(Boolean).join(', ')}
</p>
)}
</div>
</Card>
)}
{/* Notes (if available) */}
{user.notes && (
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
Notes
</h3>
<p className="text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{user.notes}
</p>
</Card>
)}
{/* Rejection Reason (if rejected) */}
{user.status === 'rejected' && user.rejection_reason && (
<Card className="p-4 border border-red-300 bg-red-50 dark:bg-red-500/10 rounded-xl">
<h3 className="text-lg font-semibold text-red-600 mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
Rejection Reason
</h3>
<p className="text-red-600" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{user.rejection_reason}
</p>
</Card>
)}
</div>
<DialogFooter>
<div className="flex-1 text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{isSaving && 'Saving changes...'}
{!isSaving && hasUnsavedChanges && 'Unsaved changes'}
{!isSaving && !hasUnsavedChanges && 'All changes saved'}
</div>
<Button
type="button"
onClick={() => saveProfile(true)}
disabled={!hasUnsavedChanges || isSaving}
className="rounded-xl border-2 border-[var(--neutral-800)] bg-white text-[var(--purple-ink)] hover:bg-[var(--lavender-300)]"
>
Save All
</Button>
<Button
type="button"
onClick={() => onOpenChange(false)}
className="rounded-xl bg-[var(--purple-ink)] hover:bg-[var(--purple-ink)]/90 text-white"
>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default ViewRegistrationDialog;

View File

@@ -0,0 +1,987 @@
import React, { useState, useEffect } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from './ui/dialog';
import { Button } from './ui/button';
import { Card } from './ui/card';
import { Badge } from './ui/badge';
import { Checkbox } from './ui/checkbox';
import { Input } from './ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table';
import { Progress } from './ui/progress';
import { Alert, AlertDescription } from './ui/alert';
import { toast } from 'sonner';
import api from '../utils/api';
import {
Upload,
FileCheck,
CheckCircle,
Eye,
Play,
CheckCircle2,
AlertCircle,
AlertTriangle,
Trash2,
FileDown,
Users,
ChevronLeft,
ChevronRight,
Loader2
} from 'lucide-react';
/**
* WordPress Import Wizard Component
*
* A comprehensive 6-step wizard for importing WordPress users to LOAF platform.
* Features:
* - CSV upload and analysis
* - Interactive status review and adjustment
* - Preview before import
* - Real-time import progress
* - Full rollback capability
* - Error reporting
*/
export default function WordPressImportWizard({ open, onOpenChange, onSuccess }) {
// Wizard state
const [currentStep, setCurrentStep] = useState(1);
const [importJobId, setImportJobId] = useState(null);
// Data state
const [uploadedFile, setUploadedFile] = useState(null);
const [analysisResult, setAnalysisResult] = useState(null);
const [previewData, setPreviewData] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
// Override state
const [statusOverrides, setStatusOverrides] = useState({});
const [selectedRows, setSelectedRows] = useState(new Set());
// Import execution state
const [importing, setImporting] = useState(false);
const [importProgress, setImportProgress] = useState(0);
const [importResults, setImportResults] = useState(null);
// UI state
const [uploading, setUploading] = useState(false);
const [loading, setLoading] = useState(false);
// Step definitions
const steps = [
{ number: 1, title: 'Upload CSV', icon: Upload },
{ number: 2, title: 'Field Mapping', icon: FileCheck },
{ number: 3, title: 'Review Status', icon: CheckCircle },
{ number: 4, title: 'Preview', icon: Eye },
{ number: 5, title: 'Execute', icon: Play },
{ number: 6, title: 'Results', icon: CheckCircle2 }
];
// Reset wizard state when dialog opens/closes
useEffect(() => {
if (!open) {
setTimeout(() => {
setCurrentStep(1);
setImportJobId(null);
setUploadedFile(null);
setAnalysisResult(null);
setPreviewData([]);
setStatusOverrides({});
setSelectedRows(new Set());
setImporting(false);
setImportProgress(0);
setImportResults(null);
}, 300); // Wait for dialog close animation
}
}, [open]);
// ============================================================================
// Step Navigation
// ============================================================================
const canProceed = () => {
switch (currentStep) {
case 1:
return uploadedFile && analysisResult;
case 2:
return true; // Field mapping auto-detected
case 3:
return true; // Status review is optional
case 4:
return true; // Preview is informational
case 5:
return !importing;
case 6:
return true;
default:
return false;
}
};
const handleNext = () => {
if (currentStep < 6 && canProceed()) {
if (currentStep === 3) {
// Load preview data when moving from step 3 to 4
loadPreviewData(1);
}
setCurrentStep(currentStep + 1);
}
};
const handleBack = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};
// ============================================================================
// Step 1: Upload CSV
// ============================================================================
const handleFileSelect = (e) => {
const file = e.target.files[0];
if (file) {
if (!file.name.endsWith('.csv')) {
toast.error('Please upload a CSV file');
return;
}
setUploadedFile(file);
}
};
const handleUpload = async () => {
if (!uploadedFile) return;
setUploading(true);
const formData = new FormData();
formData.append('file', uploadedFile);
try {
const response = await api.post('/admin/import/upload-csv', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
setImportJobId(response.data.import_job_id);
setAnalysisResult(response.data);
toast.success(`CSV analyzed: ${response.data.valid_rows} valid rows, ${response.data.warnings} warnings`);
// Auto-advance to next step
setTimeout(() => setCurrentStep(2), 500);
} catch (error) {
toast.error(error.response?.data?.detail || 'Failed to upload CSV');
} finally {
setUploading(false);
}
};
// ============================================================================
// Step 3: Review & Adjust Status
// ============================================================================
const loadPreviewData = async (page = 1) => {
if (!importJobId) return;
setLoading(true);
try {
const response = await api.get(`/admin/import/${importJobId}/preview`, {
params: { page, page_size: 50 }
});
setPreviewData(response.data.rows);
setCurrentPage(response.data.page);
setTotalPages(response.data.total_pages);
} catch (error) {
toast.error('Failed to load preview data');
} finally {
setLoading(false);
}
};
useEffect(() => {
if (currentStep === 3 && importJobId && previewData.length === 0) {
loadPreviewData(1);
}
}, [currentStep, importJobId]);
const handleStatusOverride = (rowNum, status) => {
setStatusOverrides(prev => ({
...prev,
[rowNum]: { status }
}));
};
const handleBulkStatusChange = (status) => {
const newOverrides = { ...statusOverrides };
selectedRows.forEach(rowNum => {
newOverrides[rowNum] = { status };
});
setStatusOverrides(newOverrides);
toast.success(`Updated ${selectedRows.size} users to ${status}`);
};
const toggleRowSelection = (rowNum) => {
setSelectedRows(prev => {
const newSet = new Set(prev);
if (newSet.has(rowNum)) {
newSet.delete(rowNum);
} else {
newSet.add(rowNum);
}
return newSet;
});
};
const toggleSelectAll = () => {
if (selectedRows.size === previewData.length) {
setSelectedRows(new Set());
} else {
setSelectedRows(new Set(previewData.map(row => row.row_number)));
}
};
// ============================================================================
// Step 5: Execute Import
// ============================================================================
const handleExecuteImport = async () => {
setImporting(true);
setCurrentStep(5);
try {
// Start import
const response = await api.post(`/admin/import/${importJobId}/execute`, {
overrides: statusOverrides,
options: {
send_password_emails: true,
skip_errors: true
}
});
setImportResults(response.data);
toast.success(`Import completed: ${response.data.successful_rows} users imported`);
// Move to results step
setCurrentStep(6);
} catch (error) {
toast.error(error.response?.data?.detail || 'Import failed');
} finally {
setImporting(false);
}
};
// Poll for import progress
useEffect(() => {
if (currentStep === 5 && importing && importJobId) {
const interval = setInterval(async () => {
try {
const response = await api.get(`/admin/import/${importJobId}/status`);
setImportProgress(response.data.progress_percent);
} catch (error) {
console.error('Failed to fetch import status:', error);
}
}, 1000);
return () => clearInterval(interval);
}
}, [currentStep, importing, importJobId]);
// ============================================================================
// Step 6: Rollback
// ============================================================================
const [rollbackConfirmOpen, setRollbackConfirmOpen] = useState(false);
const [confirmText, setConfirmText] = useState('');
const handleRollback = async () => {
try {
await api.post(`/admin/import/${importJobId}/rollback`, { confirm: true });
toast.success(`Rolled back ${importResults.successful_rows} users`);
onOpenChange(false);
if (onSuccess) onSuccess();
} catch (error) {
toast.error(error.response?.data?.detail || 'Rollback failed');
}
};
const handleDownloadErrors = async () => {
try {
const response = await api.get(`/admin/import/${importJobId}/errors/download`, {
responseType: 'blob'
});
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `import_errors_${importJobId}.csv`);
document.body.appendChild(link);
link.click();
link.remove();
toast.success('Error report downloaded');
} catch (error) {
toast.error('Failed to download error report');
}
};
// ============================================================================
// Status Badge Component
// ============================================================================
const StatusBadge = ({ status }) => {
const colors = {
active: 'bg-green-100 text-green-800 border-green-300',
pre_validated: 'bg-blue-100 text-blue-800 border-blue-300',
payment_pending: 'bg-yellow-100 text-yellow-800 border-yellow-300',
inactive: 'bg-gray-100 text-gray-800 border-gray-300'
};
return (
<Badge variant="outline" className={colors[status] || 'bg-gray-100 text-gray-800'}>
{status.replace('_', ' ')}
</Badge>
);
};
// ============================================================================
// Render Step Content
// ============================================================================
const renderStepContent = () => {
switch (currentStep) {
case 1:
return <Step1Upload />;
case 2:
return <Step2FieldMapping />;
case 3:
return <Step3ReviewStatus />;
case 4:
return <Step4Preview />;
case 5:
return <Step5Execute />;
case 6:
return <Step6Results />;
default:
return null;
}
};
// ============================================================================
// Step 1: Upload CSV
// ============================================================================
const Step1Upload = () => (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2">Upload WordPress CSV Export</h3>
<p className="text-sm text-brand-purple ">
Select the WordPress user export CSV file. The file will be analyzed for data quality issues.
</p>
</div>
<Card className="p-6 border-2 border-dashed border-[var(--neutral-800)] bg-[var(--lavender-400)]">
<div className="flex flex-col items-center gap-4">
<Upload className="h-12 w-12 text-brand-purple " />
<div className="text-center">
<Input
type="file"
accept=".csv"
onChange={handleFileSelect}
className="max-w-xs"
/>
{uploadedFile && (
<p className="text-sm text-brand-purple mt-2">
Selected: {uploadedFile.name}
</p>
)}
</div>
</div>
</Card>
{uploadedFile && !analysisResult && (
<Button
onClick={handleUpload}
disabled={uploading}
className="w-full bg-brand-purple hover:bg-[var(--purple-ink)]"
>
{uploading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Analyzing CSV...
</>
) : (
<>
<Upload className="mr-2 h-4 w-4" />
Upload and Analyze
</>
)}
</Button>
)}
{analysisResult && (
<Card className="p-6 bg-green-50 border-green-200">
<h4 className="font-semibold text-green-900 mb-4">Analysis Complete</h4>
<div className="grid md:grid-cols-2 gap-4">
<div>
<p className="text-sm text-green-700">Total Rows</p>
<p className="text-2xl font-semibold text-green-900">{analysisResult.total_rows}</p>
</div>
<div>
<p className="text-sm text-green-700">Valid Rows</p>
<p className="text-2xl font-semibold text-green-900">{analysisResult.valid_rows}</p>
</div>
<div>
<p className="text-sm text-yellow-700">Warnings</p>
<p className="text-2xl font-semibold text-yellow-900">{analysisResult.warnings}</p>
</div>
<div>
<p className="text-sm text-red-700">Errors</p>
<p className="text-2xl font-semibold text-red-900">{analysisResult.errors}</p>
</div>
</div>
{analysisResult.data_quality && (
<div className="mt-4 pt-4 border-t border-green-300">
<h5 className="text-sm font-semibold text-green-900 mb-2">Data Quality Issues:</h5>
<ul className="text-sm text-green-800 space-y-1">
{analysisResult.data_quality.invalid_dob > 0 && (
<li> {analysisResult.data_quality.invalid_dob} invalid dates of birth</li>
)}
{analysisResult.data_quality.missing_phone > 0 && (
<li> {analysisResult.data_quality.missing_phone} missing phone numbers</li>
)}
{analysisResult.data_quality.duplicate_email > 0 && (
<li> {analysisResult.data_quality.duplicate_email} duplicate emails</li>
)}
</ul>
</div>
)}
</Card>
)}
</div>
);
// ============================================================================
// Step 2: Field Mapping
// ============================================================================
const Step2FieldMapping = () => (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2">Field Mapping</h3>
<p className="text-sm text-brand-purple ">
WordPress fields have been automatically mapped to LOAF platform fields.
</p>
</div>
<Card className="p-6">
<Table>
<TableHeader>
<TableRow>
<TableHead>WordPress Field</TableHead>
<TableHead></TableHead>
<TableHead>LOAF Field</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-mono text-sm">user_email</TableCell>
<TableCell></TableCell>
<TableCell className="font-mono text-sm">email</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-mono text-sm">first_name</TableCell>
<TableCell></TableCell>
<TableCell className="font-mono text-sm">first_name</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-mono text-sm">last_name</TableCell>
<TableCell></TableCell>
<TableCell className="font-mono text-sm">last_name</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-mono text-sm">cell_phone</TableCell>
<TableCell></TableCell>
<TableCell className="font-mono text-sm">phone</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-mono text-sm">date_of_birth</TableCell>
<TableCell></TableCell>
<TableCell className="font-mono text-sm">date_of_birth</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-mono text-sm">wp_capabilities</TableCell>
<TableCell></TableCell>
<TableCell className="font-mono text-sm">role + status</TableCell>
</TableRow>
</TableBody>
</Table>
</Card>
<Alert className="bg-blue-50 border-blue-200">
<AlertCircle className="h-4 w-4 text-blue-600" />
<AlertDescription className="text-blue-800">
WordPress roles will be automatically converted:
<ul className="mt-2 ml-4 space-y-1">
<li> <code>loaf_admin</code> admin (active)</li>
<li> <code>loaf_treasure</code> finance (active)</li>
<li> <code>administrator</code> superadmin (active)</li>
<li> <code>pms_subscription_plan_63</code> member</li>
</ul>
</AlertDescription>
</Alert>
</div>
);
// ============================================================================
// Step 3: Review & Adjust Status (KEY FEATURE)
// ============================================================================
const Step3ReviewStatus = () => (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2">Review & Adjust User Status</h3>
<p className="text-sm text-brand-purple ">
Review suggested status mappings and override as needed before import.
</p>
</div>
{/* Bulk edit toolbar */}
<Card className="p-4 bg-[var(--lavender-400)] border-[var(--neutral-800)]">
<div className="flex items-center gap-4">
<Checkbox
checked={selectedRows.size === previewData.length && previewData.length > 0}
onCheckedChange={toggleSelectAll}
/>
<span className="text-sm text-brand-purple font-medium">
{selectedRows.size > 0 ? `${selectedRows.size} selected` : 'Select all'}
</span>
{selectedRows.size > 0 && (
<Select onValueChange={handleBulkStatusChange}>
<SelectTrigger className="w-48">
<SelectValue placeholder="Change status to..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="pre_validated">Pre-Validated</SelectItem>
<SelectItem value="payment_pending">Payment Pending</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem>
</SelectContent>
</Select>
)}
</div>
</Card>
{/* Data table */}
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-brand-purple " />
</div>
) : (
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-[var(--lavender-400)]">
<TableHead className="w-12">
<Checkbox checked={false} />
</TableHead>
<TableHead>Row</TableHead>
<TableHead>Email</TableHead>
<TableHead>Name</TableHead>
<TableHead>WP Role</TableHead>
<TableHead>Suggested Status</TableHead>
<TableHead>Override Status</TableHead>
<TableHead>Issues</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{previewData.map((row) => (
<TableRow key={row.row_number}>
<TableCell>
<Checkbox
checked={selectedRows.has(row.row_number)}
onCheckedChange={() => toggleRowSelection(row.row_number)}
/>
</TableCell>
<TableCell className="font-mono text-sm">{row.row_number}</TableCell>
<TableCell className="text-sm">{row.email}</TableCell>
<TableCell className="text-sm">
{row.first_name} {row.last_name}
</TableCell>
<TableCell>
<Badge className="bg-[var(--neutral-800)] text-[var(--purple-ink)]">
{row.wordpress_roles?.join(', ') || 'N/A'}
</Badge>
</TableCell>
<TableCell>
<StatusBadge status={row.suggested_status} />
</TableCell>
<TableCell>
<Select
value={statusOverrides[row.row_number]?.status || row.suggested_status}
onValueChange={(value) => handleStatusOverride(row.row_number, value)}
>
<SelectTrigger className="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="pre_validated">Pre-Validated</SelectItem>
<SelectItem value="payment_pending">Payment Pending</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem>
</SelectContent>
</Select>
</TableCell>
<TableCell>
{row.warnings?.map((warning, idx) => (
<Badge
key={idx}
variant="outline"
className="text-orange-600 border-orange-300 mr-1"
>
<AlertCircle className="h-3 w-3 mr-1" />
{warning}
</Badge>
))}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-between">
<p className="text-sm text-brand-purple ">
Page {currentPage} of {totalPages}
</p>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => loadPreviewData(currentPage - 1)}
disabled={currentPage === 1 || loading}
>
<ChevronLeft className="h-4 w-4" />
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => loadPreviewData(currentPage + 1)}
disabled={currentPage === totalPages || loading}
>
Next
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
)}
</div>
);
// ============================================================================
// Step 4: Preview
// ============================================================================
const Step4Preview = () => {
const overrideCount = Object.keys(statusOverrides).length;
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2">Import Preview</h3>
<p className="text-sm text-brand-purple ">
Review the final import settings before execution.
</p>
</div>
<div className="grid md:grid-cols-3 gap-4">
<Card className="p-6">
<p className="text-sm text-brand-purple ">Total Users</p>
<p className="text-3xl font-semibold text-[var(--purple-ink)]">{analysisResult?.total_rows}</p>
</Card>
<Card className="p-6">
<p className="text-sm text-brand-purple ">Status Overrides</p>
<p className="text-3xl font-semibold text-[var(--purple-ink)]">{overrideCount}</p>
</Card>
<Card className="p-6">
<p className="text-sm text-brand-purple ">Expected Imports</p>
<p className="text-3xl font-semibold text-[var(--purple-ink)]">{analysisResult?.valid_rows}</p>
</Card>
</div>
<Card className="p-6">
<h4 className="font-semibold text-[var(--purple-ink)] mb-4">Import Options</h4>
<div className="space-y-3">
<div className="flex items-center gap-2">
<CheckCircle className="h-5 w-5 text-green-600" />
<span className="text-sm text-brand-purple ">Send password reset emails to all imported users</span>
</div>
<div className="flex items-center gap-2">
<CheckCircle className="h-5 w-5 text-green-600" />
<span className="text-sm text-brand-purple ">Skip rows with errors and continue import</span>
</div>
<div className="flex items-center gap-2">
<CheckCircle className="h-5 w-5 text-green-600" />
<span className="text-sm text-brand-purple ">Full rollback capability available after import</span>
</div>
</div>
</Card>
{overrideCount > 0 && (
<Alert className="bg-yellow-50 border-yellow-200">
<AlertCircle className="h-4 w-4 text-yellow-600" />
<AlertDescription className="text-yellow-800">
You have overridden {overrideCount} user status{overrideCount > 1 ? 'es' : ''}.
These will be applied during import.
</AlertDescription>
</Alert>
)}
</div>
);
};
// ============================================================================
// Step 5: Execute
// ============================================================================
const Step5Execute = () => (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2">
{importing ? 'Import in Progress...' : 'Ready to Import'}
</h3>
<p className="text-sm text-brand-purple ">
{importing
? 'Please wait while users are imported. This may take a few minutes.'
: 'Click "Start Import" to begin importing users.'}
</p>
</div>
{importing && (
<div className="space-y-4">
<Progress value={importProgress} className="w-full" />
<p className="text-center text-sm text-brand-purple ">
{importProgress.toFixed(1)}% complete
</p>
</div>
)}
{!importing && !importResults && (
<Button
onClick={handleExecuteImport}
className="w-full bg-brand-purple hover:bg-[var(--purple-ink)] py-6 text-lg"
>
<Play className="mr-2 h-5 w-5" />
Start Import
</Button>
)}
</div>
);
// ============================================================================
// Step 6: Results & Rollback
// ============================================================================
const Step6Results = () => (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2">Import Complete</h3>
<p className="text-sm text-brand-purple ">
Review the import results and download error reports if needed.
</p>
</div>
{/* Stats cards */}
<div className="grid md:grid-cols-3 gap-4">
<Card className="p-6 bg-green-50 border-green-200">
<p className="text-sm text-green-700">Successful Imports</p>
<p className="text-4xl font-semibold text-green-900">{importResults?.successful_rows || 0}</p>
</Card>
<Card className="p-6 bg-red-50 border-red-200">
<p className="text-sm text-red-700">Failed Imports</p>
<p className="text-4xl font-semibold text-red-900">{importResults?.failed_rows || 0}</p>
</Card>
<Card className="p-6 bg-blue-50 border-blue-200">
<p className="text-sm text-blue-700">Password Emails Sent</p>
<p className="text-4xl font-semibold text-blue-900">{importResults?.password_emails_queued || 0}</p>
</Card>
</div>
{/* Action buttons */}
<div className="flex gap-4 justify-between flex-wrap">
<div className="flex gap-4 flex-wrap">
{importResults?.failed_rows > 0 && (
<Button onClick={handleDownloadErrors} variant="outline">
<FileDown className="h-4 w-4 mr-2" />
Download Error Report
</Button>
)}
<Button
onClick={() => {
onOpenChange(false);
if (onSuccess) onSuccess();
}}
variant="outline"
>
<Users className="h-4 w-4 mr-2" />
View Imported Members
</Button>
</div>
{/* Rollback button (prominent, red) */}
{importResults?.successful_rows > 0 && (
<Button
onClick={() => setRollbackConfirmOpen(true)}
variant="destructive"
className="bg-red-600 hover:bg-red-700"
>
<Trash2 className="h-4 w-4 mr-2" />
Rollback Import ({importResults.successful_rows} users)
</Button>
)}
</div>
{/* Rollback confirmation dialog */}
<Dialog open={rollbackConfirmOpen} onOpenChange={setRollbackConfirmOpen}>
<DialogContent className="sm:max-w-md">
<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-[var(--purple-ink)]">
Confirm Rollback
</DialogTitle>
</div>
<DialogDescription className="text-brand-purple ">
This will permanently delete{' '}
<strong>{importResults?.successful_rows} users</strong> that were imported.
This action cannot be undone.
</DialogDescription>
</DialogHeader>
<div className="bg-red-50 border border-red-200 rounded-lg p-4 my-4">
<p className="text-sm text-red-800 font-medium mb-2">
Type "DELETE {importResults?.successful_rows} USERS" to confirm:
</p>
<Input
value={confirmText}
onChange={(e) => setConfirmText(e.target.value)}
className="mt-2"
placeholder={`DELETE ${importResults?.successful_rows} USERS`}
/>
</div>
<DialogFooter>
<Button onClick={() => setRollbackConfirmOpen(false)} variant="outline">
Cancel
</Button>
<Button
onClick={handleRollback}
disabled={confirmText !== `DELETE ${importResults?.successful_rows} USERS`}
className="bg-red-600 hover:bg-red-700"
>
Yes, Delete {importResults?.successful_rows} Users
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
// ============================================================================
// Main Render
// ============================================================================
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto scrollbar-dashboard">
<DialogHeader>
<DialogTitle className="text-2xl font-semibold text-[var(--purple-ink)]">
WordPress Import Wizard
</DialogTitle>
<DialogDescription className="text-brand-purple ">
Import WordPress users with interactive status review and full rollback capability
</DialogDescription>
</DialogHeader>
{/* Step indicator */}
<div className="flex items-center justify-between mb-6 px-4">
{steps.map((step, index) => {
const StepIcon = step.icon;
const isCompleted = currentStep > step.number;
const isCurrent = currentStep === step.number;
return (
<React.Fragment key={step.number}>
<div className="flex flex-col items-center">
<div
className={`
w-10 h-10 rounded-full flex items-center justify-center
${isCurrent ? 'bg-brand-purple text-white' : ''}
${isCompleted ? 'bg-green-600 text-white' : ''}
${!isCurrent && !isCompleted ? 'bg-gray-200 text-gray-600' : ''}
`}
>
{isCompleted ? (
<CheckCircle className="h-5 w-5" />
) : (
<StepIcon className="h-5 w-5" />
)}
</div>
<p className={`text-xs mt-1 ${isCurrent ? 'font-semibold text-[var(--purple-ink)]' : 'text-gray-600'}`}>
{step.title}
</p>
</div>
{index < steps.length - 1 && (
<div className={`flex-1 h-0.5 mx-2 ${isCompleted ? 'bg-green-600' : 'bg-gray-300'}`} />
)}
</React.Fragment>
);
})}
</div>
{/* Step content */}
<div className="py-6">
{renderStepContent()}
</div>
{/* Navigation footer */}
<DialogFooter className="flex justify-between items-center">
<Button
variant="outline"
onClick={handleBack}
disabled={currentStep === 1 || importing}
>
<ChevronLeft className="h-4 w-4 mr-2" />
Back
</Button>
{currentStep < 5 && (
<Button
onClick={handleNext}
disabled={!canProceed()}
className="bg-brand-purple hover:bg-[var(--purple-ink)]"
>
Next
<ChevronRight className="h-4 w-4 ml-2" />
</Button>
)}
{currentStep === 6 && (
<Button
onClick={() => {
onOpenChange(false);
if (onSuccess) onSuccess();
}}
className="bg-brand-purple hover:bg-[var(--purple-ink)]"
>
Close
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,347 @@
import React from 'react';
import { Button } from '../ui/button';
import StatusBadge from '../StatusBadge';
import {
ChevronDown,
ChevronUp,
Edit,
XCircle,
CreditCard,
Info,
ExternalLink,
Copy
} from 'lucide-react';
const HEADER_CELLS = [
{ label: 'Member', align: 'text-left' },
{ label: 'Plan', align: 'text-left' },
{ label: 'Status', align: 'text-left' },
{ label: 'Period', align: 'text-left' },
{ label: 'Base Fee', align: 'text-right' },
{ label: 'Donation', align: 'text-right' },
{ label: 'Total', align: 'text-right' },
{ label: 'Details', align: 'text-center' },
{ label: 'Actions', align: 'text-center' }
];
const HeaderCell = ({ align, children }) => (
<th
className={`p-4 text-[var(--purple-ink)] font-semibold ${align}`}
style={{ fontFamily: "'Inter', sans-serif" }}
>
{children}
</th>
);
const TableCell = ({ align = 'text-left', className = '', style, children, ...props }) => (
<td
className={`p-4 ${align} ${className}`.trim()}
style={style}
{...props}
>
{children}
</td>
);
const SubscriptionRow = ({
sub,
isExpanded,
onToggle,
onEdit,
onCancel,
hasPermission,
formatDate,
formatDateTime,
formatPrice,
copyToClipboard
}) => (
<>
<tr className="border-b border-[var(--neutral-800)] hover:bg-[var(--lavender-400)] transition-colors">
<TableCell>
<div className="font-medium text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
{sub.user.first_name} {sub.user.last_name}
</div>
<div className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{sub.user.email}
</div>
</TableCell>
<TableCell>
<div className="text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{sub.plan.name}
</div>
<div className="text-xs text-brand-purple ">
{sub.plan.billing_cycle}
</div>
</TableCell>
<TableCell>
<StatusBadge status={sub.status} />
</TableCell>
<TableCell>
<div className="text-sm text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<div>{formatDate(sub.start_date)}</div>
<div className="text-xs text-brand-purple ">to {formatDate(sub.end_date)}</div>
</div>
</TableCell>
<TableCell
align="text-right"
className="text-[var(--purple-ink)]"
style={{ fontFamily: "'Inter', sans-serif" }}
>
{formatPrice(sub.base_subscription_cents || 0)}
</TableCell>
<TableCell
align="text-right"
className="text-[var(--orange-light)]"
style={{ fontFamily: "'Inter', sans-serif" }}
>
{formatPrice(sub.donation_cents || 0)}
</TableCell>
<TableCell
align="text-right"
className="font-semibold text-[var(--purple-ink)]"
style={{ fontFamily: "'Inter', sans-serif" }}
>
{formatPrice(sub.amount_paid_cents || 0)}
</TableCell>
<TableCell>
<Button
size="sm"
variant="ghost"
onClick={onToggle}
className="text-brand-purple hover:bg-[var(--neutral-800)]"
>
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</Button>
</TableCell>
<TableCell>
<div className="flex items-center justify-center gap-2">
{hasPermission('subscriptions.edit') && (
<Button
size="sm"
variant="outline"
onClick={() => onEdit(sub)}
className="text-brand-purple hover:bg-[var(--neutral-800)]"
>
<Edit className="h-4 w-4" />
</Button>
)}
{sub.status === 'active' && hasPermission('subscriptions.cancel') && (
<Button
size="sm"
variant="outline-destructive"
onClick={() => onCancel(sub.id)}
>
<XCircle className="h-4 w-4" />
</Button>
)}
</div>
</TableCell>
</tr>
{isExpanded && (
<tr className="bg-[var(--lavender-400)]/30">
<TableCell colSpan={9} className="p-6">
<div className="space-y-4">
<h4 className="font-semibold text-[var(--purple-ink)] text-lg mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Transaction Details
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-3">
<h5 className="font-medium text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<CreditCard className="h-4 w-4" />
Payment Information
</h5>
<div className="space-y-2 text-sm">
{sub.payment_completed_at && (
<div className="flex justify-between">
<span className="text-brand-purple ">Payment Date:</span>
<span className="text-[var(--purple-ink)] font-medium">{formatDateTime(sub.payment_completed_at)}</span>
</div>
)}
{sub.payment_method && (
<div className="flex justify-between">
<span className="text-brand-purple ">Payment Method:</span>
<span className="text-[var(--purple-ink)] font-medium capitalize">{sub.payment_method}</span>
</div>
)}
{sub.card_brand && sub.card_last4 && (
<div className="flex justify-between">
<span className="text-brand-purple ">Card:</span>
<span className="text-[var(--purple-ink)] font-medium">{sub.card_brand} ****{sub.card_last4}</span>
</div>
)}
</div>
</div>
<div className="space-y-3">
<h5 className="font-medium text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<Info className="h-4 w-4" />
Stripe Transaction IDs
</h5>
<div className="space-y-2 text-sm">
{sub.stripe_payment_intent_id && (
<div className="flex items-center justify-between gap-2">
<span className="text-brand-purple ">Payment Intent:</span>
<div className="flex items-center gap-1">
<code className="text-xs bg-[var(--neutral-800)]/30 px-2 py-1 rounded text-[var(--purple-ink)]">
{sub.stripe_payment_intent_id.substring(0, 20)}...
</code>
<Button
size="sm"
variant="ghost"
onClick={() => copyToClipboard(sub.stripe_payment_intent_id, 'Payment Intent ID')}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
)}
{sub.stripe_charge_id && (
<div className="flex items-center justify-between gap-2">
<span className="text-brand-purple ">Charge ID:</span>
<div className="flex items-center gap-1">
<code className="text-xs bg-[var(--neutral-800)]/30 px-2 py-1 rounded text-[var(--purple-ink)]">
{sub.stripe_charge_id.substring(0, 20)}...
</code>
<Button
size="sm"
variant="ghost"
onClick={() => copyToClipboard(sub.stripe_charge_id, 'Charge ID')}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
)}
{sub.stripe_subscription_id && (
<div className="flex items-center justify-between gap-2">
<span className="text-brand-purple ">Subscription ID:</span>
<div className="flex items-center gap-1">
<code className="text-xs bg-[var(--neutral-800)]/30 px-2 py-1 rounded text-[var(--purple-ink)]">
{sub.stripe_subscription_id.substring(0, 20)}...
</code>
<Button
size="sm"
variant="ghost"
onClick={() => copyToClipboard(sub.stripe_subscription_id, 'Subscription ID')}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
)}
{sub.stripe_invoice_id && (
<div className="flex items-center justify-between gap-2">
<span className="text-brand-purple ">Invoice ID:</span>
<div className="flex items-center gap-1">
<code className="text-xs bg-[var(--neutral-800)]/30 px-2 py-1 rounded text-[var(--purple-ink)]">
{sub.stripe_invoice_id.substring(0, 20)}...
</code>
<Button
size="sm"
variant="ghost"
onClick={() => copyToClipboard(sub.stripe_invoice_id, 'Invoice ID')}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
)}
{sub.stripe_customer_id && (
<div className="flex items-center justify-between gap-2">
<span className="text-brand-purple ">Customer ID:</span>
<div className="flex items-center gap-1">
<code className="text-xs bg-[var(--neutral-800)]/30 px-2 py-1 rounded text-[var(--purple-ink)]">
{sub.stripe_customer_id.substring(0, 20)}...
</code>
<Button
size="sm"
variant="ghost"
onClick={() => copyToClipboard(sub.stripe_customer_id, 'Customer ID')}
>
<Copy className="h-3 w-3" />
</Button>
</div>
</div>
)}
{sub.stripe_receipt_url && (
<div className="flex items-center justify-between gap-2">
<span className="text-brand-purple ">Receipt:</span>
<Button
size="sm"
variant="outline"
onClick={() => window.open(sub.stripe_receipt_url, '_blank')}
className="text-brand-purple hover:bg-[var(--neutral-800)]"
>
<ExternalLink className="h-3 w-3 mr-1" />
View Receipt
</Button>
</div>
)}
</div>
</div>
</div>
</div>
</TableCell>
</tr>
)}
</>
);
const SubscriptionsTable = ({
subscriptions,
expandedRows,
onToggleRowExpansion,
onEdit,
onCancel,
hasPermission,
formatDate,
formatDateTime,
formatPrice,
copyToClipboard
}) => (
<table className="w-full">
<thead>
<tr className="bg-[var(--neutral-800)]/20 border-b border-[var(--neutral-800)]">
{HEADER_CELLS.map((cell) => (
<HeaderCell key={cell.label} align={cell.align}>
{cell.label}
</HeaderCell>
))}
</tr>
</thead>
<tbody>
{subscriptions.length > 0 ? (
subscriptions.map((sub) => (
<SubscriptionRow
key={sub.id}
sub={sub}
isExpanded={expandedRows.has(sub.id)}
onToggle={() => onToggleRowExpansion(sub.id)}
onEdit={onEdit}
onCancel={onCancel}
hasPermission={hasPermission}
formatDate={formatDate}
formatDateTime={formatDateTime}
formatPrice={formatPrice}
copyToClipboard={copyToClipboard}
/>
))
) : (
<tr>
<TableCell
align="text-center"
className="p-12 text-brand-purple "
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
colSpan={9}
>
No subscriptions found
</TableCell>
</tr>
)}
</tbody>
</table>
);
export default SubscriptionsTable;

View File

@@ -26,7 +26,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
<div className="space-y-8"> <div className="space-y-8">
{/* Personal Information */} {/* Personal Information */}
<div className="space-y-4"> <div className="space-y-4">
<h2 className="text-2xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <h2 className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Personal Information Personal Information
</h2> </h2>
@@ -40,7 +40,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
required required
value={formData.first_name} value={formData.first_name}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
data-testid="first-name-input" data-testid="first-name-input"
/> />
</div> </div>
@@ -52,7 +52,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
required required
value={formData.last_name} value={formData.last_name}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
data-testid="last-name-input" data-testid="last-name-input"
/> />
</div> </div>
@@ -69,7 +69,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
required required
value={formData.phone} value={formData.phone}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
data-testid="phone-input" data-testid="phone-input"
/> />
</div> </div>
@@ -82,7 +82,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
required required
value={formData.date_of_birth} value={formData.date_of_birth}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
data-testid="dob-input" data-testid="dob-input"
/> />
</div> </div>
@@ -97,7 +97,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
required required
value={formData.address} value={formData.address}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]" className="h-14 rounded-xl border-2 border-var[(--neutral-300)] focus:border-[var(--orange-soft)]"
data-testid="address-input" data-testid="address-input"
/> />
</div> </div>
@@ -112,7 +112,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
required required
value={formData.city} value={formData.city}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
data-testid="city-input" data-testid="city-input"
/> />
</div> </div>
@@ -124,7 +124,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
required required
value={formData.state} value={formData.state}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
data-testid="state-input" data-testid="state-input"
/> />
</div> </div>
@@ -136,7 +136,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
required required
value={formData.zipcode} value={formData.zipcode}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
data-testid="zipcode-input" data-testid="zipcode-input"
/> />
</div> </div>
@@ -145,7 +145,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
{/* How Did You Hear About Us */} {/* How Did You Hear About Us */}
<div className="space-y-4"> <div className="space-y-4">
<h2 className="text-2xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <h2 className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
How Did You Hear About Us? * How Did You Hear About Us? *
</h2> </h2>
<div className="space-y-3"> <div className="space-y-3">
@@ -167,7 +167,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
{/* Partner Information */} {/* Partner Information */}
<div className="space-y-4"> <div className="space-y-4">
<h2 className="text-2xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <h2 className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Partner Information (Optional) Partner Information (Optional)
</h2> </h2>
@@ -179,7 +179,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
name="partner_first_name" name="partner_first_name"
value={formData.partner_first_name} value={formData.partner_first_name}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
data-testid="partner-first-name-input" data-testid="partner-first-name-input"
/> />
</div> </div>
@@ -190,7 +190,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
name="partner_last_name" name="partner_last_name"
value={formData.partner_last_name} value={formData.partner_last_name}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
data-testid="partner-last-name-input" data-testid="partner-last-name-input"
/> />
</div> </div>

View File

@@ -33,10 +33,10 @@ const RegistrationStep2 = ({ formData, setFormData, handleInputChange }) => {
<div className="space-y-8"> <div className="space-y-8">
{/* Newsletter Publication Preferences */} {/* Newsletter Publication Preferences */}
<div className="space-y-4"> <div className="space-y-4">
<h2 className="text-2xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <h2 className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Newsletter Publication Preferences * Newsletter Publication Preferences *
</h2> </h2>
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Please check what information may be published in LOAF Newsletter Please check what information may be published in LOAF Newsletter
</p> </p>
@@ -97,7 +97,7 @@ const RegistrationStep2 = ({ formData, setFormData, handleInputChange }) => {
{/* Referral */} {/* Referral */}
<div className="space-y-4"> <div className="space-y-4">
<h2 className="text-2xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <h2 className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Referral Referral
</h2> </h2>
<div> <div>
@@ -110,10 +110,10 @@ const RegistrationStep2 = ({ formData, setFormData, handleInputChange }) => {
value={formData.referred_by_member_name} value={formData.referred_by_member_name}
onChange={handleInputChange} onChange={handleInputChange}
placeholder="Enter member name or email" placeholder="Enter member name or email"
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
data-testid="referral-input" data-testid="referral-input"
/> />
<p className="text-sm text-[#664fa3] mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-sm text-brand-purple mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
If referred by a current member, you may skip the event attendance requirement. If referred by a current member, you may skip the event attendance requirement.
</p> </p>
</div> </div>
@@ -121,10 +121,10 @@ const RegistrationStep2 = ({ formData, setFormData, handleInputChange }) => {
{/* Volunteer Interests */} {/* Volunteer Interests */}
<div className="space-y-4"> <div className="space-y-4">
<h2 className="text-2xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <h2 className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Volunteer Interests (Optional) Volunteer Interests (Optional)
</h2> </h2>
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
I may at some time be interested in volunteering with LOAF in the following ways (training is provided) I may at some time be interested in volunteering with LOAF in the following ways (training is provided)
</p> </p>
@@ -158,7 +158,7 @@ const RegistrationStep2 = ({ formData, setFormData, handleInputChange }) => {
I am requesting for scholarship I am requesting for scholarship
</Label> </Label>
</div> </div>
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Scholarship information is kept confidential Scholarship information is kept confidential
</p> </p>
@@ -174,7 +174,7 @@ const RegistrationStep2 = ({ formData, setFormData, handleInputChange }) => {
onChange={handleInputChange} onChange={handleInputChange}
placeholder="Tell us why you're requesting a scholarship..." placeholder="Tell us why you're requesting a scholarship..."
rows={4} rows={4}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
/> />
</div> </div>
)} )}

View File

@@ -23,11 +23,11 @@ const RegistrationStep3 = ({ formData, setFormData, handleInputChange }) => {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="space-y-4"> <div className="space-y-4">
<h2 className="text-2xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <h2 className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Members Directory Members Directory
</h2> </h2>
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Would you like to be displayed on our private members directory? (optional and you can change the answer later) Would you like to be displayed on our private members directory? (optional and you can change the answer later)
</p> </p>
@@ -37,8 +37,8 @@ const RegistrationStep3 = ({ formData, setFormData, handleInputChange }) => {
className={` className={`
p-4 rounded-xl border-2 cursor-pointer transition-all p-4 rounded-xl border-2 cursor-pointer transition-all
${formData.show_in_directory ${formData.show_in_directory
? 'border-[#ff9e77] bg-[#ff9e77]/5' ? 'border-[var(--orange-light)] bg-[var(--orange-light)]/5'
: 'border-[#ddd8eb] hover:border-[#664fa3]' : 'border-[var(--neutral-800)] hover:border-brand-purple '
} }
`} `}
onClick={() => setFormData(prev => ({ ...prev, show_in_directory: true }))} onClick={() => setFormData(prev => ({ ...prev, show_in_directory: true }))}
@@ -46,13 +46,13 @@ const RegistrationStep3 = ({ formData, setFormData, handleInputChange }) => {
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className={` <div className={`
w-5 h-5 rounded-full border-2 flex items-center justify-center w-5 h-5 rounded-full border-2 flex items-center justify-center
${formData.show_in_directory ? 'border-[#ff9e77]' : 'border-[#ddd8eb]'} ${formData.show_in_directory ? 'border-[var(--orange-light)]' : 'border-[var(--neutral-800)]'}
`}> `}>
{formData.show_in_directory && ( {formData.show_in_directory && (
<div className="w-3 h-3 rounded-full bg-[#ff9e77]" /> <div className="w-3 h-3 rounded-full bg-[var(--orange-light)]" />
)} )}
</div> </div>
<span className="font-medium text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <span className="font-medium text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Yes, include me in the Members Directory Yes, include me in the Members Directory
</span> </span>
</div> </div>
@@ -62,8 +62,8 @@ const RegistrationStep3 = ({ formData, setFormData, handleInputChange }) => {
className={` className={`
p-4 rounded-xl border-2 cursor-pointer transition-all p-4 rounded-xl border-2 cursor-pointer transition-all
${!formData.show_in_directory ${!formData.show_in_directory
? 'border-[#ff9e77] bg-[#ff9e77]/5' ? 'border-[var(--orange-light)] bg-[var(--orange-light)]/5'
: 'border-[#ddd8eb] hover:border-[#664fa3]' : 'border-[var(--neutral-800)] hover:border-brand-purple '
} }
`} `}
onClick={() => setFormData(prev => ({ ...prev, show_in_directory: false }))} onClick={() => setFormData(prev => ({ ...prev, show_in_directory: false }))}
@@ -71,13 +71,13 @@ const RegistrationStep3 = ({ formData, setFormData, handleInputChange }) => {
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className={` <div className={`
w-5 h-5 rounded-full border-2 flex items-center justify-center w-5 h-5 rounded-full border-2 flex items-center justify-center
${!formData.show_in_directory ? 'border-[#ff9e77]' : 'border-[#ddd8eb]'} ${!formData.show_in_directory ? 'border-[var(--orange-light)]' : 'border-[var(--neutral-800)]'}
`}> `}>
{!formData.show_in_directory && ( {!formData.show_in_directory && (
<div className="w-3 h-3 rounded-full bg-[#ff9e77]" /> <div className="w-3 h-3 rounded-full bg-[var(--orange-light)]" />
)} )}
</div> </div>
<span className="font-medium text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <span className="font-medium text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
No, don't include me in the Members Directory No, don't include me in the Members Directory
</span> </span>
</div> </div>
@@ -87,8 +87,8 @@ const RegistrationStep3 = ({ formData, setFormData, handleInputChange }) => {
{/* Conditional Directory Fields */} {/* Conditional Directory Fields */}
{formData.show_in_directory && ( {formData.show_in_directory && (
<div className="space-y-4 mt-6 p-6 bg-white rounded-xl border border-[#ddd8eb]"> <div className="space-y-4 mt-6 p-6 bg-background rounded-xl border border-[var(--neutral-800)]">
<p className="text-[#664fa3] text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Below, choose what information you would like include in the Members Only Directory. Below, choose what information you would like include in the Members Only Directory.
(If you ever want to update this information, remember the Directory Section and Account Section are separate) (If you ever want to update this information, remember the Directory Section and Account Section are separate)
</p> </p>
@@ -101,7 +101,7 @@ const RegistrationStep3 = ({ formData, setFormData, handleInputChange }) => {
type="email" type="email"
value={formData.directory_email} value={formData.directory_email}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
/> />
</div> </div>
@@ -114,7 +114,7 @@ const RegistrationStep3 = ({ formData, setFormData, handleInputChange }) => {
onChange={handleInputChange} onChange={handleInputChange}
placeholder="Tell other members about yourself..." placeholder="Tell other members about yourself..."
rows={4} rows={4}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
/> />
</div> </div>
@@ -125,7 +125,7 @@ const RegistrationStep3 = ({ formData, setFormData, handleInputChange }) => {
name="directory_address" name="directory_address"
value={formData.directory_address} value={formData.directory_address}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
/> />
</div> </div>
@@ -137,7 +137,7 @@ const RegistrationStep3 = ({ formData, setFormData, handleInputChange }) => {
type="tel" type="tel"
value={formData.directory_phone} value={formData.directory_phone}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
/> />
</div> </div>
@@ -149,7 +149,7 @@ const RegistrationStep3 = ({ formData, setFormData, handleInputChange }) => {
type="date" type="date"
value={formData.directory_dob} value={formData.directory_dob}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
/> />
</div> </div>
@@ -162,7 +162,7 @@ const RegistrationStep3 = ({ formData, setFormData, handleInputChange }) => {
name="directory_partner_name" name="directory_partner_name"
value={formData.directory_partner_name} value={formData.directory_partner_name}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
/> />
</div> </div>
</div> </div>

View File

@@ -7,11 +7,11 @@ const RegistrationStep4 = ({ formData, handleInputChange }) => {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="space-y-4"> <div className="space-y-4">
<h2 className="text-2xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <h2 className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Account Credentials Account Credentials
</h2> </h2>
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Your email is also your username that you can use to login. Your email is also your username that you can use to login.
Please note you can only login after your application is validated. Please note you can only login after your application is validated.
</p> </p>
@@ -28,7 +28,7 @@ const RegistrationStep4 = ({ formData, handleInputChange }) => {
value={formData.email} value={formData.email}
onChange={handleInputChange} onChange={handleInputChange}
placeholder="your.email@example.com" placeholder="your.email@example.com"
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
data-testid="email-input" data-testid="email-input"
/> />
</div> </div>
@@ -43,10 +43,10 @@ const RegistrationStep4 = ({ formData, handleInputChange }) => {
value={formData.password} value={formData.password}
onChange={handleInputChange} onChange={handleInputChange}
placeholder="At least 6 characters" placeholder="At least 6 characters"
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
data-testid="password-input" data-testid="password-input"
/> />
<p className="text-sm text-[#664fa3] mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-sm text-brand-purple mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Must be at least 6 characters long Must be at least 6 characters long
</p> </p>
</div> </div>
@@ -60,7 +60,7 @@ const RegistrationStep4 = ({ formData, handleInputChange }) => {
value={formData.confirmPassword} value={formData.confirmPassword}
onChange={handleInputChange} onChange={handleInputChange}
placeholder="Re-enter your password" placeholder="Re-enter your password"
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
data-testid="confirm-password-input" data-testid="confirm-password-input"
/> />
{formData.confirmPassword && formData.password !== formData.confirmPassword && ( {formData.confirmPassword && formData.password !== formData.confirmPassword && (
@@ -71,7 +71,7 @@ const RegistrationStep4 = ({ formData, handleInputChange }) => {
</div> </div>
{/* Terms of Service Acceptance */} {/* Terms of Service Acceptance */}
<div className="p-4 bg-[#F8F7FB] rounded-lg border border-[#ddd8eb]"> <div className="p-4 bg-[var(--lavender-500)] rounded-lg border border-[var(--neutral-800)]">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<input <input
type="checkbox" type="checkbox"
@@ -79,26 +79,26 @@ const RegistrationStep4 = ({ formData, handleInputChange }) => {
name="accepts_tos" name="accepts_tos"
checked={formData.accepts_tos || false} checked={formData.accepts_tos || false}
onChange={handleInputChange} onChange={handleInputChange}
className="mt-1 w-4 h-4 text-[#664fa3] border-gray-300 rounded focus:ring-[#664fa3]" className="mt-1 w-4 h-4 text-brand-purple border-gray-300 rounded focus:ring-brand-purple "
required required
data-testid="tos-checkbox" data-testid="tos-checkbox"
/> />
<label htmlFor="accepts_tos" className="text-sm text-gray-700" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <label htmlFor="accepts_tos" className="text-sm text-gray-700" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
I agree to the{' '} I agree to the{' '}
<a <a
href="/membership/terms-of-service" href="/become-a-member/terms-of-service"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-[#664fa3] hover:text-[#422268] font-semibold underline" className="text-brand-purple hover:text-[var(--purple-ink)] font-semibold underline"
> >
Terms of Service Terms of Service
</a> </a>
{' '}and{' '} {' '}and{' '}
<a <a
href="/membership/privacy-policy" href="become-a-member/privacy-policy"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-[#664fa3] hover:text-[#422268] font-semibold underline" className="text-brand-purple hover:text-[var(--purple-ink)] font-semibold underline"
> >
Privacy Policy Privacy Policy
</a> </a>

View File

@@ -20,17 +20,17 @@ const RegistrationStepIndicator = ({ currentStep, totalSteps = 4 }) => {
w-12 h-12 rounded-full flex items-center justify-center font-semibold text-lg w-12 h-12 rounded-full flex items-center justify-center font-semibold text-lg
transition-all duration-300 transition-all duration-300
${currentStep === step.number ${currentStep === step.number
? 'bg-[#ff9e77] text-white scale-110 shadow-lg' ? 'bg-[var(--orange-light)] text-white scale-110 shadow-lg'
: currentStep > step.number : currentStep > step.number
? 'bg-[#81B29A] text-white' ? 'bg-[var(--green-light)] text-white'
: 'bg-[#ddd8eb] text-[#664fa3]' : 'bg-[var(--neutral-800)] text-brand-purple '
} }
`}> `}>
{currentStep > step.number ? '✓' : step.number} {currentStep > step.number ? '✓' : step.number}
</div> </div>
<span className={` <span className={`
text-sm mt-2 font-medium transition-colors text-sm mt-2 font-medium transition-colors
${currentStep === step.number ? 'text-[#ff9e77]' : 'text-[#664fa3]'} ${currentStep === step.number ? 'text-[var(--orange-light)]' : 'text-brand-purple '}
`} style={{ fontFamily: "'Nunito Sans', sans-serif" }}> `} style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{step.title} {step.title}
</span> </span>
@@ -38,11 +38,11 @@ const RegistrationStepIndicator = ({ currentStep, totalSteps = 4 }) => {
{/* Connecting Line */} {/* Connecting Line */}
{index < steps.length - 1 && ( {index < steps.length - 1 && (
<div className="flex-1 h-1 mx-2 relative -top-6 bg-[#ddd8eb]"> <div className="flex-1 h-1 mx-2 relative -top-6 bg-[var(--neutral-800)]">
<div <div
className={` className={`
h-full transition-all duration-500 h-full transition-all duration-500
${currentStep > step.number ? 'bg-[#81B29A] w-full' : 'bg-transparent w-0'} ${currentStep > step.number ? 'bg-[var(--green-light)] w-full' : 'bg-transparent w-0'}
`} `}
/> />
</div> </div>
@@ -52,8 +52,8 @@ const RegistrationStepIndicator = ({ currentStep, totalSteps = 4 }) => {
</div> </div>
{/* Step Counter */} {/* Step Counter */}
<p className="text-center text-[#664fa3] mt-6 text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-center text-brand-purple mt-6 text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Step <span className="font-semibold text-[#ff9e77]">{currentStep}</span> of {totalSteps} Step <span className="font-semibold text-[var(--orange-light)]">{currentStep}</span> of {totalSteps}
</p> </p>
</div> </div>
); );

View File

@@ -1,7 +1,7 @@
import * as React from "react" import * as React from "react";
import { cva } from "class-variance-authority"; import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const badgeVariants = cva( const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
@@ -9,26 +9,38 @@ const badgeVariants = cva(
variants: { variants: {
variant: { variant: {
default: default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", "border-transparent bg-primary text-primary-foreground shadow ",
secondary: secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground", outline: "text-foreground",
green:
"border-transparent bg-[var(--green-forest)] text-white hover:bg-[var(--green-fern)]",
orange:
"border-transparent bg-orange-500 text-white hover:bg-orange-500/80",
orange2:
"border-transparent bg-orange-100 text-orange-700 hover:bg-orange-100/80",
pink: "border-transparent bg-[var(--pink-500)] text-white hover:bg-[var(--pink-500)]/80",
red: "border-transparent bg-red-100 text-red-700 hover:bg-red-100/80",
red2: "border-transparent bg-red-500 text-white hover:bg-red-500/80",
gray: "border-transparent bg-gray-200 text-gray-700 hover:bg-gray-200/80",
gray2: "border-transparent bg-gray-400 text-white hover:bg-gray-400/80",
gray3:
"border-transparent bg-gray-300 text-gray-600 hover:bg-gray-300/80",
purple: "bg-light-lavender",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
} }
) );
function Badge({ function Badge({ className, variant, ...props }) {
className, return (
variant, <div className={cn(badgeVariants({ variant }), className)} {...props} />
...props );
}) {
return (<div className={cn(badgeVariants({ variant }), className)} {...props} />);
} }
export { Badge, badgeVariants } export { Badge, badgeVariants };

View File

@@ -1,48 +1,46 @@
import * as React from "react" import * as React from "react";
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot";
import { cva } from "class-variance-authority"; import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const buttonVariants = cva( const buttonVariants = cva("btn", {
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: { variants: {
variant: { variant: {
default: default: "btn-primary",
"bg-primary text-primary-foreground shadow hover:bg-primary/90", secondary: "btn-secondary",
destructive: ghost: "btn-ghost",
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", outline: "btn-outline",
outline: "outline-destructive": "btn-outline-destructive",
"border border-input shadow-sm hover:bg-accent hover:text-accent-foreground", accent: "btn-accent",
secondary: destructive: "btn-destructive",
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", link: "btn-link",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: "h-9 px-4 py-2", default: "btn-md",
sm: "h-8 rounded-md px-3 text-xs", sm: "btn-sm",
lg: "h-10 rounded-md px-8", lg: "btn-lg",
icon: "h-9 w-9", icon: "btn-icon",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
size: "default", size: "default",
}, },
} });
)
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => { const Button = React.forwardRef(
const Comp = asChild ? Slot : "button" ({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return ( return (
<Comp <Comp
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size }), className)}
ref={ref} ref={ref}
{...props} /> {...props}
/>
); );
}) }
Button.displayName = "Button" );
Button.displayName = "Button";
export { Button, buttonVariants } export { Button, buttonVariants };

View File

@@ -1,50 +1,65 @@
import * as React from "react" import * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Card = React.forwardRef(({ className, ...props }, ref) => ( const Card = React.forwardRef(({ className, ...props }, ref) => (
<div <div
ref={ref} ref={ref}
className={cn("rounded-xl border bg-card text-card-foreground shadow", className)} className={cn(
{...props} /> "rounded-xl border bg-card text-card-foreground shadow",
)) className,
Card.displayName = "Card" )}
{...props}
/>
));
Card.displayName = "Card";
const CardHeader = React.forwardRef(({ className, ...props }, ref) => ( const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
<div <div
ref={ref} ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)} className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props} /> {...props}
)) />
CardHeader.displayName = "CardHeader" ));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef(({ className, ...props }, ref) => ( const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
<div <div
ref={ref} ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)} className={cn("font-semibold leading-none tracking-tight", className)}
{...props} /> {...props}
)) />
CardTitle.displayName = "CardTitle" ));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef(({ className, ...props }, ref) => ( const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
<div <div
ref={ref} ref={ref}
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} /> {...props}
)) />
CardDescription.displayName = "CardDescription" ));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef(({ className, ...props }, ref) => ( const CardContent = React.forwardRef(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
)) ));
CardContent.displayName = "CardContent" CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef(({ className, ...props }, ref) => ( const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
<div <div
ref={ref} ref={ref}
className={cn("flex items-center p-6 pt-0", className)} className={cn("flex items-center p-6 pt-0", className)}
{...props} /> {...props}
)) />
CardFooter.displayName = "CardFooter" ));
CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};

View File

@@ -50,7 +50,7 @@ CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef(({ className, ...props }, ref) => ( const CommandList = React.forwardRef(({ className, ...props }, ref) => (
<CommandPrimitive.List <CommandPrimitive.List
ref={ref} ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)} className={cn("max-h-[300px] overflow-y-auto scrollbar-dashboard scrollbar-x-dashboard overflow-x-hidden", className)}
{...props} /> {...props} />
)) ))

View File

@@ -47,7 +47,7 @@ const ContextMenuContent = React.forwardRef(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Content <ContextMenuPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"z-50 max-h-[--radix-context-menu-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]", "z-50 max-h-[--radix-context-menu-content-available-height] min-w-[8rem] overflow-y-auto scrollbar-dashboard scrollbar-x-dashboard overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]",
className className
)} )}
{...props} /> {...props} />

View File

@@ -20,7 +20,7 @@ const DropdownMenuSubTrigger = React.forwardRef(({ className, inset, children, .
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", "flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-brand-light-lavender data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8", inset && "pl-8",
className className
)} )}
@@ -50,7 +50,7 @@ const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 4, ...pr
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md", "z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto scrollbar-dashboard scrollbar-x-dashboard overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]", "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
className className
)} )}
@@ -63,7 +63,7 @@ const DropdownMenuItem = React.forwardRef(({ className, inset, ...props }, ref)
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0", "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-brand-light-lavender focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
inset && "pl-8", inset && "pl-8",
className className
)} )}

View File

@@ -7,7 +7,7 @@ const Input = React.forwardRef(({ className, type, ...props }, ref) => {
<input <input
type={type} type={type}
className={cn( className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className className
)} )}
ref={ref} ref={ref}

View File

@@ -1,9 +1,9 @@
import * as React from "react" import * as React from "react";
import { Eye, EyeOff } from "lucide-react" import { Eye, EyeOff } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const PasswordInput = React.forwardRef(({ className, ...props }, ref) => { const PasswordInput = React.forwardRef(({ className, ...props }, ref) => {
const [showPassword, setShowPassword] = React.useState(false) const [showPassword, setShowPassword] = React.useState(false);
return ( return (
<div className="relative"> <div className="relative">
@@ -19,7 +19,7 @@ const PasswordInput = React.forwardRef(({ className, ...props }, ref) => {
<button <button
type="button" type="button"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-[#6B708D] hover:text-[#3D405B] transition-colors focus:outline-none" className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--slate-muted)] hover:text-[var(--slate-dark)] transition-colors focus:outline-none"
tabIndex={-1} tabIndex={-1}
> >
{showPassword ? ( {showPassword ? (
@@ -29,8 +29,8 @@ const PasswordInput = React.forwardRef(({ className, ...props }, ref) => {
)} )}
</button> </button>
</div> </div>
) );
}) });
PasswordInput.displayName = "PasswordInput" PasswordInput.displayName = "PasswordInput";
export { PasswordInput } export { PasswordInput };

View File

@@ -52,7 +52,7 @@ const SelectContent = React.forwardRef(({ className, children, position = "poppe
<SelectPrimitive.Content <SelectPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]", "relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden scrollbar-dashboard scrollbar-x-dashboard rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
position === "popper" && position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className className
@@ -83,7 +83,7 @@ const SelectItem = React.forwardRef(({ className, children, ...props }, ref) =>
<SelectPrimitive.Item <SelectPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:text-white focus:text-white",
className className
)} )}
{...props}> {...props}>
@@ -92,7 +92,7 @@ const SelectItem = React.forwardRef(({ className, children, ...props }, ref) =>
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator> </SelectPrimitive.ItemIndicator>
</span> </span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> <SelectPrimitive.ItemText className="">{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item> </SelectPrimitive.Item>
)) ))
SelectItem.displayName = SelectPrimitive.Item.displayName SelectItem.displayName = SelectPrimitive.Item.displayName

View File

@@ -1,78 +1,91 @@
import * as React from "react" import * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Table = React.forwardRef(({ className, ...props }, ref) => ( const Table = React.forwardRef(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto"> <div className="relative w-full overflow-auto">
<table <table ref={ref} className={cn("w-full", className)} {...props} />
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props} />
</div> </div>
)) ));
Table.displayName = "Table" Table.displayName = "Table";
const TableHeader = React.forwardRef(({ className, ...props }, ref) => ( const TableHeader = React.forwardRef(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} /> <thead
)) ref={ref}
TableHeader.displayName = "TableHeader" className={cn(
"bg-[var(--lavender-300)] border-b border-[var(--neutral-800)]",
className,
)}
{...props}
/>
));
TableHeader.displayName = "TableHeader";
const TableBody = React.forwardRef(({ className, ...props }, ref) => ( const TableBody = React.forwardRef(({ className, ...props }, ref) => (
<tbody <tbody
ref={ref} ref={ref}
className={cn("[&_tr:last-child]:border-0", className)} className={cn("[&_tr:last-child]:border-0", className)}
{...props} /> {...props}
)) />
TableBody.displayName = "TableBody" ));
TableBody.displayName = "TableBody";
const TableFooter = React.forwardRef(({ className, ...props }, ref) => ( const TableFooter = React.forwardRef(({ className, ...props }, ref) => (
<tfoot <tfoot
ref={ref} ref={ref}
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)} className={cn(
{...props} /> "border-t border-[var(--neutral-800)] font-medium [&>tr]:last:border-b-0",
)) className,
TableFooter.displayName = "TableFooter" )}
{...props}
/>
));
TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef(({ className, ...props }, ref) => ( const TableRow = React.forwardRef(({ className, ...props }, ref) => (
<tr <tr
ref={ref} ref={ref}
className={cn( className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", "border-b border-[var(--neutral-800)] transition-colors hover:bg-[var(--lavender-400)]",
className className,
)} )}
{...props} /> {...props}
)) />
TableRow.displayName = "TableRow" ));
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef(({ className, ...props }, ref) => ( const TableHead = React.forwardRef(({ className, ...props }, ref) => (
<th <th
ref={ref} ref={ref}
className={cn( className={cn(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", "p-4 text-left align-middle font-semibold text-[var(--purple-ink)] [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className className,
)} )}
{...props} /> {...props}
)) />
TableHead.displayName = "TableHead" ));
TableHead.displayName = "TableHead";
const TableCell = React.forwardRef(({ className, ...props }, ref) => ( const TableCell = React.forwardRef(({ className, ...props }, ref) => (
<td <td
ref={ref} ref={ref}
className={cn( className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", "p-4 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] ",
className className,
)} )}
{...props} /> {...props}
)) />
TableCell.displayName = "TableCell" ));
TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef(({ className, ...props }, ref) => ( const TableCaption = React.forwardRef(({ className, ...props }, ref) => (
<caption <caption
ref={ref} ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)} className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props} /> {...props}
)) />
TableCaption.displayName = "TableCaption" ));
TableCaption.displayName = "TableCaption";
export { export {
Table, Table,
@@ -83,4 +96,4 @@ export {
TableRow, TableRow,
TableCell, TableCell,
TableCaption, TableCaption,
} };

View File

@@ -1,31 +1,33 @@
import * as React from "react" import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs" import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Tabs = TabsPrimitive.Root const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef(({ className, ...props }, ref) => ( const TabsList = React.forwardRef(({ className, ...props }, ref) => (
<TabsPrimitive.List <TabsPrimitive.List
ref={ref} ref={ref}
className={cn( className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground", "inline-flex h-full items-center justify-center rounded-lg gap-6 p-1 text-muted-foreground",
className className
)} )}
{...props} /> {...props}
)) />
TabsList.displayName = TabsPrimitive.List.displayName ));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => ( const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger <TabsPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow", "inline-flex items-center justify-center whitespace-nowrap hover:bg-[var(--lavender-300)] border-2 border-brand-purple rounded-2xl px-3 py-1 text-brand-purple text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-foreground data-[state=active]:text-background data-[state=active]:border-foreground data-[state=active]:shadow dark:data-[state=active]:bg-brand-light-lavender dark:data-[state=active]:text-background dark:border-brand-light-lavender dark:text-brand-light-lavender",
className className
)} )}
{...props} /> {...props}
)) />
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName ));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef(({ className, ...props }, ref) => ( const TabsContent = React.forwardRef(({ className, ...props }, ref) => (
<TabsPrimitive.Content <TabsPrimitive.Content
@@ -34,8 +36,9 @@ const TabsContent = React.forwardRef(({ className, ...props }, ref) => (
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className className
)} )}
{...props} /> {...props}
)) />
TabsContent.displayName = TabsPrimitive.Content.displayName ));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent } export { Tabs, TabsList, TabsTrigger, TabsContent };

70
src/config/MemberTiers.js Normal file
View File

@@ -0,0 +1,70 @@
// src/config/memberTiers.js
/**
* Default member tier configuration
* Used as fallback when API is unavailable
* Format matches backend MemberTier interface
*/
export const DEFAULT_MEMBER_TIERS = [
{
id: 'new_member',
label: 'New Member',
minYears: 0,
maxYears: 0.999,
iconKey: 'sparkle',
badgeClass: 'bg-blue-100 text-blue-800 border-blue-200',
},
{
id: 'member_1_year',
label: '1 Year Member',
minYears: 1,
maxYears: 2.999,
iconKey: 'star',
badgeClass: 'bg-green-100 text-green-800 border-green-200',
},
{
id: 'member_3_year',
label: '3+ Year Member',
minYears: 3,
maxYears: 4.999,
iconKey: 'award',
badgeClass: 'bg-purple-100 text-purple-800 border-purple-200',
},
{
id: 'veteran',
label: 'Veteran Member',
minYears: 5,
maxYears: 999,
iconKey: 'crown',
badgeClass: 'bg-amber-100 text-amber-800 border-amber-200',
},
];
/**
* Available icon options for tier configuration
*/
export const TIER_ICON_OPTIONS = [
{ key: 'sparkle', label: 'Sparkle' },
{ key: 'star', label: 'Star' },
{ key: 'award', label: 'Award' },
{ key: 'crown', label: 'Crown' },
{ key: 'medal', label: 'Medal' },
{ key: 'trophy', label: 'Trophy' },
{ key: 'gem', label: 'Gem' },
{ key: 'heart', label: 'Heart' },
{ key: 'shield', label: 'Shield' },
];
/**
* Available badge color presets
*/
export const BADGE_COLOR_PRESETS = [
{ label: 'Blue', badgeClass: 'bg-blue-100 text-blue-800 border-blue-200' },
{ label: 'Green', badgeClass: 'bg-green-100 text-green-800 border-green-200' },
{ label: 'Purple', badgeClass: 'bg-purple-100 text-purple-800 border-purple-200' },
{ label: 'Amber', badgeClass: 'bg-amber-100 text-amber-800 border-amber-200' },
{ label: 'Red', badgeClass: 'bg-red-100 text-red-800 border-red-200' },
{ label: 'Teal', badgeClass: 'bg-teal-100 text-teal-800 border-teal-200' },
{ label: 'Pink', badgeClass: 'bg-pink-100 text-pink-800 border-pink-200' },
{ label: 'Indigo', badgeClass: 'bg-indigo-100 text-indigo-800 border-indigo-200' },
];

View File

@@ -0,0 +1,29 @@
// src/config/memberTierIcons.js
import { User, Star, Crown, Award, Sparkles, Medal, Trophy, Gem, Heart, Shield } from 'lucide-react';
/**
* Member tier icon mapping
* Maps iconKey strings from backend to Lucide React components
*/
export const MEMBER_TIER_ICONS = {
// Primary tier icons
sparkle: Sparkles,
sparkles: Sparkles,
star: Star,
award: Award,
crown: Crown,
// Additional options
medal: Medal,
trophy: Trophy,
gem: Gem,
heart: Heart,
shield: Shield,
user: User,
};
/**
* Get icon component by key with fallback
*/
export const getTierIcon = (iconKey) => {
return MEMBER_TIER_ICONS[iconKey?.toLowerCase()] || MEMBER_TIER_ICONS.sparkle;
};

View File

@@ -1,9 +1,18 @@
import React, { createContext, useState, useContext, useEffect } from 'react'; import React, { createContext, useState, useContext, useEffect } from 'react';
import axios from 'axios'; import axios from 'axios';
import api from '../utils/api';
import logger from '../utils/logger';
const AuthContext = createContext(); const AuthContext = createContext();
const API_URL = process.env.REACT_APP_BACKEND_URL; const API_URL = process.env.REACT_APP_BACKEND_URL || window.location.origin;
// Log environment on module load for debugging
logger.log('[AuthContext] Module initialized with:', {
REACT_APP_BACKEND_URL: process.env.REACT_APP_BACKEND_URL,
REACT_APP_BASENAME: process.env.REACT_APP_BASENAME,
API_URL: API_URL
});
export const AuthProvider = ({ children }) => { export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
@@ -48,22 +57,98 @@ export const AuthProvider = ({ children }) => {
}); });
setPermissions(response.data.permissions || []); setPermissions(response.data.permissions || []);
} catch (error) { } catch (error) {
console.error('Failed to fetch permissions:', error); logger.error('Failed to fetch permissions:', error);
setPermissions([]); setPermissions([]);
} }
}; };
const login = async (email, password) => { const login = async (email, password) => {
const response = await axios.post(`${API_URL}/api/auth/login`, { email, password }); try {
logger.log('[AuthContext] Starting login request...', {
API_URL: API_URL,
envBackendUrl: process.env.REACT_APP_BACKEND_URL,
fullUrl: `${API_URL}/api/auth/login`
});
// Use api instance for retry logic
const response = await api.post(
'/auth/login',
{ email, password },
{
headers: {
'Content-Type': 'application/json'
}
}
);
logger.log('[AuthContext] Login response received:', {
status: response.status,
hasToken: !!response.data?.access_token,
hasUser: !!response.data?.user
});
const { access_token, user: userData } = response.data; const { access_token, user: userData } = response.data;
if (!access_token || !userData) {
throw new Error('Invalid response from server - missing token or user data');
}
// Store token FIRST and verify it was stored
localStorage.setItem('token', access_token); localStorage.setItem('token', access_token);
const storedToken = localStorage.getItem('token');
if (storedToken !== access_token) {
throw new Error('Failed to store token in localStorage');
}
logger.log('[AuthContext] Token stored and verified in localStorage');
// Update state in correct order
setToken(access_token); setToken(access_token);
setUser(userData); setUser(userData);
logger.log('[AuthContext] User state updated:', {
email: userData.email,
role: userData.role
});
// Fetch user permissions // Fetch permissions immediately and WAIT for it (but don't fail login if it fails)
try {
logger.log('[AuthContext] Fetching permissions...');
await fetchPermissions(access_token); await fetchPermissions(access_token);
logger.log('[AuthContext] Permissions fetched successfully');
} catch (permError) {
logger.error('[AuthContext] Failed to fetch permissions (non-critical):', {
message: permError.message,
response: permError.response?.data,
status: permError.response?.status
});
// Set empty permissions array so hasPermission doesn't break
setPermissions([]);
// Don't throw - login succeeded even if permissions failed
}
return userData; return userData;
} catch (error) {
// Enhanced error logging
logger.error('[AuthContext] Login failed:', {
message: error.message,
response: error.response?.data,
status: error.response?.status,
code: error.code,
config: {
url: error.config?.url,
method: error.config?.method,
timeout: error.config?.timeout
}
});
// Clear any partial state
localStorage.removeItem('token');
setToken(null);
setUser(null);
setPermissions([]);
// Re-throw to let Login component handle the error
throw error;
}
}; };
const logout = () => { const logout = () => {
@@ -90,7 +175,7 @@ export const AuthProvider = ({ children }) => {
setUser(response.data); setUser(response.data);
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error('Failed to refresh user:', error); logger.error('Failed to refresh user:', error);
// If token expired, logout // If token expired, logout
if (error.response?.status === 401) { if (error.response?.status === 401) {
logout(); logout();

View File

@@ -0,0 +1,161 @@
import React, { createContext, useState, useContext, useEffect, useCallback } from 'react';
import axios from 'axios';
const ThemeConfigContext = createContext();
const API_URL = process.env.REACT_APP_BACKEND_URL || window.location.origin;
const DEFAULT_THEME = {
site_name: 'LOAF - Lesbians Over Age Fifty',
site_short_name: 'LOAF',
site_description: 'A community organization for lesbians over age fifty in Houston and surrounding areas.',
logo_url: null,
favicon_url: null,
colors: {
primary: '280 47% 27%',
primary_foreground: '0 0% 100%',
accent: '24 86% 55%',
brand_purple: '256 35% 47%',
brand_orange: '24 86% 55%',
brand_lavender: '262 46% 80%'
},
meta_theme_color: '#664fa3'
};
export const ThemeConfigProvider = ({ children }) => {
const [themeConfig, setThemeConfig] = useState(DEFAULT_THEME);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const applyThemeToDOM = useCallback((config) => {
// Apply CSS variables for colors
if (config.colors) {
const root = document.documentElement;
Object.entries(config.colors).forEach(([key, value]) => {
// Convert snake_case to kebab-case for CSS variable names
const cssVarName = `--${key.replace(/_/g, '-')}`;
root.style.setProperty(cssVarName, value);
});
}
// Update favicon
if (config.favicon_url) {
let link = document.querySelector("link[rel*='icon']");
if (!link) {
link = document.createElement('link');
link.rel = 'icon';
document.head.appendChild(link);
}
link.href = config.favicon_url;
}
// Update document title
if (config.site_name) {
document.title = config.site_name;
// Also store for use by pages that want to append their own title
window.__SITE_NAME__ = config.site_name;
}
// Update meta description
if (config.site_description) {
let metaDesc = document.querySelector("meta[name='description']");
if (!metaDesc) {
metaDesc = document.createElement('meta');
metaDesc.name = 'description';
document.head.appendChild(metaDesc);
}
metaDesc.content = config.site_description;
}
// Update meta theme-color for PWA
if (config.meta_theme_color) {
let meta = document.querySelector("meta[name='theme-color']");
if (!meta) {
meta = document.createElement('meta');
meta.name = 'theme-color';
document.head.appendChild(meta);
}
meta.content = config.meta_theme_color;
}
}, []);
const fetchThemeConfig = useCallback(async () => {
try {
setLoading(true);
setError(null);
const response = await axios.get(`${API_URL}/api/config/theme`);
const config = { ...DEFAULT_THEME, ...response.data };
// Merge colors if provided
if (response.data.colors) {
config.colors = { ...DEFAULT_THEME.colors, ...response.data.colors };
}
setThemeConfig(config);
applyThemeToDOM(config);
} catch (err) {
console.warn('Failed to fetch theme config, using defaults:', err.message);
setError(err.message);
// Apply default theme to DOM
applyThemeToDOM(DEFAULT_THEME);
} finally {
setLoading(false);
}
}, [applyThemeToDOM]);
// Fetch theme config on mount
useEffect(() => {
fetchThemeConfig();
}, [fetchThemeConfig]);
// Helper function to get logo URL with fallback
const getLogoUrl = useCallback(() => {
return themeConfig.logo_url || `${process.env.PUBLIC_URL}/loaf-logo.png`;
}, [themeConfig.logo_url]);
// Helper function to get favicon URL with fallback
const getFaviconUrl = useCallback(() => {
return themeConfig.favicon_url || `${process.env.PUBLIC_URL}/favicon.ico`;
}, [themeConfig.favicon_url]);
const value = {
// Theme configuration
themeConfig,
loading,
error,
// Convenience accessors
siteName: themeConfig.site_name,
siteShortName: themeConfig.site_short_name,
siteDescription: themeConfig.site_description,
colors: themeConfig.colors,
metaThemeColor: themeConfig.meta_theme_color,
// Helper functions
getLogoUrl,
getFaviconUrl,
// Actions
refreshTheme: fetchThemeConfig,
// Default theme for reference
DEFAULT_THEME
};
return (
<ThemeConfigContext.Provider value={value}>
{children}
</ThemeConfigContext.Provider>
);
};
export const useThemeConfig = () => {
const context = useContext(ThemeConfigContext);
if (context === undefined) {
throw new Error('useThemeConfig must be used within a ThemeConfigProvider');
}
return context;
};
export default ThemeConfigContext;

View File

@@ -0,0 +1,93 @@
import React, { createContext, useState, useContext, useEffect, useCallback, useMemo } from 'react';
import { toast } from 'sonner';
import api from '../utils/api';
const UsersContext = createContext();
// Role definitions
const STAFF_ROLES = ['admin', 'superadmin', 'finance'];
const MEMBER_ROLES = ['member'];
export const UsersProvider = ({ children }) => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchUsers = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await api.get('/admin/users');
setUsers(response.data);
} catch (err) {
setError(err);
toast.error('Failed to fetch users');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchUsers();
}, [fetchUsers]);
// Filtered views based on role
const staff = useMemo(
() => users.filter(user => STAFF_ROLES.includes(user.role)),
[users]
);
const members = useMemo(
() => users.filter(user => MEMBER_ROLES.includes(user.role)),
[users]
);
const allUsers = users;
// Update a single user in the local state (useful after edits)
const updateUser = useCallback((updatedUser) => {
setUsers(prev => prev.map(user =>
user.id === updatedUser.id ? updatedUser : user
));
}, []);
// Remove a user from local state
const removeUser = useCallback((userId) => {
setUsers(prev => prev.filter(user => user.id !== userId));
}, []);
// Add a user to local state
const addUser = useCallback((newUser) => {
setUsers(prev => [...prev, newUser]);
}, []);
return (
<UsersContext.Provider value={{
// All data
users: allUsers,
staff,
members,
// State
loading,
error,
// Actions
refetch: fetchUsers,
updateUser,
removeUser,
addUser,
}}>
{children}
</UsersContext.Provider>
);
};
// Base hook to access the context
export const useUsers = () => {
const context = useContext(UsersContext);
if (!context) {
throw new Error('useUsers must be used within a UsersProvider');
}
return context;
};
export default UsersContext;

View File

@@ -0,0 +1,106 @@
// src/hooks/use-member-tiers.js
import { useState, useEffect, useCallback } from 'react';
import api from '../utils/api';
import { DEFAULT_MEMBER_TIERS } from '../config/MemberTiers';
/**
* Hook for fetching and managing member tier configuration
* @param {Object} options
* @param {boolean} options.isAdmin - Whether to use admin endpoint (includes metadata)
* @returns {Object} Tier state and methods
*/
const useMemberTiers = ({ isAdmin = false } = {}) => {
const [tiers, setTiers] = useState(DEFAULT_MEMBER_TIERS);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [saving, setSaving] = useState(false);
const endpoint = isAdmin
? '/admin/settings/member-tiers'
: '/settings/member-tiers';
const fetchTiers = useCallback(async () => {
try {
setLoading(true);
setError(null);
const response = await api.get(endpoint);
const data = response.data?.tiers || response.data || DEFAULT_MEMBER_TIERS;
setTiers(data);
} catch (err) {
console.error('Failed to fetch member tiers:', err);
setError('Failed to load member tiers');
// Use defaults on error
setTiers(DEFAULT_MEMBER_TIERS);
} finally {
setLoading(false);
}
}, [endpoint]);
useEffect(() => {
fetchTiers();
}, [fetchTiers]);
/**
* Update tier configuration (admin only)
* @param {Array} newTiers - Updated tier array
* @returns {Promise<boolean>} Success status
*/
const updateTiers = useCallback(async (newTiers) => {
if (!isAdmin) {
console.error('updateTiers requires admin access');
return false;
}
try {
setSaving(true);
setError(null);
await api.put('/admin/settings/member-tiers', { tiers: newTiers });
setTiers(newTiers);
return true;
} catch (err) {
console.error('Failed to update member tiers:', err);
setError('Failed to save member tiers');
return false;
} finally {
setSaving(false);
}
}, [isAdmin]);
/**
* Reset tiers to defaults (superadmin only)
* @returns {Promise<boolean>} Success status
*/
const resetToDefaults = useCallback(async () => {
if (!isAdmin) {
console.error('resetToDefaults requires admin access');
return false;
}
try {
setSaving(true);
setError(null);
const response = await api.post('/admin/settings/member-tiers/reset');
const data = response.data?.tiers || response.data || DEFAULT_MEMBER_TIERS;
setTiers(data);
return true;
} catch (err) {
console.error('Failed to reset member tiers:', err);
setError('Failed to reset member tiers');
return false;
} finally {
setSaving(false);
}
}, [isAdmin]);
return {
tiers,
loading,
error,
saving,
fetchTiers,
updateTiers,
resetToDefaults,
};
};
export default useMemberTiers;

90
src/hooks/use-members.js Normal file
View File

@@ -0,0 +1,90 @@
import { useCallback, useEffect, useState } from 'react';
import { toast } from 'sonner';
import api from '../utils/api';
const DEFAULT_SEARCH_FIELDS = ['first_name', 'last_name', 'email'];
/**
* Hook for fetching users from a custom endpoint (e.g., member-facing directory).
* For admin pages, use hooks from use-users.js instead which share a centralized context.
*/
const useMembers = ({
endpoint = '/admin/users',
initialFilter = 'active',
initialSearch = '',
filterKey = 'status',
allowedRoles = ['member'],
searchFields = DEFAULT_SEARCH_FIELDS,
fetchErrorMessage = 'Failed to fetch members',
searchAccessor,
transform,
onFetchError,
} = {}) => {
const [users, setUsers] = useState([]);
const [filteredUsers, setFilteredUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState(initialSearch);
const [filterValue, setFilterValue] = useState(initialFilter);
const fetchMembers = useCallback(async () => {
try {
const response = await api.get(endpoint);
let filtered = response.data;
if (typeof transform === 'function') {
filtered = transform(filtered);
}
if (allowedRoles && allowedRoles.length) {
filtered = filtered.filter(user => allowedRoles.includes(user.role));
}
setUsers(filtered);
} catch (error) {
if (typeof onFetchError === 'function') {
onFetchError(error);
} else {
toast.error(fetchErrorMessage);
}
} finally {
setLoading(false);
}
}, [allowedRoles, endpoint, fetchErrorMessage, onFetchError, transform]);
useEffect(() => {
fetchMembers();
}, [fetchMembers]);
useEffect(() => {
let filtered = users;
if (filterValue && filterValue !== 'all') {
filtered = filtered.filter(user => user[filterKey] === filterValue);
}
if (searchQuery) {
const query = searchQuery.toLowerCase();
filtered = filtered.filter(user => {
const values = typeof searchAccessor === 'function'
? searchAccessor(user)
: searchFields.map(field => user?.[field]);
return values
.filter(Boolean)
.some(value => value.toString().toLowerCase().includes(query));
});
}
setFilteredUsers(filtered);
}, [users, searchQuery, filterKey, filterValue, searchAccessor, searchFields]);
return {
users,
filteredUsers,
loading,
searchQuery,
setSearchQuery,
filterValue,
setFilterValue,
fetchMembers,
};
};
export default useMembers;

171
src/hooks/use-users.js Normal file
View File

@@ -0,0 +1,171 @@
import { useState, useMemo } from 'react';
import { useUsers } from '../context/UsersContext';
const DEFAULT_SEARCH_FIELDS = ['first_name', 'last_name', 'email'];
/**
* Base hook that adds search and filter functionality to any user list
*/
const useFilteredUsers = ({
users,
initialFilter = 'all',
filterKey = 'status',
searchFields = DEFAULT_SEARCH_FIELDS,
searchAccessor,
}) => {
const [searchQuery, setSearchQuery] = useState('');
const [filterValue, setFilterValue] = useState(initialFilter);
const filteredUsers = useMemo(() => {
let filtered = users;
// Apply filter
if (filterValue && filterValue !== 'all') {
filtered = filtered.filter(user => user[filterKey] === filterValue);
}
// Apply search
if (searchQuery) {
const query = searchQuery.toLowerCase();
filtered = filtered.filter(user => {
const values = typeof searchAccessor === 'function'
? searchAccessor(user)
: searchFields.map(field => user?.[field]);
return values
.filter(Boolean)
.some(value => value.toString().toLowerCase().includes(query));
});
}
return filtered;
}, [users, searchQuery, filterKey, filterValue, searchAccessor, searchFields]);
return {
filteredUsers,
searchQuery,
setSearchQuery,
filterValue,
setFilterValue,
};
};
/**
* Hook for staff users (admin, superadmin, finance roles)
*/
export const useStaff = ({
initialFilter = 'all',
filterKey = 'role',
searchFields = DEFAULT_SEARCH_FIELDS,
searchAccessor,
} = {}) => {
const { staff, loading, error, refetch, updateUser, removeUser } = useUsers();
const {
filteredUsers,
searchQuery,
setSearchQuery,
filterValue,
setFilterValue,
} = useFilteredUsers({
users: staff,
initialFilter,
filterKey,
searchFields,
searchAccessor,
});
return {
users: staff,
filteredUsers,
loading,
error,
searchQuery,
setSearchQuery,
filterValue,
setFilterValue,
refetch,
updateUser,
removeUser,
};
};
/**
* Hook for member users (non-admin roles)
*/
export const useMembers = ({
initialFilter = 'active',
filterKey = 'status',
searchFields = DEFAULT_SEARCH_FIELDS,
searchAccessor,
} = {}) => {
const { members, loading, error, refetch, updateUser, removeUser } = useUsers();
const {
filteredUsers,
searchQuery,
setSearchQuery,
filterValue,
setFilterValue,
} = useFilteredUsers({
users: members,
initialFilter,
filterKey,
searchFields,
searchAccessor,
});
return {
users: members,
filteredUsers,
loading,
error,
searchQuery,
setSearchQuery,
filterValue,
setFilterValue,
refetch,
updateUser,
removeUser,
};
};
/**
* Hook for all users (both staff and members)
*/
export const useAllUsers = ({
initialFilter = 'all',
filterKey = 'status',
searchFields = DEFAULT_SEARCH_FIELDS,
searchAccessor,
} = {}) => {
const { users, loading, error, refetch, updateUser, removeUser } = useUsers();
const {
filteredUsers,
searchQuery,
setSearchQuery,
filterValue,
setFilterValue,
} = useFilteredUsers({
users,
initialFilter,
filterKey,
searchFields,
searchAccessor,
});
return {
users,
filteredUsers,
loading,
error,
searchQuery,
setSearchQuery,
filterValue,
setFilterValue,
refetch,
updateUser,
removeUser,
};
};

View File

@@ -1,115 +1,10 @@
@tailwind base; @import "./styles/App.css";
@tailwind components; @import "./styles/theme.css";
@tailwind utilities; @import "./styles/components.css";
@import "./styles/base.css";
body { @import "./styles/utilities.css";
margin: 0; /*
font-family: =========================
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", End of File
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", ========================
sans-serif; */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family:
source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 280 47% 27%;
--card: 0 0% 100%;
--card-foreground: 280 47% 27%;
--popover: 0 0% 100%;
--popover-foreground: 280 47% 27%;
--primary: 280 47% 27%;
--primary-foreground: 0 0% 100%;
--secondary: 268 33% 89%;
--secondary-foreground: 280 47% 27%;
--muted: 268 43% 95%;
--muted-foreground: 268 35% 47%;
--accent: 17 100% 73%;
--accent-foreground: 280 47% 27%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 268 33% 89%;
--input: 268 33% 89%;
--ring: 268 35% 47%;
--chart-1: 268 36% 46%;
--chart-2: 17 100% 73%;
--chart-3: 268 33% 89%;
--chart-4: 280 44% 29%;
--chart-5: 268 35% 47%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
@layer base {
[data-debug-wrapper="true"] {
display: contents !important;
}
[data-debug-wrapper="true"] > * {
margin-left: inherit;
margin-right: inherit;
margin-top: inherit;
margin-bottom: inherit;
padding-left: inherit;
padding-right: inherit;
padding-top: inherit;
padding-bottom: inherit;
column-gap: inherit;
row-gap: inherit;
gap: inherit;
border-left-width: inherit;
border-right-width: inherit;
border-top-width: inherit;
border-bottom-width: inherit;
border-left-style: inherit;
border-right-style: inherit;
border-top-style: inherit;
border-bottom-style: inherit;
border-left-color: inherit;
border-right-color: inherit;
border-top-color: inherit;
border-bottom-color: inherit;
}
}

View File

@@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import { ThemeProvider } from 'next-themes';
import { ThemeConfigProvider } from './context/ThemeConfigContext';
import '@fontsource/fraunces/600.css'; import '@fontsource/fraunces/600.css';
import '@fontsource/dm-sans/400.css'; import '@fontsource/dm-sans/400.css';
import '@fontsource/dm-sans/700.css'; import '@fontsource/dm-sans/700.css';
@@ -9,6 +11,15 @@ import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root')); const root = ReactDOM.createRoot(document.getElementById('root'));
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<ThemeProvider
attribute="data-theme"
defaultTheme="light"
enableSystem={false}
storageKey="admin-theme"
>
<ThemeConfigProvider>
<App /> <App />
</ThemeConfigProvider>
</ThemeProvider>
</React.StrictMode> </React.StrictMode>
); );

View File

@@ -1,9 +1,14 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useTheme } from 'next-themes';
import { Menu } from 'lucide-react';
import AdminSidebar from '../components/AdminSidebar'; import AdminSidebar from '../components/AdminSidebar';
import { UsersProvider } from '../context/UsersContext';
const AdminLayout = ({ children }) => { const AdminLayout = ({ children }) => {
const [sidebarOpen, setSidebarOpen] = useState(true); const [sidebarOpen, setSidebarOpen] = useState(true);
const [isMobile, setIsMobile] = useState(false); const [isMobile, setIsMobile] = useState(false);
const { theme } = useTheme();
const isDark = theme === 'dark';
// Initialize sidebar state from localStorage // Initialize sidebar state from localStorage
useEffect(() => { useEffect(() => {
@@ -43,7 +48,8 @@ const AdminLayout = ({ children }) => {
}; };
return ( return (
<div className="flex h-screen bg-white"> <UsersProvider>
<div className={`flex h-screen bg-background ${isDark ? 'dark' : ''}`}>
{/* Sidebar */} {/* Sidebar */}
<AdminSidebar <AdminSidebar
isOpen={sidebarOpen} isOpen={sidebarOpen}
@@ -60,12 +66,30 @@ const AdminLayout = ({ children }) => {
)} )}
{/* Main Content Area */} {/* Main Content Area */}
<main className="flex-1 overflow-y-auto"> <main className="flex-1 overflow-y-auto scrollbar-dashboard">
{isMobile && (
<div className="sticky top-0 z-20 bg-background/90 backdrop-blur border-b border-[var(--neutral-800)] px-4 py-3 flex items-center gap-3">
<button
onClick={toggleSidebar}
className="p-2 rounded-lg hover:bg-[var(--neutral-800)]/20 transition-colors"
aria-label={sidebarOpen ? 'Close sidebar' : 'Open sidebar'}
>
<Menu className="h-5 w-5 text-primary" />
</button>
<span
className="text-sm font-semibold text-primary"
style={{ fontFamily: "'Inter', sans-serif" }}
>
Menu
</span>
</div>
)}
<div className="max-w-7xl mx-auto px-6 py-8"> <div className="max-w-7xl mx-auto px-6 py-8">
{children} {children}
</div> </div>
</main> </main>
</div> </div>
</UsersProvider>
); );
}; };

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { Outlet } from 'react-router-dom';
import SettingsTabs from '../components/SettingsSidebar';
import { Settings } from 'lucide-react';
const SettingsLayout = () => {
return (
<div className="space-y-6">
{/* Header */}
<div>
<h1 className="text-2xl font-bold text-foreground flex items-center gap-2">
<Settings className="h-6 w-6" />
Settings
</h1>
<p className="text-muted-foreground mt-1">
Manage your platform configuration and preferences
</p>
</div>
{/* Tabs Navigation */}
<SettingsTabs />
{/* Content Area */}
<div className="min-w-0">
<Outlet />
</div>
</div>
);
};
export default SettingsLayout;

View File

@@ -19,6 +19,8 @@ const AcceptInvitation = () => {
const [invitation, setInvitation] = useState(null); const [invitation, setInvitation] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [success, setSuccess] = useState(false);
const [successUser, setSuccessUser] = useState(null);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
password: '', password: '',
@@ -134,19 +136,23 @@ const AcceptInvitation = () => {
const { access_token, user } = response.data; const { access_token, user } = response.data;
localStorage.setItem('token', access_token); localStorage.setItem('token', access_token);
toast.success('Welcome to LOAF! Your account has been created successfully.');
// Call login to update auth context // Call login to update auth context
if (login) { if (login) {
await login(invitation.email, formData.password); await login(invitation.email, formData.password);
} }
// Redirect based on role // Show success state
setSuccessUser(user);
setSuccess(true);
// Auto-redirect after 3 seconds
setTimeout(() => {
if (user.role === 'admin' || user.role === 'superadmin') { if (user.role === 'admin' || user.role === 'superadmin') {
navigate('/admin/dashboard'); navigate('/admin');
} else { } else {
navigate('/dashboard'); navigate('/dashboard');
} }
}, 3000);
} catch (error) { } catch (error) {
const errorMessage = error.response?.data?.detail || 'Failed to accept invitation'; const errorMessage = error.response?.data?.detail || 'Failed to accept invitation';
toast.error(errorMessage); toast.error(errorMessage);
@@ -157,9 +163,9 @@ const AcceptInvitation = () => {
const getRoleBadge = (role) => { const getRoleBadge = (role) => {
const config = { const config = {
superadmin: { label: 'Superadmin', className: 'bg-[#664fa3] text-white' }, superadmin: { label: 'Superadmin', className: 'bg-brand-purple text-white' },
admin: { label: 'Admin', className: 'bg-[#81B29A] text-white' }, admin: { label: 'Admin', className: 'bg-[var(--green-light)] text-white' },
member: { label: 'Member', className: 'bg-[#DDD8EB] text-[#422268]' } member: { label: 'Member', className: 'bg-[var(--neutral-800)] text-[var(--purple-ink)]' }
}; };
const roleConfig = config[role] || { label: role, className: 'bg-gray-500 text-white' }; const roleConfig = config[role] || { label: role, className: 'bg-gray-500 text-white' };
@@ -173,10 +179,10 @@ const AcceptInvitation = () => {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-gradient-to-br from-[#F9F8FB] to-white flex items-center justify-center p-4"> <div className="min-h-screen bg-gradient-to-br from-[var(--lavender-700)] to-white flex items-center justify-center p-4">
<Card className="w-full max-w-md p-12 bg-white rounded-2xl border border-[#ddd8eb] text-center"> <Card className="w-full max-w-md p-12 bg-background rounded-2xl border border-[var(--neutral-800)] text-center">
<Loader2 className="h-12 w-12 text-[#664fa3] mx-auto mb-4 animate-spin" /> <Loader2 className="h-12 w-12 text-brand-purple mx-auto mb-4 animate-spin" />
<p className="text-lg text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <p className="text-lg text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Verifying your invitation... Verifying your invitation...
</p> </p>
</Card> </Card>
@@ -186,18 +192,18 @@ const AcceptInvitation = () => {
if (error) { if (error) {
return ( return (
<div className="min-h-screen bg-gradient-to-br from-[#F9F8FB] to-white flex items-center justify-center p-4"> <div className="min-h-screen bg-gradient-to-br from-[var(--lavender-700)] to-white flex items-center justify-center p-4">
<Card className="w-full max-w-md p-12 bg-white rounded-2xl border border-[#ddd8eb] text-center"> <Card className="w-full max-w-md p-12 bg-background rounded-2xl border border-[var(--neutral-800)] text-center">
<XCircle className="h-16 w-16 text-red-500 mx-auto mb-6" /> <XCircle className="h-16 w-16 text-red-500 mx-auto mb-6" />
<h1 className="text-2xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h1 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Invalid Invitation Invalid Invitation
</h1> </h1>
<p className="text-[#664fa3] mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{error} {error}
</p> </p>
<Button <Button
onClick={() => navigate('/login')} onClick={() => navigate('/login')}
className="rounded-xl bg-[#664fa3] hover:bg-[#422268] text-white" className="rounded-xl bg-brand-purple hover:bg-[var(--purple-ink)] text-white"
> >
Go to Login Go to Login
</Button> </Button>
@@ -206,47 +212,124 @@ const AcceptInvitation = () => {
); );
} }
if (success) {
const redirectPath = successUser?.role === 'admin' || successUser?.role === 'superadmin' ? '/admin' : '/dashboard';
return ( return (
<div className="min-h-screen bg-gradient-to-br from-[#F9F8FB] to-white flex items-center justify-center p-4"> <div className="min-h-screen bg-gradient-to-br from-[var(--lavender-700)] to-white flex items-center justify-center p-4">
<Card className="w-full max-w-3xl p-8 md:p-12 bg-white rounded-2xl border border-[#ddd8eb]"> <Card className="w-full max-w-2xl p-12 bg-background rounded-2xl border border-[var(--neutral-800)] text-center">
{/* Success Animation */}
<div className="mb-8">
<div className="h-24 w-24 mx-auto rounded-full bg-gradient-to-br from-[var(--green-light)] to-[var(--green-fern)] flex items-center justify-center animate-bounce">
<CheckCircle className="h-12 w-12 text-white" />
</div>
</div>
{/* Success Message */}
<h1 className="text-4xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Welcome to LOAF! 🎉
</h1>
<p className="text-xl text-brand-purple mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Your account has been created successfully.
</p>
{/* User Info Card */}
<div className="mb-8 p-6 bg-gradient-to-r from-[var(--neutral-800)] to-[var(--lavender-700)] rounded-xl">
<div className="grid md:grid-cols-2 gap-4 text-left">
<div>
<p className="text-sm text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Name
</p>
<p className="font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
{successUser?.first_name} {successUser?.last_name}
</p>
</div>
<div>
<p className="text-sm text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Email
</p>
<p className="font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
{successUser?.email}
</p>
</div>
<div>
<p className="text-sm text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Role
</p>
<div>{getRoleBadge(successUser?.role)}</div>
</div>
<div>
<p className="text-sm text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Status
</p>
<Badge className="bg-[var(--green-light)] text-white px-4 py-2 rounded-full text-sm">
{successUser?.status}
</Badge>
</div>
</div>
</div>
{/* Redirect Info */}
<div className="mb-8 p-4 bg-blue-50 border border-blue-200 rounded-xl">
<p className="text-sm text-blue-800" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<Loader2 className="h-4 w-4 inline mr-2 animate-spin" />
Redirecting you to your dashboard in 3 seconds...
</p>
</div>
{/* Manual Continue Button */}
<Button
onClick={() => navigate(redirectPath)}
className="w-full h-14 rounded-xl bg-gradient-to-r from-[var(--green-light)] to-[var(--green-fern)] hover:from-[var(--green-fern)] hover:to-[var(--green-sage)] text-white text-lg font-semibold"
>
Continue to Dashboard
</Button>
</Card>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-[var(--lavender-700)] to-white flex items-center justify-center p-4">
<Card className="w-full max-w-3xl p-8 md:p-12 bg-background rounded-2xl border border-[var(--neutral-800)]">
{/* Header */} {/* Header */}
<div className="text-center mb-8"> <div className="text-center mb-8">
<div className="flex justify-center mb-4"> <div className="flex justify-center mb-4">
<div className="h-16 w-16 rounded-full bg-gradient-to-br from-[#664fa3] to-[#422268] flex items-center justify-center"> <div className="h-16 w-16 rounded-full bg-gradient-to-br from-brand-purple to-[var(--purple-ink)] flex items-center justify-center">
<Mail className="h-8 w-8 text-white" /> <Mail className="h-8 w-8 text-white" />
</div> </div>
</div> </div>
<h1 className="text-3xl md:text-4xl font-semibold text-[#422268] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}> <h1 className="text-3xl md:text-4xl font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
Welcome to LOAF! Welcome to LOAF!
</h1> </h1>
<p className="text-lg text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Complete your profile to accept the invitation Complete your profile to accept the invitation
</p> </p>
</div> </div>
{/* Invitation Details */} {/* Invitation Details */}
<div className="mb-8 p-6 bg-gradient-to-r from-[#DDD8EB] to-[#F9F8FB] rounded-xl"> <div className="mb-8 p-6 bg-gradient-to-r from-[var(--neutral-800)] to-[var(--lavender-700)] rounded-xl">
<div className="grid md:grid-cols-2 gap-4 text-sm"> <div className="grid md:grid-cols-2 gap-4 text-sm">
<div> <div>
<p className="text-[#664fa3] mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Email Address Email Address
</p> </p>
<p className="font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <p className="font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
{invitation?.email} {invitation?.email}
</p> </p>
</div> </div>
<div> <div>
<p className="text-[#664fa3] mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Role Role
</p> </p>
<div>{getRoleBadge(invitation?.role)}</div> <div>{getRoleBadge(invitation?.role)}</div>
</div> </div>
<div className="md:col-span-2"> <div className="md:col-span-2">
<p className="text-[#664fa3] mb-1 flex items-center gap-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple mb-1 flex items-center gap-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<Calendar className="h-4 w-4" /> <Calendar className="h-4 w-4" />
Invitation Expires Invitation Expires
</p> </p>
<p className="font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <p className="font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
{invitation?.expires_at ? new Date(invitation.expires_at).toLocaleString() : 'N/A'} {invitation?.expires_at ? new Date(invitation.expires_at).toLocaleString() : 'N/A'}
</p> </p>
</div> </div>
@@ -259,7 +342,7 @@ const AcceptInvitation = () => {
{/* Password Fields */} {/* Password Fields */}
<div className="grid md:grid-cols-2 gap-4"> <div className="grid md:grid-cols-2 gap-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="password" className="text-[#422268]"> <Label htmlFor="password" className="text-[var(--purple-ink)]">
Password <span className="text-red-500">*</span> Password <span className="text-red-500">*</span>
</Label> </Label>
<Input <Input
@@ -267,7 +350,7 @@ const AcceptInvitation = () => {
type="password" type="password"
value={formData.password} value={formData.password}
onChange={(e) => handleChange('password', e.target.value)} onChange={(e) => handleChange('password', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="Minimum 8 characters" placeholder="Minimum 8 characters"
/> />
{formErrors.password && ( {formErrors.password && (
@@ -276,7 +359,7 @@ const AcceptInvitation = () => {
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="confirmPassword" className="text-[#422268]"> <Label htmlFor="confirmPassword" className="text-[var(--purple-ink)]">
Confirm Password <span className="text-red-500">*</span> Confirm Password <span className="text-red-500">*</span>
</Label> </Label>
<Input <Input
@@ -284,7 +367,7 @@ const AcceptInvitation = () => {
type="password" type="password"
value={formData.confirmPassword} value={formData.confirmPassword}
onChange={(e) => handleChange('confirmPassword', e.target.value)} onChange={(e) => handleChange('confirmPassword', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="Re-enter password" placeholder="Re-enter password"
/> />
{formErrors.confirmPassword && ( {formErrors.confirmPassword && (
@@ -296,15 +379,15 @@ const AcceptInvitation = () => {
{/* Name Fields */} {/* Name Fields */}
<div className="grid md:grid-cols-2 gap-4"> <div className="grid md:grid-cols-2 gap-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="first_name" className="text-[#422268]"> <Label htmlFor="first_name" className="text-[var(--purple-ink)]">
First Name <span className="text-red-500">*</span> First Name <span className="text-red-500">*</span>
</Label> </Label>
<Input <Input
id="first_name" id="first_name"
value={formData.first_name} value={formData.first_name}
onChange={(e) => handleChange('first_name', e.target.value)} onChange={(e) => handleChange('first_name', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="John" placeholder="Jane"
/> />
{formErrors.first_name && ( {formErrors.first_name && (
<p className="text-sm text-red-500">{formErrors.first_name}</p> <p className="text-sm text-red-500">{formErrors.first_name}</p>
@@ -312,14 +395,14 @@ const AcceptInvitation = () => {
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="last_name" className="text-[#422268]"> <Label htmlFor="last_name" className="text-[var(--purple-ink)]">
Last Name <span className="text-red-500">*</span> Last Name <span className="text-red-500">*</span>
</Label> </Label>
<Input <Input
id="last_name" id="last_name"
value={formData.last_name} value={formData.last_name}
onChange={(e) => handleChange('last_name', e.target.value)} onChange={(e) => handleChange('last_name', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="Doe" placeholder="Doe"
/> />
{formErrors.last_name && ( {formErrors.last_name && (
@@ -330,7 +413,7 @@ const AcceptInvitation = () => {
{/* Phone */} {/* Phone */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="phone" className="text-[#422268]"> <Label htmlFor="phone" className="text-[var(--purple-ink)]">
Phone <span className="text-red-500">*</span> Phone <span className="text-red-500">*</span>
</Label> </Label>
<Input <Input
@@ -338,7 +421,7 @@ const AcceptInvitation = () => {
type="tel" type="tel"
value={formData.phone} value={formData.phone}
onChange={(e) => handleChange('phone', e.target.value)} onChange={(e) => handleChange('phone', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="(555) 123-4567" placeholder="(555) 123-4567"
/> />
{formErrors.phone && ( {formErrors.phone && (
@@ -349,20 +432,20 @@ const AcceptInvitation = () => {
{/* Optional Fields Section */} {/* Optional Fields Section */}
{invitation?.role === 'member' && ( {invitation?.role === 'member' && (
<> <>
<div className="border-t border-[#ddd8eb] pt-6 mt-2"> <div className="border-t border-[var(--neutral-800)] pt-6 mt-2">
<h3 className="text-lg font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Additional Information (Optional) Additional Information (Optional)
</h3> </h3>
</div> </div>
{/* Address */} {/* Address */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="address" className="text-[#422268]">Address</Label> <Label htmlFor="address" className="text-[var(--purple-ink)]">Address</Label>
<Input <Input
id="address" id="address"
value={formData.address} value={formData.address}
onChange={(e) => handleChange('address', e.target.value)} onChange={(e) => handleChange('address', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="123 Main St" placeholder="123 Main St"
/> />
</div> </div>
@@ -370,35 +453,35 @@ const AcceptInvitation = () => {
{/* City, State, Zipcode */} {/* City, State, Zipcode */}
<div className="grid md:grid-cols-3 gap-4"> <div className="grid md:grid-cols-3 gap-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="city" className="text-[#422268]">City</Label> <Label htmlFor="city" className="text-[var(--purple-ink)]">City</Label>
<Input <Input
id="city" id="city"
value={formData.city} value={formData.city}
onChange={(e) => handleChange('city', e.target.value)} onChange={(e) => handleChange('city', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="San Francisco" placeholder="San Francisco"
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="state" className="text-[#422268]">State</Label> <Label htmlFor="state" className="text-[var(--purple-ink)]">State</Label>
<Input <Input
id="state" id="state"
value={formData.state} value={formData.state}
onChange={(e) => handleChange('state', e.target.value)} onChange={(e) => handleChange('state', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="CA" placeholder="CA"
maxLength={2} maxLength={2}
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="zipcode" className="text-[#422268]">Zipcode</Label> <Label htmlFor="zipcode" className="text-[var(--purple-ink)]">Zipcode</Label>
<Input <Input
id="zipcode" id="zipcode"
value={formData.zipcode} value={formData.zipcode}
onChange={(e) => handleChange('zipcode', e.target.value)} onChange={(e) => handleChange('zipcode', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="94102" placeholder="94102"
/> />
</div> </div>
@@ -406,13 +489,13 @@ const AcceptInvitation = () => {
{/* Date of Birth */} {/* Date of Birth */}
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="date_of_birth" className="text-[#422268]">Date of Birth</Label> <Label htmlFor="date_of_birth" className="text-[var(--purple-ink)]">Date of Birth</Label>
<Input <Input
id="date_of_birth" id="date_of_birth"
type="date" type="date"
value={formData.date_of_birth} value={formData.date_of_birth}
onChange={(e) => handleChange('date_of_birth', e.target.value)} onChange={(e) => handleChange('date_of_birth', e.target.value)}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
/> />
</div> </div>
</> </>
@@ -424,7 +507,7 @@ const AcceptInvitation = () => {
<Button <Button
type="submit" type="submit"
disabled={submitting} disabled={submitting}
className="w-full h-14 rounded-xl bg-gradient-to-r from-[#81B29A] to-[#6DA085] hover:from-[#6DA085] hover:to-[#5A8F72] text-white text-lg font-semibold" className="w-full h-14 rounded-xl bg-gradient-to-r from-[var(--green-light)] to-[var(--green-fern)] hover:from-[var(--green-fern)] hover:to-[var(--green-sage)] text-white text-lg font-semibold"
> >
{submitting ? ( {submitting ? (
<> <>
@@ -443,11 +526,11 @@ const AcceptInvitation = () => {
{/* Footer Note */} {/* Footer Note */}
<div className="mt-6 text-center"> <div className="mt-6 text-center">
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Already have an account?{' '} Already have an account?{' '}
<button <button
onClick={() => navigate('/login')} onClick={() => navigate('/login')}
className="text-[#664fa3] hover:text-[#422268] font-semibold underline" className="text-brand-purple hover:text-[var(--purple-ink)] font-semibold underline"
> >
Sign in instead Sign in instead
</button> </button>

View File

@@ -14,30 +14,28 @@ const BecomeMember = () => {
const imgIconAdminFee5 = `${process.env.PUBLIC_URL}/imgIconAdminFee5.png`; const imgIconAdminFee5 = `${process.env.PUBLIC_URL}/imgIconAdminFee5.png`;
const imgShootingStar = `${process.env.PUBLIC_URL}/imgShootingStar.png`; const imgShootingStar = `${process.env.PUBLIC_URL}/imgShootingStar.png`;
const Arrow = ({ ...props }) => (
<div className="flex justify-center mb-2">
<ArrowDown className="size-8 text---brand-purple font-bold" strokeWidth={2} />
</div>
);
return ( return (
<div className="min-h-screen bg-gray-50 relative"> <div className="min-h-screen bg-gray-50 relative">
<PublicNavbar /> <PublicNavbar />
{/* Decorative shooting star element */}
<div className="hidden lg:block absolute left-[88px] top-[974px] w-[195px] h-[1135px] pointer-events-none opacity-50">
<img
src={imgShootingStar}
alt=""
className="w-full h-full object-contain"
/>
</div>
{/* Hero Section */} {/* Hero Section */}
<div className="relative bg-gray-50 pt-20 pb-24"> <div className="relative bg-gray-50 pt-20 px-6 pb-16">
<div className="max-w-7xl mx-auto px-6 text-center"> <div className="max-w-7xl mx-auto text-center">
<h1 <h1
className="text-3xl sm:text-4xl md:text-5xl font-bold text-[#48286e] mb-6 sm:mb-8 leading-[1.2] tracking-[-0.96px]" className="text-3xl sm:text-4xl md:text-5xl font-bold text-[var(--purple-deep)] mb-6 sm:mb-8 leading-[1.2] tracking-[-0.96px]"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}
> >
Become a Member Become a Member
</h1> </h1>
<p <p
className="text-base sm:text-lg font-medium text-[#48286e] max-w-2xl mx-auto leading-[1.6]" className="text-base sm:text-lg font-medium text-[var(--purple-deep)] max-w-2xl mx-auto leading-[1.6]"
style={{ fontFamily: "'Nunito Sans', sans-serif", fontVariationSettings: "'YTLC' 500, 'wdth' 100" }} style={{ fontFamily: "'Nunito Sans', sans-serif", fontVariationSettings: "'YTLC' 500, 'wdth' 100" }}
> >
Become a member to receive our monthly newsletter and find out about all the activities LOAF has planned each month. LOAF hosts over 40 social activities each year and occasionally covers the costs for members only Become a member to receive our monthly newsletter and find out about all the activities LOAF has planned each month. LOAF hosts over 40 social activities each year and occasionally covers the costs for members only
@@ -46,7 +44,7 @@ const BecomeMember = () => {
</div> </div>
{/* Annual Administrative Fees Section */} {/* Annual Administrative Fees Section */}
<div className="max-w-[1340px] mx-auto px-6 mb-12 sm:mb-16"> <div className="max-w-[1340px] z-10 mx-auto px-6 mb-12 sm:mb-16">
<div className="flex flex-col sm:flex-row gap-4 sm:gap-5 items-center"> <div className="flex flex-col sm:flex-row gap-4 sm:gap-5 items-center">
<div className="w-24 h-24 sm:w-32 sm:h-32 md:w-[153px] md:h-[138px] flex-shrink-0"> <div className="w-24 h-24 sm:w-32 sm:h-32 md:w-[153px] md:h-[138px] flex-shrink-0">
<img <img
@@ -55,15 +53,15 @@ const BecomeMember = () => {
className="w-full h-full object-contain" className="w-full h-full object-contain"
/> />
</div> </div>
<div className="flex-1 bg-[#eeebf4] rounded-[25px] px-4 py-6 sm:px-6 sm:py-7 md:px-8 md:py-8"> <div className="flex-1 bg-[var(--lavender-200)] rounded-[25px] px-4 py-6 sm:px-6 sm:py-7 md:px-8 md:py-8">
<h3 <h3
className="text-xl sm:text-2xl md:text-[32px] font-semibold text-[#48286e] mb-3 sm:mb-4 md:mb-5 leading-[1.49]" className="text-xl sm:text-2xl md:text-[32px] font-semibold text-[var(--purple-deep)] mb-3 sm:mb-4 md:mb-5 leading-[1.49]"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}
> >
Annual Administrative Fees Annual Administrative Fees
</h3> </h3>
<p <p
className="text-base sm:text-lg font-medium text-[#48286e] leading-[1.6]" className="text-base sm:text-lg font-medium text-[var(--purple-deep)] leading-[1.6]"
style={{ fontFamily: "'Nunito Sans', sans-serif", fontVariationSettings: "'YTLC' 500, 'wdth' 100" }} style={{ fontFamily: "'Nunito Sans', sans-serif", fontVariationSettings: "'YTLC' 500, 'wdth' 100" }}
> >
Annual Administrative Fees for all members are $30 per person. These fees help cover general business expenses (website, advertising, e-newsletter). Annual Administrative Fees for all members are $30 per person. These fees help cover general business expenses (website, advertising, e-newsletter).
@@ -73,42 +71,50 @@ const BecomeMember = () => {
</div> </div>
{/* Membership Process Section */} {/* Membership Process Section */}
<div className="relative bg-gray-50 py-16"> <div className="relative bg-gray-50 py-32 bg-gradient-to-bl from-[var(--neutral-100:)] to-[var(--neutral-800)] ">
<div className="max-w-7xl mx-auto px-6 text-center"> {/* Decorative shooting star element */}
<div className="hidden lg:block absolute left-0 top-64 w-[195px] h-[1130px] pointer-events-none opacity-50">
<img
src={imgShootingStar}
alt=""
className="w-full h-full z-20 object-contain"
/>
</div>
<div className="max-w-7xl mx-auto px-6 mb-24 text-center">
<h2 <h2
className="text-2xl sm:text-3xl md:text-4xl font-bold text-[#48286e] mb-6 sm:mb-8 leading-[1.2] tracking-[-0.8px]" className="text-2xl sm:text-3xl md:text-[40px] font-bold text-[var(--purple-deep)] mb-6 sm:mb-8 leading-[1.2] tracking-[-0.8px]"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}
> >
Membership Process Membership Process
</h2> </h2>
<p <p
className="text-base sm:text-lg font-medium text-[#48286e] max-w-2xl mx-auto leading-[1.6]" className="text-base sm:text-lg md:text-xl lg:text-2xl lg:font-semibold font-medium text-[var(--purple-deep)] max-w-2xl mx-auto leading-[1.6]"
style={{ fontFamily: "'Nunito Sans', sans-serif", fontVariationSettings: "'YTLC' 500, 'wdth' 100" }} style={{ fontFamily: "'Nunito Sans', sans-serif", fontVariationSettings: "'YTLC' 500, 'wdth' 100" }}
> >
Becoming a member is easy, but for the safety and privacy of our membership, there are a few steps: Becoming a member is easy, but for the safety and privacy of our membership, there are a few steps:
</p> </p>
</div> </div>
</div>
{/* Step 1 */} {/* Step 1 */}
<div className="max-w-[1340px] mx-auto px-6 mb-6 sm:mb-8"> <div className="max-w-[1340px] mx-auto px-6 mb-2 ">
<div className="flex flex-col sm:flex-row gap-4 sm:gap-5 items-center"> <div className="flex flex-col sm:flex-row gap-4 sm:gap-5 items-center">
<div className="w-24 h-24 sm:w-32 sm:h-32 md:w-[153px] md:h-[138px] flex-shrink-0"> <div className="w-24 h-24 sm:w-32 z-40 sm:h-32 md:w-[153px] md:h-[138px] flex-shrink-0">
<img <img
src={imgIconAdminFee2} src={imgIconAdminFee2}
alt="Step 1 Icon" alt="Step 1 Icon"
className="w-full h-full object-contain" className="w-full h-full object-contain"
/> />
</div> </div>
<div className="flex-1 bg-white rounded-[25px] px-4 py-6 sm:px-6 sm:py-7 md:px-8 md:py-8"> <div className="flex-1 bg-background rounded-[25px] px-4 py-6 sm:px-6 sm:py-7 md:px-8 md:py-8">
<h3 <h3
className="text-xl sm:text-2xl md:text-[32px] font-semibold text-[#48286e] mb-3 sm:mb-4 md:mb-5 leading-[1.49]" className="text-xl sm:text-2xl md:text-[32px] font-semibold text-[var(--purple-deep)] mb-3 sm:mb-4 md:mb-5 leading-[1.49]"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}
> >
Step 1: Application & Email Confirmation Step 1: Application & Email Confirmation
</h3> </h3>
<p <p
className="text-base sm:text-lg font-medium text-[#48286e] leading-[1.6]" className="text-base sm:text-lg font-medium text-[var(--purple-deep)] leading-[1.6]"
style={{ fontFamily: "'Nunito Sans', sans-serif", fontVariationSettings: "'YTLC' 500, 'wdth' 100" }} style={{ fontFamily: "'Nunito Sans', sans-serif", fontVariationSettings: "'YTLC' 500, 'wdth' 100" }}
> >
Complete the online application form and submit it. Check your email for a confirmation link and use it to verify your email. You will then begin to receive LOAF's monthly e-newsletter where all of the social events are listed. Your application will remain pending, and you won't be able to log into the Members Only section of the website until step 2 is complete and you are validated by an admin. Complete the online application form and submit it. Check your email for a confirmation link and use it to verify your email. You will then begin to receive LOAF's monthly e-newsletter where all of the social events are listed. Your application will remain pending, and you won't be able to log into the Members Only section of the website until step 2 is complete and you are validated by an admin.
@@ -118,29 +124,27 @@ const BecomeMember = () => {
</div> </div>
{/* Arrow Down Icon */} {/* Arrow Down Icon */}
<div className="flex justify-center mb-8"> <Arrow />
<ArrowDown className="w-8 h-8 text-[#4f378a]" strokeWidth={2} />
</div>
{/* Step 2 */} {/* Step 2 */}
<div className="max-w-[1340px] mx-auto px-6 mb-6 sm:mb-8"> <div className="max-w-[1340px] mx-auto px-6 mb-2">
<div className="flex flex-col sm:flex-row gap-4 sm:gap-5 items-center"> <div className="flex flex-col sm:flex-row gap-4 sm:gap-5 items-center">
<div className="w-24 h-24 sm:w-32 sm:h-32 md:w-[153px] md:h-[138px] flex-shrink-0"> <div className="w-24 h-24 z-40 sm:w-32 sm:h-32 md:w-[153px] md:h-[138px] flex-shrink-0">
<img <img
src={imgIconAdminFee3} src={imgIconAdminFee3}
alt="Step 2 Icon" alt="Step 2 Icon"
className="w-full h-full object-contain" className="w-full h-full object-contain"
/> />
</div> </div>
<div className="flex-1 bg-white rounded-[25px] px-4 py-6 sm:px-6 sm:py-7 md:px-8 md:py-8"> <div className="flex-1 bg-background rounded-[25px] px-4 py-6 sm:px-6 sm:py-7 md:px-8 md:py-8">
<h3 <h3
className="text-xl sm:text-2xl md:text-[32px] font-semibold text-[#48286e] mb-3 sm:mb-4 md:mb-5 leading-[1.49]" className="text-xl sm:text-2xl md:text-[32px] font-semibold text-[var(--purple-deep)] mb-3 sm:mb-4 md:mb-5 leading-[1.49]"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}
> >
Step 2: Attend an event and meet us! Step 2: Attend an event and meet us!
</h3> </h3>
<p <p
className="text-base sm:text-lg font-medium text-[#48286e] leading-[1.6]" className="text-base sm:text-lg font-medium text-[var(--purple-deep)] leading-[1.6]"
style={{ fontFamily: "'Nunito Sans', sans-serif", fontVariationSettings: "'YTLC' 500, 'wdth' 100" }} style={{ fontFamily: "'Nunito Sans', sans-serif", fontVariationSettings: "'YTLC' 500, 'wdth' 100" }}
> >
You have 3 months to attend a LOAF event and introduce yourself to a board member. If you do not attend an event within 3 months, you will no longer receive the e-newsletter. (This step can be skipped if you have been referred from a current member and list her on your registration form). You have 3 months to attend a LOAF event and introduce yourself to a board member. If you do not attend an event within 3 months, you will no longer receive the e-newsletter. (This step can be skipped if you have been referred from a current member and list her on your registration form).
@@ -150,29 +154,26 @@ const BecomeMember = () => {
</div> </div>
{/* Arrow Down Icon */} {/* Arrow Down Icon */}
<div className="flex justify-center mb-8"> <Arrow />
<ArrowDown className="w-8 h-8 text-[#4f378a]" strokeWidth={2} />
</div>
{/* Step 3 */} {/* Step 3 */}
<div className="max-w-[1340px] mx-auto px-6 mb-6 sm:mb-8"> <div className="max-w-[1340px] mx-auto px-6 mb-2">
<div className="flex flex-col sm:flex-row gap-4 sm:gap-5 items-center"> <div className="flex flex-col sm:flex-row gap-4 sm:gap-5 items-center">
<div className="w-24 h-24 sm:w-32 sm:h-32 md:w-[153px] md:h-[138px] flex-shrink-0"> <div className="w-24 h-24 z-40 sm:w-32 sm:h-32 md:w-[153px] md:h-[138px] flex-shrink-0">
<img <img
src={imgIconAdminFee4} src={imgIconAdminFee4}
alt="Step 3 Icon" alt="Step 3 Icon"
className="w-full h-full object-contain" className="w-full h-full object-contain"
/> />
</div> </div>
<div className="flex-1 bg-white rounded-[25px] px-4 py-6 sm:px-6 sm:py-7 md:px-8 md:py-8"> <div className="flex-1 bg-background rounded-[25px] px-4 py-6 sm:px-6 sm:py-7 md:px-8 md:py-8">
<h3 <h3
className="text-xl sm:text-2xl md:text-[32px] font-semibold text-[#48286e] mb-3 sm:mb-4 md:mb-5 leading-[1.49]" className="text-xl sm:text-2xl md:text-[32px] font-semibold text-[var(--purple-deep)] mb-3 sm:mb-4 md:mb-5 leading-[1.49]"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}
> >
Step 3: Login and pay the annual fee Step 3: Login and pay the annual fee
</h3> </h3>
<p <p
className="text-base sm:text-lg font-medium text-[#48286e] leading-[1.6]" className="text-base sm:text-lg font-medium text-[var(--purple-deep)] leading-[1.6]"
style={{ fontFamily: "'Nunito Sans', sans-serif", fontVariationSettings: "'YTLC' 500, 'wdth' 100" }} style={{ fontFamily: "'Nunito Sans', sans-serif", fontVariationSettings: "'YTLC' 500, 'wdth' 100" }}
> >
Once we know that you are indeed you, an admin will validate your application and you will receive an email prompting you to login to your user profile and pay the annual administrative fee. Once we know that you are indeed you, an admin will validate your application and you will receive an email prompting you to login to your user profile and pay the annual administrative fee.
@@ -182,21 +183,19 @@ const BecomeMember = () => {
</div> </div>
{/* Arrow Down Icon */} {/* Arrow Down Icon */}
<div className="flex justify-center mb-8"> <Arrow />
<ArrowDown className="w-8 h-8 text-[#4f378a]" strokeWidth={2} />
</div>
{/* Step 4 - With Gradient Background */} {/* Step 4 - With Gradient Background */}
<div className="max-w-[1340px] mx-auto px-6 mb-12 sm:mb-16"> <div className="max-w-[1340px] mx-auto px-6 mb-2">
<div className="flex flex-col sm:flex-row gap-4 sm:gap-5 items-center"> <div className="flex flex-col sm:flex-row gap-4 sm:gap-5 items-center">
<div className="w-24 h-24 sm:w-32 sm:h-32 md:w-[153px] md:h-[138px] flex-shrink-0"> <div className="w-24 h-24 z-40 sm:w-32 sm:h-32 md:w-[153px] md:h-[138px] flex-shrink-0">
<img <img
src={imgIconAdminFee5} src={imgIconAdminFee5}
alt="Step 4 Icon" alt="Step 4 Icon"
className="w-full h-full object-contain" className="w-full h-full object-contain"
/> />
</div> </div>
<div className="flex-1 bg-gradient-to-r from-[#48286e] to-[#664fa3] rounded-[25px] px-4 py-6 sm:px-6 sm:py-7 md:px-8 md:py-8"> <div className="flex-1 bg-gradient-to-r from-[var(--purple-deep)] to-[var(--purple-lavender)] rounded-[25px] px-4 py-6 sm:px-6 sm:py-7 md:px-8 md:py-8">
<h3 <h3
className="text-xl sm:text-2xl md:text-[32px] font-semibold text-white mb-3 sm:mb-4 md:mb-5 leading-[1.49]" className="text-xl sm:text-2xl md:text-[32px] font-semibold text-white mb-3 sm:mb-4 md:mb-5 leading-[1.49]"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}
@@ -212,19 +211,19 @@ const BecomeMember = () => {
</div> </div>
</div> </div>
</div> </div>
</div>
{/* CTA Section */} {/* CTA Section */}
<div className="relative bg-gray-50 py-16 "> <div className="relative bg-gray-50 py-16 ">
<div className="max-w-7xl mx-auto px-6 text-center"> <div className="max-w-7xl mx-auto px-6 flex flex-col md:flex-row items-center justify-center gap-12 text-center">
<h2 <h2
className="text-2xl sm:text-3xl md:text-4xl font-bold text-[#48286e] mb-6 sm:mb-8 leading-[1.2] tracking-[-0.8px]" className="text-2xl sm:text-3xl md:text-[40px] content-center font-bold text-[var(--purple-deep)] leading-[1.2] tracking-[-0.8px]"
style={{ fontFamily: "'Poppins', sans-serif" }} style={{ fontFamily: "'Poppins', sans-serif" }}
> >
Ready to Join Us? Ready to Join Us?
</h2> </h2>
<Link to="/register"> <Link to="/register">
<Button <Button
className="bg-[#664fa3] text-white hover:bg-[#48286e] rounded-[35px] px-6 py-3 sm:px-12 sm:py-5 md:px-16 md:py-6 text-base sm:text-lg font-medium tracking-[-0.09px] h-auto" className="bg-[var(--purple-lavender)] text-white hover:bg-[var(--purple-deep)] rounded-[35px] px-6 py-3 sm:px-12 sm:py-5 md:px-16 md:py-6 text-[19px] sm:text-lg font-medium tracking-[-0.09px] leading-5 h-auto"
style={{ fontFamily: "'Inter', sans-serif" }} style={{ fontFamily: "'Inter', sans-serif" }}
> >
Register Now! Register Now!

View File

@@ -12,97 +12,148 @@ const BoardOfDirectors = () => {
]; ];
const boardMembers = [ const boardMembers = [
'Danita Cole', { name: 'Danita Cole', title: 'Director' },
'Roxanne Cherico', { name: 'Roxanne Cherico', title: 'Director' },
'Lucretia Copeland', { name: 'Lucretia Copeland', title: 'Director' },
'Julie Fischer' { name: 'Julie Fischer', title: 'Director' }
]; ];
const DirectorCards = ({ title, members }) => {
return ( return (
<div className="min-h-screen bg-white"> <section className=" w-full">
<div className="mx-auto bg-background rounded-3xl p-10 shadow-lg h-full">
{title && (
<h2
className="text-2xl sm:text-4xl font-bold text-[var(--purple-deep)] text-center mb-8"
style={{ fontFamily: "'Poppins', sans-serif" }}
>
{title}
</h2>
)}
<div className="grid grid-col-span-1 lg:grid-cols-2 gap-5 w-full">
{members.map((member, index) => {
const { name, title } =
typeof member === "string"
? { name: member, title: "" }
: member;
return (
<Card
key={`${name}-${index}`}
style={{ fontFamily: "'Poppins', sans-serif" }}
className="bg-[var(--lavender-200)] text-[var(--purple-deep)] text-center px-6 py-5 rounded-3xl border border-white/70 shadow-sm"
>
<div className="min-h-16">
<p className="text-xl font-bold text-[var(--purple-deep)]">
{name}
</p>
{title && (
<p className="text-xl mt-2 font-semibold">
{title}
</p>
)}
</div>
</Card>
);
})}
</div>
</div>
</section>
);
};
return (
<div className="min-h-screen bg-background">
<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-b from-[var(--neutral-100:)] to-[var(--neutral-800)] pt-8 sm:pt-10 md:pt-12">
{/* Hero Section with Contact */} {/* Hero Section */}
<section className="bg-gradient-to-r from-[#664fa3] to-[#48286e] py-8 rounded-2xl mb-12"> <section className=" pt-16 pb-4 px-4 sm:px-6 md:px-8 lg:px-12 xl:px-20">
<div className="max-w-5xl mx-auto text-center px-8"> <div className="max-w-5xl mx-auto text-center px-8">
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold text-white mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h1 className="text-2xl sm:text-3xl md:text-[40px] leading-[1.2] text-[var(--purple-deep)] font-bold mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
LOAF Board of Directors 2025 LOAF Board of Directors 2025
</h1> </h1>
<p className="text-white text-lg mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
</div>
</section>
{/* Contact Info */}
<section className="flex justify-center mt-4 mb-8">
<div className=" w-full text-center px-8 justify-center bg-gradient-to-r from-[var(--purple-lavender)] to-[var(--purple-deep)] max-w-5xl mx-6 flex lg:mx-12 py-5 rounded-3xl sm:px-6 md:px-8 lg:px-12 xl:px-20">
<p className="text-white text-xl font-bold " style={{ fontFamily: "'Poppins', sans-serif" }}>
For any questions or inquiries please email us at{' '} For any questions or inquiries please email us at{' '}
<a href="mailto:info@loaftx.org" className="text-[#c5b4e3] underline font-bold hover:text-white transition-colors"> <a href="mailto:info@loaftx.org" className="text-[var(--neutral-500)] underline font-bold hover:text-white transition-colors">
info@loaftx.org info@loaftx.org
</a> </a>
</p> </p>
</div> </div>
</section> </section>
{/* Board Members Section */}
<section className=' flex lg:flex-row flex-col gap-10 items-stretch mt-8 px-4 sm:px-6 md:px-8 lg:px-12 xl:px-20 pb-12'>
{/* Officers Grid */} {/* Officers Grid */}
<section className="py-12"> <DirectorCards title="Officers" members={officers} />
<div className="max-w-6xl mx-auto">
<h2 className="text-xl sm:text-2xl md:text-3xl font-bold text-[#48286e] text-center mb-8" style={{ fontFamily: "'Inter', sans-serif" }}>
Officers
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{officers.map((officer, index) => (
<Card key={index} className="bg-[#eeebf4] p-6 text-center rounded-xl shadow-md hover:shadow-lg transition-shadow">
<h3 className="text-xl font-bold text-[#48286e] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
{officer.name}
</h3>
<p className="text-lg text-[#48286e]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{officer.title}
</p>
</Card>
))}
</div>
</div>
</section>
{/* Board Members Grid */} {/* Board Members Grid */}
<section className="py-12 bg-gray-50 rounded-2xl"> <DirectorCards title="Board Of Directors" members={boardMembers} />
<div className="max-w-6xl mx-auto px-8">
<h2 className="text-xl sm:text-2xl md:text-3xl font-bold text-[#48286e] text-center mb-8" style={{ fontFamily: "'Inter', sans-serif" }}>
Board of Directors
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{boardMembers.map((member, index) => (
<Card key={index} className="bg-[#eeebf4] p-6 text-center rounded-xl shadow-md hover:shadow-lg transition-shadow">
<h3 className="text-xl font-bold text-[#48286e]" style={{ fontFamily: "'Inter', sans-serif" }}>
{member}
</h3>
</Card>
))}
</div>
</div>
</section> </section>
{/* Join the Board Section */} {/* Join the Board Section */}
<section className="py-12"> <section className="py-12 bg-background mt-12 ">
<div className="max-w-4xl mx-auto"> {/* content containter */}
<h2 className="text-xl sm:text-2xl md:text-3xl font-bold text-[#48286e] text-center mb-8" style={{ fontFamily: "'Inter', sans-serif" }}> <div className="w-full mx-auto flex flex-col px-4 sm:px-6 md:px-8 lg:px-12 xl:px-20">
<h2 className="text-xl mx-auto sm:text-2xl md:text-4xl font-bold text-[var(--purple-deep)] text-center mb-8" style={{ fontFamily: "'Poppins', sans-serif" }}>
Join the Board of Directors Join the Board of Directors
</h2> </h2>
<p className="text-xl text-[#48286e] text-center mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="lg:text-2xl text-md md:text-lg max-w-4xl mx-auto justify-center font-bold text-[var(--purple-deep)] text-center mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Our elections take place at our December holiday social. Here are some things Our elections take place at our December holiday social. Here are some things to know if you are thinking about serving on the Board of Directors.
to know if you are thinking about serving on the Board of Directors.
</p> </p>
<Card className="bg-[#eeebf4] p-8 rounded-2xl shadow-lg"> {/* card */}
<ol className="list-decimal list-inside space-y-4 text-lg text-[#48286e]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <Card className="bg-[var(--lavender-200)] p-8 rounded-2xl shadow-lg mx-auto border border-white/70">
<ol className="list-decimal list-inside space-y-4 text-lg text-[var(--purple-deep)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<li> <li>
Nominations are due by November 1. Nomination Form:{' '} Nominations are due by November 1. Nomination Form:{' '}
<a href="https://docs.google.com/forms/d/e/1FAIpQLSfNomination" target="_blank" rel="noopener noreferrer" <a href="https://docs.google.com/forms/d/e/1FAIpQLSfNomination" target="_blank" rel="noopener noreferrer"
className="text-[#664fa3] underline hover:text-[#48286e] transition-colors"> className="text-[var(--purple-lavender)] underline hover:text-[var(--purple-deep)] transition-colors">
Click Here Click Here
</a> </a>
</li> </li>
<li>Nominees must have been a member for at least 1 year and current with their dues.</li> <li>Nominees must have been a member for at least 1 year, however it is possible to be elected prior to 1 year, but start the term on the 1 year anniversary.</li>
<li>Officer positions are only available to current directors.</li> <li>Officer positions are only available to current directors.</li>
<li>Each director shall serve a 2 year term.</li>
<li>The time commitment is 1-2 hours per week.</li> <li>Each director shall serve a 2-year term.</li>
<li>The tasks that directors perform depend on individual interests, skills, and time available.</li>
<li>Directors must attend Board meetings which are held the second Thursday of each month at 6:30pm via Zoom.</li> <li>The time commitment is approximately 12 hours per week.</li>
<li>We are a fun group, and we would love for you to join us in providing this service for our community.</li>
<li>
The tasks that directors perform depend on individual interests. Recent
tasks include researching how to obtain an extra PO Box key, ordering
Welcome Team name tags, taking pictures at events, researching new venues
for holiday socials, and monitoring Facebook posts. For more information
about director duties, see Article 2 of the bylaws in the Members Only
section of the website:&nbsp;
<a
href="https://loaftx.org/bylaws/"
target="_blank"
rel="noopener noreferrer"
className="text-[var(--purple-deep)] underline"
>
https://loaftx.org/bylaws/
</a>
</li>
<li>
Directors must attend Board meetings held on the second Thursday of each
month at 6:30pm via Zoom.
</li>
<li>
We are a fun group, and we would love for you to join us in providing this
service for our community.
</li>
</ol> </ol>
</Card> </Card>
</div> </div>

View File

@@ -83,19 +83,19 @@ const ChangePasswordRequired = () => {
} }
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-background">
<Navbar /> <Navbar />
<div className="max-w-md mx-auto px-6 py-12"> <div className="max-w-md mx-auto px-6 py-12">
<Card className="p-8 md:p-12 bg-white rounded-2xl border border-[#ddd8eb] shadow-lg"> <Card className="p-8 md:p-12 bg-background rounded-2xl border border-[var(--neutral-800)] shadow-lg">
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-[#FFEBEE] mb-4"> <div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-[#FFEBEE] mb-4">
<AlertTriangle className="h-8 w-8 text-orange-500" /> <AlertTriangle className="h-8 w-8 text-orange-500" />
</div> </div>
<h1 className="text-4xl md:text-5xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Password Change Required Password Change Required
</h1> </h1>
<p className="text-lg text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Your password was reset by an administrator. Please create a new password to continue. Your password was reset by an administrator. Please create a new password to continue.
</p> </p>
</div> </div>
@@ -111,7 +111,7 @@ const ChangePasswordRequired = () => {
value={formData.currentPassword} value={formData.currentPassword}
onChange={handleInputChange} onChange={handleInputChange}
placeholder="Enter temporary password" placeholder="Enter temporary password"
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
/> />
</div> </div>
@@ -125,7 +125,7 @@ const ChangePasswordRequired = () => {
value={formData.newPassword} value={formData.newPassword}
onChange={handleInputChange} onChange={handleInputChange}
placeholder="Enter new password (min. 6 characters)" placeholder="Enter new password (min. 6 characters)"
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
/> />
</div> </div>
@@ -139,15 +139,15 @@ const ChangePasswordRequired = () => {
value={formData.confirmPassword} value={formData.confirmPassword}
onChange={handleInputChange} onChange={handleInputChange}
placeholder="Re-enter new password" placeholder="Re-enter new password"
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
/> />
</div> </div>
<div className="bg-[#f1eef9] border-l-4 border-[#664fa3] p-4 rounded-lg"> <div className="bg-[var(--lavender-300)] border-l-4 border-brand-purple p-4 rounded-lg">
<div className="flex items-start"> <div className="flex items-start">
<Lock className="h-5 w-5 text-[#664fa3] mr-2 mt-0.5 flex-shrink-0" /> <Lock className="h-5 w-5 text-brand-purple mr-2 mt-0.5 flex-shrink-0" />
<div className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <div className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<p className="font-medium text-[#422268] mb-1">Password Requirements:</p> <p className="font-medium text-[var(--purple-ink)] mb-1">Password Requirements:</p>
<ul className="list-disc list-inside space-y-1"> <ul className="list-disc list-inside space-y-1">
<li>At least 6 characters long</li> <li>At least 6 characters long</li>
<li>Both passwords must match</li> <li>Both passwords must match</li>
@@ -159,17 +159,17 @@ const ChangePasswordRequired = () => {
<Button <Button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="w-full bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform disabled:opacity-50" className="w-full bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background rounded-full py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform disabled:opacity-50"
> >
{loading ? 'Changing Password...' : 'Change Password'} {loading ? 'Changing Password...' : 'Change Password'}
<ArrowRight className="ml-2 h-5 w-5" /> <ArrowRight className="ml-2 h-5 w-5" />
</Button> </Button>
<div className="text-center pt-4 border-t border-[#ddd8eb]"> <div className="text-center pt-4 border-t border-[var(--neutral-800)]">
<button <button
type="button" type="button"
onClick={handleLogout} onClick={handleLogout}
className="text-[#664fa3] hover:text-[#ff9e77] text-sm underline" className="text-brand-purple hover:text-[var(--orange-light)] text-sm underline"
> >
Logout instead Logout instead
</button> </button>

View File

@@ -10,7 +10,7 @@ import { Checkbox } from '../components/ui/checkbox';
import { Mail, MapPin, Loader2 } from 'lucide-react'; import { Mail, MapPin, Loader2 } from 'lucide-react';
import api from '../utils/api'; import api from '../utils/api';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { PiMapTrifoldBold } from "react-icons/pi";
const ContactUs = () => { const ContactUs = () => {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
firstName: '', firstName: '',
@@ -96,15 +96,15 @@ const ContactUs = () => {
}; };
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-background">
<PublicNavbar /> <PublicNavbar />
<main className="bg-gradient-to-b from-[#e8e0f5] to-[#f1eef9] px-6 py-16"> <main className="bg-gradient-to-b from-[var(--lavender-100)] to-[var(--lavender-300)] px-6 py-16">
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8">
{/* Contact Form */} {/* Contact Form */}
<Card className="p-8 bg-white rounded-2xl border-2 border-[#ddd8eb] shadow-lg"> <Card className="p-8 bg-background rounded-2xl">
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold text-[#48286e] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}> <h1 className="text-2xl sm:text-[28px] leading-5 font-bold text-[var(--purple-deep)] mb-12" style={{ fontFamily: "'Poppins', sans-serif" }}>
Contact Form Contact Form
</h1> </h1>
@@ -112,7 +112,7 @@ const ContactUs = () => {
{/* First Name & Last Name */} {/* First Name & Last Name */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<Label htmlFor="firstName" className="text-[#48286e] font-medium mb-2 block" style={{ fontFamily: "'Inter', sans-serif" }}> <Label htmlFor="firstName" className="text-[var(--purple-deep)] font-medium mb-2 block" style={{ fontFamily: "'Inter', sans-serif" }}>
First Name <span className="text-red-500">*</span> First Name <span className="text-red-500">*</span>
</Label> </Label>
<Input <Input
@@ -120,13 +120,13 @@ const ContactUs = () => {
name="firstName" name="firstName"
value={formData.firstName} value={formData.firstName}
onChange={handleChange} onChange={handleChange}
className="border-2 border-[#ddd8eb] focus:border-[#664fa3] rounded-full h-12 px-4" className="border-2 border-[var(--neutral-800)] bg-[var(--lavender-800)] focus:border-[var(--purple-lavender)] rounded-full h-12 px-4"
style={{ fontFamily: "'Nunito Sans', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
required required
/> />
</div> </div>
<div> <div>
<Label htmlFor="lastName" className="text-[#48286e] font-medium mb-2 block" style={{ fontFamily: "'Inter', sans-serif" }}> <Label htmlFor="lastName" className="text-[var(--purple-deep)] font-medium mb-2 block" style={{ fontFamily: "'Inter', sans-serif" }}>
Last Name <span className="text-red-500">*</span> Last Name <span className="text-red-500">*</span>
</Label> </Label>
<Input <Input
@@ -134,7 +134,7 @@ const ContactUs = () => {
name="lastName" name="lastName"
value={formData.lastName} value={formData.lastName}
onChange={handleChange} onChange={handleChange}
className="border-2 border-[#ddd8eb] focus:border-[#664fa3] rounded-full h-12 px-4" className="border-2 bg-[var(--lavender-800)] border-[var(--neutral-800)] focus:border-[var(--purple-lavender)] rounded-full h-12 px-4"
style={{ fontFamily: "'Nunito Sans', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
required required
/> />
@@ -143,7 +143,7 @@ const ContactUs = () => {
{/* Email */} {/* Email */}
<div> <div>
<Label htmlFor="email" className="text-[#48286e] font-medium mb-2 block" style={{ fontFamily: "'Inter', sans-serif" }}> <Label htmlFor="email" className="text-[var(--purple-deep)] font-medium mb-2 block" style={{ fontFamily: "'Inter', sans-serif" }}>
Email <span className="text-red-500">*</span> Email <span className="text-red-500">*</span>
</Label> </Label>
<Input <Input
@@ -152,7 +152,7 @@ const ContactUs = () => {
type="email" type="email"
value={formData.email} value={formData.email}
onChange={handleChange} onChange={handleChange}
className="border-2 border-[#ddd8eb] focus:border-[#664fa3] rounded-full h-12 px-4" className="border-2 bg-[var(--lavender-800)] border-[var(--neutral-800)] focus:border-[var(--purple-lavender)] rounded-full h-12 px-4"
style={{ fontFamily: "'Nunito Sans', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
required required
/> />
@@ -160,7 +160,7 @@ const ContactUs = () => {
{/* Subject */} {/* Subject */}
<div> <div>
<Label htmlFor="subject" className="text-[#48286e] font-medium mb-2 block" style={{ fontFamily: "'Inter', sans-serif" }}> <Label htmlFor="subject" className="text-[var(--purple-deep)] font-medium mb-2 block" style={{ fontFamily: "'Inter', sans-serif" }}>
Subject <span className="text-red-500">*</span> Subject <span className="text-red-500">*</span>
</Label> </Label>
<Input <Input
@@ -168,7 +168,7 @@ const ContactUs = () => {
name="subject" name="subject"
value={formData.subject} value={formData.subject}
onChange={handleChange} onChange={handleChange}
className="border-2 border-[#ddd8eb] focus:border-[#664fa3] rounded-full h-12 px-4" className="border-2 bg-[var(--lavender-800)] border-[var(--neutral-800)] focus:border-[var(--purple-lavender)] rounded-full h-12 px-4"
style={{ fontFamily: "'Nunito Sans', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
required required
/> />
@@ -176,7 +176,7 @@ const ContactUs = () => {
{/* Message */} {/* Message */}
<div> <div>
<Label htmlFor="message" className="text-[#48286e] font-medium mb-2 block" style={{ fontFamily: "'Inter', sans-serif" }}> <Label htmlFor="message" className="text-[var(--purple-deep)] font-medium mb-2 block" style={{ fontFamily: "'Inter', sans-serif" }}>
Your Message <span className="text-red-500">*</span> Your Message <span className="text-red-500">*</span>
</Label> </Label>
<Textarea <Textarea
@@ -184,7 +184,7 @@ const ContactUs = () => {
name="message" name="message"
value={formData.message} value={formData.message}
onChange={handleChange} onChange={handleChange}
className="border-2 border-[#ddd8eb] focus:border-[#664fa3] rounded-2xl min-h-[150px] px-4 py-3 resize-none" className="border-2 bg-[var(--lavender-800)] border-[var(--neutral-800)] focus:border-[var(--purple-lavender)] rounded-2xl min-h-[150px] px-4 py-3 resize-none"
style={{ fontFamily: "'Nunito Sans', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
required required
/> />
@@ -196,9 +196,9 @@ const ContactUs = () => {
id="consent" id="consent"
checked={formData.consent} checked={formData.consent}
onCheckedChange={handleConsentChange} onCheckedChange={handleConsentChange}
className="mt-1 border-2 border-[#ddd8eb] data-[state=checked]:bg-[#664fa3] data-[state=checked]:border-[#664fa3]" className="mt-1 border-2 border-[var(--neutral-800)] data-[state=checked]:bg-[var(--purple-lavender)] data-[state=checked]:border-[var(--purple-lavender)]"
/> />
<Label htmlFor="consent" className="text-[#48286e] text-sm font-normal cursor-pointer" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <Label htmlFor="consent" className="text-[var(--purple-deep)] text-sm font-normal cursor-pointer" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
I consent to LOAF storing my submitted information so they can respond to my inquiry <span className="text-red-500">*</span> I consent to LOAF storing my submitted information so they can respond to my inquiry <span className="text-red-500">*</span>
</Label> </Label>
</div> </div>
@@ -207,7 +207,7 @@ const ContactUs = () => {
<Button <Button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="w-full bg-[#664fa3] hover:bg-[#48286e] text-white rounded-full py-6 text-lg font-semibold disabled:opacity-50" className="w-full bg-[var(--purple-lavender)] hover:bg-[var(--purple-deep)] text-white rounded-full py-6 text-lg font-semibold disabled:opacity-50"
style={{ fontFamily: "'Inter', sans-serif" }} style={{ fontFamily: "'Inter', sans-serif" }}
> >
{loading ? ( {loading ? (
@@ -225,22 +225,22 @@ const ContactUs = () => {
{/* Contact Information */} {/* Contact Information */}
<div className="space-y-6"> <div className="space-y-6">
{/* Message Card */} {/* Message Card */}
<Card className="p-8 bg-gradient-to-r from-[#664fa3] to-[#48286e] rounded-2xl shadow-lg text-white"> <Card className="p-8 bg-gradient-to-r from-[var(--purple-lavender)] to-[var(--purple-deep)] rounded-2xl shadow-lg text-white">
<p className="text-xl leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-[28px] font-semibold leading-relaxed" style={{ fontFamily: "'Poppins', sans-serif" }}>
If you have questions, or are interested in joining, we would love hearing from you. If you have questions, or are interested in joining, we would love hearing from you.
</p> </p>
</Card> </Card>
{/* Email Card */} {/* Email Card */}
<Card className="p-6 bg-white rounded-2xl border-2 border-[#ddd8eb] shadow-lg"> <Card className="p-6 bg-background rounded-2xl">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="w-12 h-12 bg-[#e8e0f5] rounded-full flex items-center justify-center flex-shrink-0"> <div className="flex items-center justify-center flex-shrink-0">
<Mail className="h-6 w-6 text-[#664fa3]" /> <Mail className="size-12 text-[var(--purple-lavender)]" />
</div> </div>
<div> <div>
<a <a
href="mailto:info@loaftx.org" href="mailto:info@loaftx.org"
className="text-[#664fa3] text-xl font-semibold hover:text-[#48286e] transition-colors" className="text-[var(--purple-electric)] text-xl font-semibold hover:text-[var(--purple-deep)] transition-colors"
style={{ fontFamily: "'Inter', sans-serif" }} style={{ fontFamily: "'Inter', sans-serif" }}
> >
info@loaftx.org info@loaftx.org
@@ -250,16 +250,16 @@ const ContactUs = () => {
</Card> </Card>
{/* Address Card */} {/* Address Card */}
<Card className="p-6 bg-white rounded-2xl border-2 border-[#ddd8eb] shadow-lg"> <Card className="p-6 bg-background rounded-2xl ">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="w-12 h-12 bg-[#e8e0f5] rounded-full flex items-center justify-center flex-shrink-0"> <div className="flex items-center justify-center flex-shrink-0">
<MapPin className="h-6 w-6 text-[#664fa3]" /> <PiMapTrifoldBold className="size-12 text-[var(--purple-lavender)]" />
</div> </div>
<div> <div>
<p className="text-[#48286e] text-lg font-semibold mb-1" style={{ fontFamily: "'Inter', sans-serif" }}> <p className="text-[var(--purple-deep)] text-[28px] font-semibold mb-1" style={{ fontFamily: "'Poppins', sans-serif" }}>
LOAF LOAF
</p> </p>
<p className="text-[#664fa3] text-base leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-[var(--purple-deep)] text-[28px] font-semibold leading-relaxed" style={{ fontFamily: "'Poppins', sans-serif" }}>
P.O. Box 7207<br /> P.O. Box 7207<br />
Houston, Texas 77248-7207 Houston, Texas 77248-7207
</p> </p>

View File

@@ -7,17 +7,29 @@ import { Button } from '../components/ui/button';
import { Badge } from '../components/ui/badge'; import { Badge } from '../components/ui/badge';
import Navbar from '../components/Navbar'; import Navbar from '../components/Navbar';
import MemberFooter from '../components/MemberFooter'; import MemberFooter from '../components/MemberFooter';
import { Calendar, User, CheckCircle, Clock, AlertCircle, Mail, Users, Image, FileText, DollarSign, Scale } from 'lucide-react'; import { Calendar, User, CheckCircle, Clock, AlertCircle, Mail, Users, Image, FileText, DollarSign, Scale, Receipt, Heart, CreditCard } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import TransactionHistory from '../components/TransactionHistory';
import MemberBadge from '@/components/MemberBadge';
import useMemberTiers from '../hooks/use-member-tiers'
const Dashboard = () => { const Dashboard = () => {
const { user, resendVerificationEmail, refreshUser } = useAuth(); const { user, resendVerificationEmail, refreshUser } = useAuth();
const [events, setEvents] = useState([]); const [events, setEvents] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [resendLoading, setResendLoading] = useState(false); const [resendLoading, setResendLoading] = useState(false);
const [eventActivity, setEventActivity] = useState(null);
const [activityLoading, setActivityLoading] = useState(true);
const [transactionsLoading, setTransactionsLoading] = useState(true);
const [transactions, setTransactions] = useState({ subscriptions: [], donations: [] });
const [activeTransactionTab, setActiveTransactionTab] = useState('all');
const joinedDate = user?.member_since || user?.created_at;
const { tiers, loading: tiersLoading } = useMemberTiers();
useEffect(() => { useEffect(() => {
fetchUpcomingEvents(); fetchUpcomingEvents();
fetchEventActivity();
fetchTransactions();
}, []); }, []);
const fetchUpcomingEvents = async () => { const fetchUpcomingEvents = async () => {
@@ -32,6 +44,30 @@ const Dashboard = () => {
} }
}; };
const fetchEventActivity = async () => {
try {
const response = await api.get('/members/event-activity');
setEventActivity(response.data);
} catch (error) {
console.error('Failed to fetch event activity:', error);
} finally {
setActivityLoading(false);
}
};
const fetchTransactions = async () => {
try {
setTransactionsLoading(true);
const response = await api.get('/members/transactions');
setTransactions(response.data);
} catch (error) {
console.error('Failed to load transactions:', error);
// Don't show error toast - transactions are optional
} finally {
setTransactionsLoading(false);
}
};
const handleResendVerification = async () => { const handleResendVerification = async () => {
setResendLoading(true); setResendLoading(true);
try { try {
@@ -56,13 +92,14 @@ const Dashboard = () => {
} }
}; };
const getStatusBadge = (status) => { const getStatusBadge = (status) => {
const statusConfig = { const statusConfig = {
pending_email: { icon: Clock, label: 'Pending Email', className: 'bg-orange-100 text-orange-700' }, pending_email: { icon: Clock, label: 'Pending Email', className: 'bg-orange-100 text-orange-700' },
pending_validation: { icon: Clock, label: 'Pending Validation', className: 'bg-gray-200 text-gray-700' }, pending_validation: { icon: Clock, label: 'Pending Validation', className: 'bg-gray-200 text-gray-700' },
pre_validated: { icon: CheckCircle, label: 'Pre-Validated', className: 'bg-[#81B29A] text-white' }, pre_validated: { icon: CheckCircle, label: 'Pre-Validated', className: 'bg-[var(--green-light)] text-white' },
payment_pending: { icon: AlertCircle, label: 'Payment Pending', className: 'bg-orange-500 text-white' }, payment_pending: { icon: AlertCircle, label: 'Payment Pending', className: 'bg-orange-500 text-white' },
active: { icon: CheckCircle, label: 'Active', className: 'bg-[#81B29A] text-white' }, active: { icon: CheckCircle, label: 'Active', className: 'bg-[var(--green-light)] text-white' },
inactive: { icon: AlertCircle, label: 'Inactive', className: 'bg-gray-400 text-white' }, inactive: { icon: AlertCircle, label: 'Inactive', className: 'bg-gray-400 text-white' },
canceled: { icon: AlertCircle, label: 'Canceled', className: 'bg-red-100 text-red-700' }, canceled: { icon: AlertCircle, label: 'Canceled', className: 'bg-red-100 text-red-700' },
expired: { icon: Clock, label: 'Expired', className: 'bg-red-500 text-white' }, expired: { icon: Clock, label: 'Expired', className: 'bg-red-500 text-white' },
@@ -96,31 +133,33 @@ const Dashboard = () => {
return messages[status] || ''; return messages[status] || '';
}; };
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-background">
<Navbar /> <Navbar />
<div className="max-w-7xl mx-auto px-6 py-12"> <div className="max-w-7xl mx-auto px-6 py-12">
{/* Welcome Section */} {/* Welcome Section */}
<div className="mb-12"> <div className="mb-12">
<h1 className="text-4xl md:text-5xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Welcome Back, {user?.first_name}! Welcome Back, {user?.first_name}!
</h1> </h1>
<p className="text-lg text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Here's an overview of your membership status and upcoming events. Here's an overview of your membership status and upcoming events.
</p> </p>
</div> </div>
{/* Email Verification Alert */} {/* Email Verification Alert */}
{user && !user.email_verified && ( {user && !user.email_verified && (
<Card className="p-6 bg-[#f1eef9] border-2 border-[#664fa3] mb-8"> <Card className="p-6 bg-[var(--lavender-300)] border-2 border-brand-purple mb-8">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<AlertCircle className="h-6 w-6 text-[#664fa3] flex-shrink-0 mt-1" /> <AlertCircle className="h-6 w-6 text-brand-purple flex-shrink-0 mt-1" />
<div className="flex-1"> <div className="flex-1">
<h3 className="text-lg font-semibold text-[#422268] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}> <h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
Verify Your Email Address Verify Your Email Address
</h3> </h3>
<p className="text-[#664fa3] mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Please verify your email address to complete your registration. Please verify your email address to complete your registration.
Check your inbox for the verification link. Check your inbox for the verification link.
</p> </p>
@@ -128,7 +167,7 @@ const Dashboard = () => {
onClick={handleResendVerification} onClick={handleResendVerification}
disabled={resendLoading} disabled={resendLoading}
variant="outline" variant="outline"
className="border-2 border-[#664fa3] text-[#664fa3] hover:bg-[#664fa3] hover:text-white" className="border-2 border-brand-purple text-brand-purple hover:bg-brand-purple hover:text-white"
> >
<Mail className="h-4 w-4 mr-2" /> <Mail className="h-4 w-4 mr-2" />
{resendLoading ? 'Sending...' : 'Resend Verification Email'} {resendLoading ? 'Sending...' : 'Resend Verification Email'}
@@ -139,64 +178,88 @@ const Dashboard = () => {
)} )}
{/* Status Card */} {/* Status Card */}
<Card className="p-8 bg-white rounded-2xl border border-[#ddd8eb] shadow-lg mb-8" data-testid="status-card"> <Card className="p-8 bg-background rounded-2xl border border-[var(--neutral-800)] shadow-lg mb-8" data-testid="status-card">
<div className="flex items-start justify-between flex-wrap gap-4"> <div className="flex items-start justify-between flex-wrap gap-4">
<div> <div>
<h2 className="text-2xl font-semibold text-[#422268] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}> <h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
Membership Status Membership Status
</h2> </h2>
<div className="mb-4"> <div className="mb-4">
{getStatusBadge(user?.status)} {getStatusBadge(user?.status)}
</div> </div>
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{getStatusMessage(user?.status)} {getStatusMessage(user?.status)}
</p> </p>
</div> </div>
<Link to="/profile"> <Link to="/profile">
<Button <Button
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-6" className="btn-lavender"
data-testid="view-profile-button" data-testid="view-profile-button"
> >
<User className="h-4 w-4 mr-2" /> <User className="h-4 w-4 mr-2" />
View Profile Edit Profile
</Button> </Button>
</Link> </Link>
</div> </div>
</Card> </Card>
{/* Grid Layout */} {/* Grid Layout */}
<div className="grid lg:grid-cols-3 gap-8"> <div className="grid lg:grid-cols-2 gap-8">
{/* Quick Stats */} {/* Quick Stats */}
<Card className="p-6 bg-white rounded-2xl border border-[#ddd8eb]" data-testid="quick-stats-card"> <div className='space-y-8'>
<h3 className="text-xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]" data-testid="quick-stats-card">
<h3 className="text-xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Quick Info Quick Info
</h3> </h3>
<div className="space-y-4"> <div className="space-y-4">
{/* member date and badge */}
<div className='flex justify-between'>
<div> <div>
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Email</p> <p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Member Since</p>
<p className="text-[#422268] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{user?.email}</p> <p className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
</div> {joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}
<div>
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Role</p>
<p className="text-[#422268] font-medium capitalize" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{user?.role}</p>
</div>
<div>
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Member Since</p>
<p className="text-[#422268] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{user?.created_at ? new Date(user.created_at).toLocaleDateString() : 'N/A'}
</p> </p>
</div> </div>
{!tiersLoading && (
<div className='lg:mr-10'>
<MemberBadge memberSince={joinedDate} tiers={tiers} />
</div>
)}
</div>
{/* email */}
<div>
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Email</p>
<p className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{user?.email}</p>
</div>
{/* role */}
<div>
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Role</p>
<p className="text-[var(--purple-ink)] font-medium capitalize" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{user?.role}</p>
</div>
</div>
</Card>
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]" data-testid="quick-stats-card">
<h3 className="text-xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Membership Info
</h3>
<div className="space-y-4">
{!user.subscription_end_date && !user.subscription_end_date && (
<div>No subscriptions yet</div>
)}
{user?.subscription_start_date && user?.subscription_end_date && ( {user?.subscription_start_date && user?.subscription_end_date && (
<> <>
<div className="pt-4 border-t border-[#ddd8eb]"> <div className="pt-4 border-t border-[var(--neutral-800)]">
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Membership Period</p> <p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Membership Period</p>
<p className="text-[#422268] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{new Date(user.subscription_start_date).toLocaleDateString()} - {new Date(user.subscription_end_date).toLocaleDateString()} {new Date(user.subscription_start_date).toLocaleDateString()} - {new Date(user.subscription_end_date).toLocaleDateString()}
</p> </p>
</div> </div>
<div> <div>
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Days Remaining</p> <p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Days Remaining</p>
<p className="text-[#422268] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{Math.max(0, Math.ceil((new Date(user.subscription_end_date) - new Date()) / (1000 * 60 * 60 * 24)))} days {Math.max(0, Math.ceil((new Date(user.subscription_end_date) - new Date()) / (1000 * 60 * 60 * 24)))} days
</p> </p>
</div> </div>
@@ -204,45 +267,43 @@ const Dashboard = () => {
)} )}
</div> </div>
</Card> </Card>
</div>
{/* Upcoming Events */} {/* Upcoming Events */}
<Card className="lg:col-span-2 p-6 bg-white rounded-2xl border border-[#ddd8eb]" data-testid="upcoming-events-card"> <Card className="lg:col-span-1 p-6 bg-background rounded-2xl border border-[var(--neutral-800)]" data-testid="upcoming-events-card">
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<h3 className="text-xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <h3 className="text-xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Upcoming Events My Event Activity
</h3> </h3>
<Link to="/events"> <Link to="/events">
<Button <Button className="bg-[var(--purple-lavender)] text-white hover:bg-[var(--purple-muted)] rounded-full dark:hover:bg-brand-lavender dark:hover:text-brand-dark-lavender px-6">
variant="ghost" <Calendar className="h-4 w-4 mr-2" />
className="text-[#ff9e77] hover:text-[#664fa3]" Browse Events
data-testid="view-all-events-button"
>
View All
</Button> </Button>
</Link> </Link>
</div> </div>
{loading ? ( {loading ? (
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading events...</p> <p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading events...</p>
) : events.length > 0 ? ( ) : events.length > 0 ? (
<div className="space-y-4"> <div className="space-y-4">
{events.map((event) => ( {events.map((event) => (
<Link to={`/events/${event.id}`} key={event.id}> <Link to={`/events/${event.id}`} key={event.id}>
<div <div
className="p-4 border border-[#ddd8eb] rounded-xl hover:border-[#664fa3] hover:shadow-md transition-all cursor-pointer" className="p-4 border border-[var(--neutral-800)] rounded-xl hover:border-brand-purple hover:shadow-md transition-all cursor-pointer"
data-testid={`event-card-${event.id}`} data-testid={`event-card-${event.id}`}
> >
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="bg-[#DDD8EB]/20 p-3 rounded-lg"> <div className="bg-[var(--neutral-800)]/20 p-3 rounded-lg">
<Calendar className="h-6 w-6 text-[#664fa3]" /> <Calendar className="h-6 w-6 text-brand-purple " />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<h4 className="font-semibold text-[#422268] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>{event.title}</h4> <h4 className="font-semibold text-[var(--purple-ink)] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>{event.title}</h4>
<p className="text-sm text-[#664fa3] mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{new Date(event.start_at).toLocaleDateString()} at{' '} {new Date(event.start_at).toLocaleDateString()} at{' '}
{new Date(event.start_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} {new Date(event.start_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</p> </p>
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{event.location}</p> <p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{event.location}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -251,9 +312,9 @@ const Dashboard = () => {
</div> </div>
) : ( ) : (
<div className="text-center py-12"> <div className="text-center py-12">
<Calendar className="h-16 w-16 text-[#ddd8eb] mx-auto mb-4" /> <Calendar className="h-16 w-16 text-[var(--neutral-800)] mx-auto mb-4" />
<p className="text-[#664fa3] mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>No upcoming events at the moment.</p> <p className="text-brand-purple mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>No upcoming events at the moment.</p>
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Check back later for new events!</p> <p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Check back later for new events!</p>
</div> </div>
)} )}
</Card> </Card>
@@ -261,12 +322,12 @@ const Dashboard = () => {
{/* CTA Section */} {/* CTA Section */}
{user?.status === 'pending_validation' && ( {user?.status === 'pending_validation' && (
<Card className="mt-8 p-8 bg-gradient-to-br from-[#DDD8EB]/20 to-[#f1eef9]/20 rounded-2xl border border-[#ddd8eb]"> <Card className="mt-8 p-8 bg-gradient-to-br from-[var(--neutral-800)]/20 to-[var(--lavender-300)]/20 rounded-2xl border border-[var(--neutral-800)]">
<div className="text-center"> <div className="text-center">
<h3 className="text-2xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h3 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Application Under Review Application Under Review
</h3> </h3>
<p className="text-[#664fa3] mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Your membership application is being reviewed by our admin team. You'll be notified once validated! Your membership application is being reviewed by our admin team. You'll be notified once validated!
</p> </p>
</div> </div>
@@ -275,20 +336,20 @@ const Dashboard = () => {
{/* Payment Prompt for payment_pending status */} {/* Payment Prompt for payment_pending status */}
{user?.status === 'payment_pending' && ( {user?.status === 'payment_pending' && (
<Card className="mt-8 p-8 bg-gradient-to-br from-[#DDD8EB]/20 to-[#f1eef9]/20 rounded-2xl border-2 border-[#664fa3]"> <Card className="mt-8 p-8 bg-gradient-to-br from-[var(--neutral-800)]/20 to-[var(--lavender-300)]/20 rounded-2xl border-2 border-brand-purple ">
<div className="text-center"> <div className="text-center">
<div className="mb-4"> <div className="mb-4">
<AlertCircle className="h-16 w-16 text-[#664fa3] mx-auto" /> <AlertCircle className="h-16 w-16 text-brand-purple mx-auto" />
</div> </div>
<h3 className="text-2xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h3 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Complete Your Payment Complete Your Payment
</h3> </h3>
<p className="text-[#664fa3] mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Great news! Your membership application has been validated. Complete your payment to activate your membership and gain full access to all member benefits. Great news! Your membership application has been validated. Complete your payment to activate your membership and gain full access to all member benefits.
</p> </p>
<Link to="/plans"> <Link to="/plans">
<Button <Button
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-8 py-6 text-lg font-semibold" className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background rounded-full px-8 py-6 text-lg font-semibold"
data-testid="complete-payment-cta" data-testid="complete-payment-cta"
> >
<CheckCircle className="mr-2 h-5 w-5" /> <CheckCircle className="mr-2 h-5 w-5" />
@@ -298,6 +359,19 @@ const Dashboard = () => {
</div> </div>
</Card> </Card>
)} )}
{/* Transaction History Section */}
<div className="mt-8">
<TransactionHistory
subscriptions={transactions.subscriptions}
donations={transactions.donations}
totalSubscriptionCents={transactions.total_subscription_amount_cents}
totalDonationCents={transactions.total_donation_amount_cents}
loading={transactionsLoading}
isAdmin={false}
/>
</div>
</div> </div>
<MemberFooter /> <MemberFooter />
</div> </div>

View File

@@ -55,33 +55,37 @@ const Donate = () => {
}; };
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen">
<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-bl from-[var(--neutral-100:)] to-[var(--neutral-800)] 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-4xl mx-auto text-center"> <div className="max-w-4xl mx-auto text-center h-full">
<div className="flex justify-center mb-4"> <div className="flex justify-center mb-4">
<img src={loafHearts} alt="Hearts" className="w-32 h-auto" onError={(e) => e.target.style.display = 'none'} /> <img src={loafHearts} alt="Hearts" className="w-32 h-auto" onError={(e) => e.target.style.display = 'none'} />
</div> </div>
<h1 className="text-3xl sm:text-4xl md:text-5xl font-bold text-[#48286e] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}> <h1 className="text-3xl sm:text-4xl md:text-5xl font-bold text-[var(--purple-deep)] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
Donate Donate
</h1> </h1>
<p className="text-xl text-[#48286e]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-xl text-[var(--purple-deep)] font-medium" style={{ fontFamily: "'Poppins', sans-serif" }}>
We really appreciate your donations. You can make your donation online We really appreciate your donations. You can make your donation online
or send a check by mail. or send a check by mail.
</p> </p>
</div> </div>
</section> </section>
{/* Columns */}
<div className="py-12">
<div className='grid grid-cols-1 items-stretch lg:grid-cols-[2fr_1fr] gap-8 lg:max-h-[450px]'>
{/* Donation Amount Buttons */} {/* Donation Amount Buttons */}
<section className="py-12"> <section className="flex flex-col h-full">
<div className="max-w-4xl mx-auto"> <div className="mx-auto flex-1 w-full h-full">
<Card className="p-8 bg-white rounded-2xl shadow-lg"> <Card className="p-8 bg-background rounded-3xl w-full h-full content-center">
<div className="flex items-center gap-4 mb-6"> <div className="flex items-center gap-4 mb-6">
<CreditCard className="w-12 h-12 text-[#664fa3]" /> <CreditCard className="size-24 text-[var(--purple-lavender)]" />
<h2 className="text-3xl font-bold text-[#48286e]" style={{ fontFamily: "'Inter', sans-serif" }}> <h2 className="text-3xl font-bold text-[var(--purple-deep)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Select Your Donation Amount Select Your Donation Amount
</h2> </h2>
</div> </div>
@@ -93,7 +97,7 @@ const Donate = () => {
key={amount} key={amount}
onClick={() => handleDonateAmount(amount * 100)} onClick={() => handleDonateAmount(amount * 100)}
disabled={processingAmount === amount * 100} disabled={processingAmount === amount * 100}
className="bg-[#664fa3] hover:bg-[#48286e] text-white text-xl py-8 rounded-full disabled:opacity-50" className="bg-[var(--purple-lavender)] hover:bg-[var(--purple-deep)] text-white text-xl py-8 rounded-full disabled:opacity-50"
> >
{processingAmount === amount * 100 ? ( {processingAmount === amount * 100 ? (
<Loader2 className="h-6 w-6 animate-spin" /> <Loader2 className="h-6 w-6 animate-spin" />
@@ -108,13 +112,13 @@ const Donate = () => {
<Button <Button
onClick={() => setCustomAmountDialogOpen(true)} onClick={() => setCustomAmountDialogOpen(true)}
disabled={processingAmount !== null} disabled={processingAmount !== null}
className="w-full bg-[#664fa3] hover:bg-[#48286e] text-white text-xl py-8 rounded-full flex items-center justify-center gap-2" className="w-full bg-[var(--purple-lavender)] hover:bg-[var(--purple-deep)] text-white text-xl py-8 rounded-full flex items-center justify-center gap-2"
> >
<Heart className="h-6 w-6" /> <Heart className="h-6 w-6" />
Donate Any Amount Donate Any Amount
</Button> </Button>
<p className="text-sm text-[#664fa3] text-center mt-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-sm text-[var(--purple-lavender)] text-center mt-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Secure donation processing powered by Stripe Secure donation processing powered by Stripe
</p> </p>
</Card> </Card>
@@ -122,66 +126,76 @@ const Donate = () => {
</section> </section>
{/* Alternative Payment Methods */} {/* Alternative Payment Methods */}
<section className="py-12"> <section className="flex flex-col">
<div className="max-w-6xl mx-auto"> <div className="max-w-6xl mx-auto w-full">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8"> <div className="flex flex-col gap-8 w-full">
{/* Mail Check */} {/* Mail Check */}
<Card className="p-8 bg-white rounded-2xl shadow-lg"> <Card className="p-8 bg-background rounded-3xl flex gap-4 items-center flex-1">
<Mail className="w-12 h-12 text-[#664fa3] mb-4" /> <Mail className="size-24 text-[var(--purple-lavender)]" />
<h3 className="text-2xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
<div>
<h3 className="text-2xl font-bold text-[var(--purple-deep)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Mail a Check Mail a Check
</h3> </h3>
<p className="text-lg text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-lg text-[var(--purple-deep)] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Our mailing address for checks:<br /> Our mailing address for checks:<br />
<span className="font-semibold">LOAF</span><br /> <span className="font-semibold">LOAF</span><br />
P.O. Box 7207<br /> P.O. Box 7207<br />
Houston, Texas 77248-7207 Houston, Texas 77248-7207
</p> </p>
</div>
</Card> </Card>
{/* Zelle */} {/* Zelle */}
<Card className="p-8 bg-white rounded-2xl shadow-lg"> <Card className="p-8 bg-background rounded-3xl flex gap-4 items-center flex-1">
<div className="mb-4"> <div className="w-44">
<img src={zelleLogo} alt="Zelle" className="h-32" onError={(e) => e.target.style.display = 'none'} /> <img src={zelleLogo} alt="Zelle" className=" w-32" onError={(e) => e.target.style.display = 'none'} />
</div> </div>
<h3 className="text-2xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
<div>
<h3 className="text-2xl font-bold text-[var(--purple-deep)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Pay with Zelle Pay with Zelle
</h3> </h3>
<p className="text-lg text-[#48286e] leading-relaxed mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-lg text-[var(--purple-deep)] leading-relaxed mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
If your bank allows the use of Zelle, please feel free to send money to: If your bank allows the use of Zelle, please feel free to send money to:
</p> </p>
<a href="mailto:LOAFHoustonTX@gmail.com" <a href="mailto:LOAFHoustonTX@gmail.com"
className="text-[#664fa3] text-lg font-bold underline hover:text-[#48286e] transition-colors" className="text-[var(--purple-lavender)] text-lg font-bold underline hover:text-[var(--purple-deep)] transition-colors"
style={{ fontFamily: "'Nunito Sans', sans-serif" }}> style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
LOAFHoustonTX@gmail.com LOAFHoustonTX@gmail.com
</a> </a>
</div>
</Card> </Card>
</div> </div>
</div> </div>
</section> </section>
</div>
</div>
{/* Columns end */}
</main> </main>
<PublicFooter /> <PublicFooter />
{/* Custom Amount Dialog */} {/* Custom Amount Dialog */}
<Dialog open={customAmountDialogOpen} onOpenChange={setCustomAmountDialogOpen}> <Dialog open={customAmountDialogOpen} onOpenChange={setCustomAmountDialogOpen}>
<DialogContent className="sm:max-w-[450px] bg-white rounded-2xl"> <DialogContent className="sm:max-w-[450px] bg-background rounded-3xl">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-2xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <DialogTitle className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Enter Donation Amount Enter Donation Amount
</DialogTitle> </DialogTitle>
<DialogDescription className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <DialogDescription className="text-[var(--purple-lavender)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Choose how much you'd like to donate to support our community Choose how much you'd like to donate to support our community
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
<div> <div>
<Label htmlFor="customAmount" className="text-[#422268] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}> <Label htmlFor="customAmount" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
Amount (USD) Amount (USD)
</Label> </Label>
<div className="relative mt-2"> <div className="relative mt-2">
<span className="absolute left-4 top-1/2 transform -translate-y-1/2 text-[#664fa3] text-xl font-semibold"> <span className="absolute left-4 top-1/2 transform -translate-y-1/2 text-[var(--purple-lavender)] text-xl font-semibold">
$ $
</span> </span>
<Input <Input
@@ -192,7 +206,7 @@ const Donate = () => {
value={customAmount} value={customAmount}
onChange={(e) => setCustomAmount(e.target.value)} onChange={(e) => setCustomAmount(e.target.value)}
placeholder="50.00" placeholder="50.00"
className="pl-10 h-14 text-xl border-2 border-[#ddd8eb] focus:border-[#664fa3] rounded-xl" className="pl-10 h-14 text-xl border-2 border-[var(--neutral-800)] focus:border-[var(--purple-lavender)] rounded-xl"
onKeyPress={(e) => { onKeyPress={(e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
handleCustomDonate(); handleCustomDonate();
@@ -200,13 +214,13 @@ const Donate = () => {
}} }}
/> />
</div> </div>
<p className="text-sm text-[#664fa3] mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-sm text-[var(--purple-lavender)] mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Minimum donation: $1.00 Minimum donation: $1.00
</p> </p>
</div> </div>
<div className="bg-[#f1eef9] rounded-lg p-4"> <div className="bg-[var(--lavender-300)] rounded-lg p-4">
<p className="text-sm text-[#422268] text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-sm text-[var(--purple-ink)] text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<strong>Thank you for supporting LOAF!</strong><br /> <strong>Thank you for supporting LOAF!</strong><br />
Your donation helps us continue our mission and provide meaningful experiences for our community. Your donation helps us continue our mission and provide meaningful experiences for our community.
</p> </p>
@@ -218,14 +232,14 @@ const Donate = () => {
type="button" type="button"
variant="outline" variant="outline"
onClick={() => setCustomAmountDialogOpen(false)} onClick={() => setCustomAmountDialogOpen(false)}
className="rounded-full border-2 border-[#ddd8eb]" className="rounded-full border-2 border-[var(--neutral-800)]"
> >
Cancel Cancel
</Button> </Button>
<Button <Button
type="button" type="button"
onClick={handleCustomDonate} onClick={handleCustomDonate}
className="bg-[#664fa3] text-white hover:bg-[#48286e] rounded-full" className="bg-[var(--purple-lavender)] text-white hover:bg-[var(--purple-deep)] rounded-full"
> >
Continue to Payment Continue to Payment
</Button> </Button>

View File

@@ -11,12 +11,12 @@ const DonationSuccess = () => {
const loafHearts = `${process.env.PUBLIC_URL}/loaf-hearts.png`; const loafHearts = `${process.env.PUBLIC_URL}/loaf-hearts.png`;
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-background">
<PublicNavbar /> <PublicNavbar />
<main className="bg-gradient-to-b from-white to-[#f1eef9] px-6 py-20"> <main className="bg-gradient-to-b from-white to-[var(--lavender-300)] px-6 py-20">
<div className="max-w-2xl mx-auto"> <div className="max-w-2xl mx-auto">
<Card className="p-6 sm:p-8 md:p-12 bg-white rounded-2xl border-2 border-[#ddd8eb] shadow-xl text-center"> <Card className="p-6 sm:p-8 md:p-12 bg-background rounded-2xl border-2 border-[var(--neutral-800)] shadow-xl text-center">
{/* Success Icon */} {/* Success Icon */}
<div className="flex justify-center mb-4"> <div className="flex justify-center mb-4">
<img <img
@@ -26,34 +26,34 @@ const DonationSuccess = () => {
onError={(e) => e.target.style.display = 'none'} onError={(e) => e.target.style.display = 'none'}
/> />
</div> </div>
<div className="inline-flex items-center justify-center w-20 h-20 bg-[#81B29A]/10 rounded-full mb-6"> <div className="inline-flex items-center justify-center w-20 h-20 bg-[var(--green-light)]/10 rounded-full mb-6">
<CheckCircle className="h-12 w-12 text-[#81B29A]" /> <CheckCircle className="h-12 w-12 text-[var(--green-light)]" />
</div> </div>
{/* Title */} {/* Title */}
<h1 className="text-2xl sm:text-3xl md:text-4xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h1 className="text-2xl sm:text-3xl md:text-4xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Thank You for Your Donation! Thank You for Your Donation!
</h1> </h1>
{/* Message */} {/* Message */}
<div className="space-y-4 mb-8"> <div className="space-y-4 mb-8">
<p className="text-xl text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-xl text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Your generous contribution helps support our community and continue our mission. Your generous contribution helps support our community and continue our mission.
</p> </p>
<div className="bg-gradient-to-r from-[#f1eef9] to-[#DDD8EB]/30 rounded-2xl p-6 border-2 border-[#ddd8eb]"> <div className="bg-gradient-to-r from-[var(--lavender-300)] to-[var(--neutral-800)]/30 rounded-2xl p-6 border-2 border-[var(--neutral-800)]">
<div className="flex items-center justify-center gap-2 text-[#ff9e77] mb-2"> <div className="flex items-center justify-center gap-2 text-[var(--orange-light)] mb-2">
<Heart className="h-6 w-6" /> <Heart className="h-6 w-6" />
<span className="text-lg font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}> <span className="text-lg font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
Your Support Makes a Difference Your Support Makes a Difference
</span> </span>
</div> </div>
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
A receipt for your donation has been sent to your email address. A receipt for your donation has been sent to your email address.
</p> </p>
</div> </div>
<p className="text-base text-[#664fa3] pt-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-base text-brand-purple pt-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
We deeply appreciate your support and commitment to LOAF's mission of building a vibrant, inclusive community. We deeply appreciate your support and commitment to LOAF's mission of building a vibrant, inclusive community.
</p> </p>
</div> </div>
@@ -62,7 +62,7 @@ const DonationSuccess = () => {
<div className="flex flex-col sm:flex-row gap-4 justify-center"> <div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button <Button
onClick={() => navigate('/')} onClick={() => navigate('/')}
className="bg-[#664fa3] text-white hover:bg-[#422268] rounded-full px-8 py-6 text-lg font-medium shadow-lg" className="bg-brand-purple text-white hover:bg-[var(--purple-ink)] rounded-full px-8 py-6 text-lg font-medium shadow-lg"
style={{ fontFamily: "'Inter', sans-serif" }} style={{ fontFamily: "'Inter', sans-serif" }}
> >
Return to Home Return to Home
@@ -70,7 +70,7 @@ const DonationSuccess = () => {
<Button <Button
onClick={() => navigate('/donate')} onClick={() => navigate('/donate')}
variant="outline" variant="outline"
className="border-2 border-[#664fa3] text-[#664fa3] hover:bg-[#DDD8EB]/20 rounded-full px-8 py-6 text-lg font-medium" className="border-2 border-brand-purple text-brand-purple hover:bg-[var(--neutral-800)]/20 rounded-full px-8 py-6 text-lg font-medium"
style={{ fontFamily: "'Inter', sans-serif" }} style={{ fontFamily: "'Inter', sans-serif" }}
> >
Make Another Donation Make Another Donation
@@ -80,12 +80,12 @@ const DonationSuccess = () => {
{/* Additional Info */} {/* Additional Info */}
<div className="mt-12 text-center"> <div className="mt-12 text-center">
<p className="text-sm text-[#664fa3] mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-sm text-brand-purple mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Have questions about your donation? Have questions about your donation?
</p> </p>
<a <a
href="mailto:support@loaf.org" href="mailto:support@loaf.org"
className="text-[#ff9e77] hover:text-[#664fa3] font-medium transition-colors" className="text-[var(--orange-light)] hover:text-brand-purple font-medium transition-colors"
style={{ fontFamily: "'Inter', sans-serif" }} style={{ fontFamily: "'Inter', sans-serif" }}
> >
Contact us at support@loaf.org Contact us at support@loaf.org

View File

@@ -48,10 +48,10 @@ const EventDetails = () => {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-background">
<Navbar /> <Navbar />
<div className="flex items-center justify-center min-h-[60vh]"> <div className="flex items-center justify-center min-h-[60vh]">
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading event...</p> <p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading event...</p>
</div> </div>
</div> </div>
); );
@@ -62,30 +62,29 @@ const EventDetails = () => {
} }
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-background">
<Navbar /> <Navbar />
<div className="max-w-4xl mx-auto px-6 py-12"> <div className="max-w-4xl mx-auto px-6 py-12">
<button <button
onClick={() => navigate('/events')} onClick={() => navigate('/events')}
className="inline-flex items-center text-[#664fa3] hover:text-[#ff9e77] transition-colors mb-8" className="inline-flex items-center text-brand-purple hover:text-[var(--orange-light)] transition-colors mb-8"
data-testid="back-to-events-button" data-testid="back-to-events-button"
> >
<ArrowLeft className="h-4 w-4 mr-2" /> <ArrowLeft className="h-4 w-4 mr-2" />
Back to Events Back to Events
</button> </button>
<Card className="p-8 md:p-12 bg-white rounded-2xl border border-[#ddd8eb] shadow-lg"> <Card className="p-8 md:p-12 bg-background rounded-2xl border border-[var(--neutral-800)] shadow-lg">
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center gap-4 mb-6"> <div className="flex items-center gap-4 mb-6">
<div className="bg-[#DDD8EB]/20 p-4 rounded-xl"> <div className="bg-[var(--neutral-800)]/20 p-4 rounded-xl">
<Calendar className="h-10 w-10 text-[#664fa3]" /> <Calendar className="h-10 w-10 text-brand-purple " />
</div> </div>
{event.user_rsvp_status && ( {event.user_rsvp_status && (
<Badge <Badge
className={`px-4 py-2 rounded-full text-sm ${ className={`px-4 py-2 rounded-full text-sm ${event.user_rsvp_status === 'yes'
event.user_rsvp_status === 'yes' ? 'bg-[var(--green-light)] text-white'
? 'bg-[#81B29A] text-white'
: event.user_rsvp_status === 'no' : event.user_rsvp_status === 'no'
? 'bg-gray-400 text-white' ? 'bg-gray-400 text-white'
: 'bg-orange-100 text-orange-700' : 'bg-orange-100 text-orange-700'
@@ -98,12 +97,12 @@ const EventDetails = () => {
)} )}
</div> </div>
<h1 className="text-4xl md:text-5xl font-semibold text-[#422268] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}> <h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
{event.title} {event.title}
</h1> </h1>
<div className="space-y-4 text-lg"> <div className="space-y-4 text-lg">
<div className="flex items-center gap-3 text-[#664fa3]"> <div className="flex items-center gap-3 text-brand-purple ">
<Calendar className="h-5 w-5" /> <Calendar className="h-5 w-5" />
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{new Date(event.start_at).toLocaleDateString('en-US', { {new Date(event.start_at).toLocaleDateString('en-US', {
@@ -114,18 +113,18 @@ const EventDetails = () => {
})} })}
</span> </span>
</div> </div>
<div className="flex items-center gap-3 text-[#664fa3]"> <div className="flex items-center gap-3 text-brand-purple ">
<Calendar className="h-5 w-5" /> <Calendar className="h-5 w-5" />
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{new Date(event.start_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} -{' '} {new Date(event.start_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} -{' '}
{new Date(event.end_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} {new Date(event.end_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</span> </span>
</div> </div>
<div className="flex items-center gap-3 text-[#664fa3]"> <div className="flex items-center gap-3 text-brand-purple ">
<MapPin className="h-5 w-5" /> <MapPin className="h-5 w-5" />
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{event.location}</span> <span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{event.location}</span>
</div> </div>
<div className="flex items-center gap-3 text-[#664fa3]"> <div className="flex items-center gap-3 text-brand-purple ">
<Users className="h-5 w-5" /> <Users className="h-5 w-5" />
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{event.rsvp_count || 0} {event.rsvp_count === 1 ? 'person' : 'people'} attending {event.rsvp_count || 0} {event.rsvp_count === 1 ? 'person' : 'people'} attending
@@ -136,28 +135,27 @@ const EventDetails = () => {
</div> </div>
{event.description && ( {event.description && (
<div className="mb-8 pb-8 border-b border-[#ddd8eb]"> <div className="mb-8 pb-8 border-b border-[var(--neutral-800)]">
<h2 className="text-2xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
About This Event About This Event
</h2> </h2>
<p className="text-[#664fa3] leading-relaxed whitespace-pre-line" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple leading-relaxed whitespace-pre-line" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{event.description} {event.description}
</p> </p>
</div> </div>
)} )}
<div> <div>
<h2 className="text-2xl font-semibold text-[#422268] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}> <h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
RSVP to This Event RSVP to This Event
</h2> </h2>
<div className="flex gap-4 flex-wrap"> <div className="flex gap-4 flex-wrap">
<Button <Button
onClick={() => handleRSVP('yes')} onClick={() => handleRSVP('yes')}
disabled={rsvpLoading} disabled={rsvpLoading}
className={`rounded-full px-8 py-6 flex items-center gap-2 ${ className={`rounded-full px-8 py-6 flex items-center gap-2 ${event.user_rsvp_status === 'yes'
event.user_rsvp_status === 'yes' ? 'bg-[var(--green-light)] text-white'
? 'bg-[#81B29A] text-white' : 'bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-brand-lavender'
: 'bg-[#DDD8EB] text-[#422268] hover:bg-white'
}`} }`}
data-testid="rsvp-yes-button" data-testid="rsvp-yes-button"
> >
@@ -168,10 +166,9 @@ const EventDetails = () => {
onClick={() => handleRSVP('maybe')} onClick={() => handleRSVP('maybe')}
disabled={rsvpLoading} disabled={rsvpLoading}
variant="outline" variant="outline"
className={`rounded-full px-8 py-6 flex items-center gap-2 border-2 ${ className={`rounded-full px-8 py-6 flex items-center gap-2 border-2 ${event.user_rsvp_status === 'maybe'
event.user_rsvp_status === 'maybe'
? 'border-orange-400 bg-orange-100 text-orange-700' ? 'border-orange-400 bg-orange-100 text-orange-700'
: 'border-[#664fa3] text-[#664fa3] hover:bg-[#f1eef9]' : 'border-brand-purple text-brand-purple hover:bg-[var(--lavender-300)]'
}`} }`}
data-testid="rsvp-maybe-button" data-testid="rsvp-maybe-button"
> >
@@ -182,8 +179,7 @@ const EventDetails = () => {
onClick={() => handleRSVP('no')} onClick={() => handleRSVP('no')}
disabled={rsvpLoading} disabled={rsvpLoading}
variant="outline" variant="outline"
className={`rounded-full px-8 py-6 flex items-center gap-2 border-2 ${ className={`rounded-full px-8 py-6 flex items-center gap-2 border-2 ${event.user_rsvp_status === 'no'
event.user_rsvp_status === 'no'
? 'border-gray-400 bg-gray-100 text-gray-700' ? 'border-gray-400 bg-gray-100 text-gray-700'
: 'border-gray-400 text-gray-600 hover:bg-gray-50' : 'border-gray-400 text-gray-600 hover:bg-gray-50'
}`} }`}
@@ -195,11 +191,11 @@ const EventDetails = () => {
</div> </div>
{/* Add to Calendar Section */} {/* Add to Calendar Section */}
<div className="mt-8 pt-8 border-t border-[#ddd8eb]"> <div className="mt-8 pt-8 border-t border-[var(--neutral-800)]">
<h2 className="text-xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h2 className="text-xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Add to Your Calendar Add to Your Calendar
</h2> </h2>
<p className="text-[#664fa3] mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Never miss this event! Add it to your calendar app for reminders. Never miss this event! Add it to your calendar app for reminders.
</p> </p>
<AddToCalendarButton <AddToCalendarButton

View File

@@ -30,7 +30,7 @@ const Events = () => {
if (!rsvpStatus) return null; if (!rsvpStatus) return null;
const config = { const config = {
yes: { label: 'Going', className: 'bg-[#81B29A] text-white' }, yes: { label: 'Going', className: 'bg-[var(--green-light)] text-white' },
no: { label: 'Not Going', className: 'bg-gray-400 text-white' }, no: { label: 'Not Going', className: 'bg-gray-400 text-white' },
maybe: { label: 'Maybe', className: 'bg-orange-100 text-orange-700' } maybe: { label: 'Maybe', className: 'bg-orange-100 text-orange-700' }
}; };
@@ -46,67 +46,67 @@ const Events = () => {
}; };
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-background">
<Navbar /> <Navbar />
<div className="max-w-7xl mx-auto px-6 py-12"> <div className="max-w-7xl mx-auto px-6 py-12">
<div className="mb-12"> <div className="mb-12">
<h1 className="text-4xl md:text-5xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Upcoming Events Upcoming Events
</h1> </h1>
<p className="text-lg text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Browse and RSVP to our community events. Browse and RSVP to our community events.
</p> </p>
</div> </div>
{loading ? ( {loading ? (
<div className="text-center py-20"> <div className="text-center py-20">
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading events...</p> <p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading events...</p>
</div> </div>
) : events.length > 0 ? ( ) : events.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8">
{events.map((event) => ( {events.map((event) => (
<Link to={`/events/${event.id}`} key={event.id}> <Link to={`/events/${event.id}`} key={event.id}>
<Card <Card
className="p-6 bg-white rounded-2xl border border-[#ddd8eb] hover:shadow-lg hover:-translate-y-1 transition-all cursor-pointer h-full" className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-lg hover:-translate-y-1 transition-all cursor-pointer h-full"
data-testid={`event-card-${event.id}`} data-testid={`event-card-${event.id}`}
> >
<div className="flex justify-between items-start mb-4"> <div className="flex justify-between items-start mb-4">
<div className="bg-[#DDD8EB]/20 p-3 rounded-lg"> <div className="bg-[var(--neutral-800)]/20 p-3 rounded-lg">
<Calendar className="h-6 w-6 text-[#664fa3]" /> <Calendar className="h-6 w-6 text-brand-purple " />
</div> </div>
{getRSVPBadge(event.user_rsvp_status)} {getRSVPBadge(event.user_rsvp_status)}
</div> </div>
<h3 className="text-xl font-semibold text-[#422268] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}> <h3 className="text-xl font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
{event.title} {event.title}
</h3> </h3>
{event.description && ( {event.description && (
<p className="text-[#664fa3] mb-4 line-clamp-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple mb-4 line-clamp-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{event.description} {event.description}
</p> </p>
)} )}
<div className="space-y-2 text-sm"> <div className="space-y-2 text-sm">
<div className="flex items-center gap-2 text-[#664fa3]"> <div className="flex items-center gap-2 text-brand-purple ">
<Calendar className="h-4 w-4" /> <Calendar className="h-4 w-4" />
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{new Date(event.start_at).toLocaleDateString()} at{' '} {new Date(event.start_at).toLocaleDateString()} at{' '}
{new Date(event.start_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} {new Date(event.start_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</span> </span>
</div> </div>
<div className="flex items-center gap-2 text-[#664fa3]"> <div className="flex items-center gap-2 text-brand-purple ">
<MapPin className="h-4 w-4" /> <MapPin className="h-4 w-4" />
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{event.location}</span> <span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{event.location}</span>
</div> </div>
<div className="flex items-center gap-2 text-[#664fa3]"> <div className="flex items-center gap-2 text-brand-purple ">
<Users className="h-4 w-4" /> <Users className="h-4 w-4" />
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{event.rsvp_count || 0} attending</span> <span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{event.rsvp_count || 0} attending</span>
</div> </div>
</div> </div>
<div className="mt-6 flex items-center text-[#ff9e77] font-medium"> <div className="mt-6 flex items-center text-[var(--orange-light)] font-medium">
View Details View Details
<ArrowRight className="ml-2 h-4 w-4" /> <ArrowRight className="ml-2 h-4 w-4" />
</div> </div>
@@ -116,11 +116,11 @@ const Events = () => {
</div> </div>
) : ( ) : (
<div className="text-center py-20"> <div className="text-center py-20">
<Calendar className="h-20 w-20 text-[#ddd8eb] mx-auto mb-6" /> <Calendar className="h-20 w-20 text-[var(--neutral-800)] mx-auto mb-6" />
<h3 className="text-2xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h3 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
No Events Available No Events Available
</h3> </h3>
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
There are no upcoming events at the moment. Check back later! There are no upcoming events at the moment. Check back later!
</p> </p>
</div> </div>

View File

@@ -32,28 +32,28 @@ const ForgotPassword = () => {
}; };
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-background">
<PublicNavbar /> <PublicNavbar />
<div className="max-w-md mx-auto px-6 py-12"> <div className="max-w-md mx-auto px-6 py-12">
<div className="mb-8"> <div className="mb-8">
<Link to="/login" className="inline-flex items-center text-[#664fa3] hover:text-[#ff9e77] transition-colors"> <Link to="/login" className="inline-flex items-center text-brand-purple hover:text-[var(--orange-light)] transition-colors">
<ArrowLeft className="h-4 w-4 mr-2" /> <ArrowLeft className="h-4 w-4 mr-2" />
Back to Login Back to Login
</Link> </Link>
</div> </div>
<Card className="p-8 md:p-12 bg-white rounded-2xl border border-[#ddd8eb] shadow-lg"> <Card className="p-8 md:p-12 bg-background rounded-2xl border border-[var(--neutral-800)] shadow-lg">
{!submitted ? ( {!submitted ? (
<> <>
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-[#f1eef9] mb-4"> <div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-[var(--lavender-300)] mb-4">
<Mail className="h-8 w-8 text-[#664fa3]" /> <Mail className="h-8 w-8 text-brand-purple " />
</div> </div>
<h1 className="text-4xl md:text-5xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Forgot Password? Forgot Password?
</h1> </h1>
<p className="text-lg text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
No worries! Enter your email and we'll send you reset instructions. No worries! Enter your email and we'll send you reset instructions.
</p> </p>
</div> </div>
@@ -69,22 +69,22 @@ const ForgotPassword = () => {
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
placeholder="your.email@example.com" placeholder="your.email@example.com"
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
/> />
</div> </div>
<Button <Button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="w-full bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform disabled:opacity-50" className="w-full bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background rounded-full py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform disabled:opacity-50"
> >
{loading ? 'Sending...' : 'Send Reset Link'} {loading ? 'Sending...' : 'Send Reset Link'}
<ArrowRight className="ml-2 h-5 w-5" /> <ArrowRight className="ml-2 h-5 w-5" />
</Button> </Button>
<p className="text-center text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-center text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Remember your password?{' '} Remember your password?{' '}
<Link to="/login" className="text-[#ff9e77] hover:underline font-medium"> <Link to="/login" className="text-[var(--orange-light)] hover:underline font-medium">
Login here Login here
</Link> </Link>
</p> </p>
@@ -92,21 +92,21 @@ const ForgotPassword = () => {
</> </>
) : ( ) : (
<div className="text-center"> <div className="text-center">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-[#E8F5E9] mb-6"> <div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-[var(--green-bg)] mb-6">
<CheckCircle className="h-8 w-8 text-[#4CAF50]" /> <CheckCircle className="h-8 w-8 text-[var(--green-success)]" />
</div> </div>
<h1 className="text-4xl md:text-5xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Check Your Email Check Your Email
</h1> </h1>
<p className="text-lg text-[#664fa3] mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-lg text-brand-purple mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
If an account exists for <span className="font-medium text-[#422268]">{email}</span>, If an account exists for <span className="font-medium text-[var(--purple-ink)]">{email}</span>,
you will receive a password reset link shortly. you will receive a password reset link shortly.
</p> </p>
<p className="text-sm text-[#664fa3] mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-sm text-brand-purple mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
The link will expire in 1 hour. If you don't see the email, check your spam folder. The link will expire in 1 hour. If you don't see the email, check your spam folder.
</p> </p>
<Link to="/login"> <Link to="/login">
<Button className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-8 py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform"> <Button className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background rounded-full px-8 py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform">
Return to Login Return to Login
<ArrowRight className="ml-2 h-5 w-5" /> <ArrowRight className="ml-2 h-5 w-5" />
</Button> </Button>

View File

@@ -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-background 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-[var(--purple-deep)] mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
{children}
</h2>
);
const Paragragh = ({ children }) => (
<p className="text-md text-[var(--purple-deep)] 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-background">
<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-[var(--neutral-100:)] to-[var(--neutral-700)] 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-[var(--purple-deep)] "
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-[var(--purple-deep)]">
<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,223 +64,211 @@ 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'} />
<p className="text-sm text-[#48286e] mt-2 text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-sm text-[var(--purple-deep)] mt-2 text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Arden Eversmeyer and Charlotte Avery Arden Eversmeyer and Charlotte Avery
</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-[var(--purple-deep)] 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-[var(--purple-deep)] 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-[var(--purple-deep)] 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-[var(--purple-deep)] 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-[var(--purple-deep)] 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-[var(--purple-deep)]" 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&apos;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 welcomeunchanged 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&apos;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-[var(--purple-deep)] 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-[var(--purple-deep)] 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-[var(--purple-deep)] 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&apos;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-[var(--purple-deep)] 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&apos;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-[var(--purple-deep)] 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-[var(--purple-deep)] 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-[var(--purple-deep)] 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-[var(--purple-deep)] 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&apos;t want to attend meetings, we changed Third Sunday Meeting to Meet &apos;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-[var(--purple-deep)] 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-[var(--purple-deep)] 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-[var(--purple-deep)] 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&apos;s, it was our way to celebrate birthdays of the month. It was well attended.
</p> </p>
</Card>
</div> <p className="text-md text-[var(--purple-deep)] 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-[var(--purple-deep)] 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-[var(--purple-deep)] 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-[var(--purple-deep)] 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 &apos;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&apos;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&apos;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-[var(--purple-deep)] 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-[var(--purple-deep)] 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-[var(--purple-deep)] 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-[var(--purple-deep)] 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 Womens Studies Department at the University of Houston for their “Living archive” series. We have done several programs for the Womens 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-[var(--purple-deep)] 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-[var(--purple-deep)] 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-background 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-[var(--purple-deep)] 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-[var(--purple-deep)] mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Check out "A Life Remembered", a tribute dedicated to Arden Eversmeyer, one of the founding mothers of LOAF. Check out "A Life Remembered", a tribute dedicated to Arden Eversmeyer, one of the founding mothers of LOAF.
</p> </p>
<a href="https://www.oldlesbianhistory.org/arden-eversmeyer" target="_blank" rel="noopener noreferrer"> <a href="https://www.oldlesbianhistory.org/arden-eversmeyer" target="_blank" rel="noopener noreferrer">
<Button className="bg-[#664fa3] hover:bg-[#48286e] text-white rounded-full px-6 py-3"> <Button className="bg-[var(--purple-lavender)] hover:bg-[var(--purple-deep)] text-white rounded-full px-6 py-3">
View Arden's Tribute View Arden's Tribute
</Button> </Button>
</a> </a>
</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-background 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-[var(--purple-deep)] 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-[var(--purple-deep)] mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Arden Eversmeyer was also involved with The Old Lesbian Oral Herstory Project, preserving the stories of old lesbians. Arden Eversmeyer was also involved with The Old Lesbian Oral Herstory Project, preserving the stories of old lesbians.
</p> </p>
<a href="https://www.olohp.org" target="_blank" rel="noopener noreferrer"> <a href="https://www.olohp.org" target="_blank" rel="noopener noreferrer">
<Button className="bg-[#664fa3] hover:bg-[#48286e] text-white rounded-full px-6 py-3"> <Button className="bg-[var(--purple-lavender)] hover:bg-[var(--purple-deep)] text-white rounded-full px-6 py-3">
Learn More About OLOHP Learn More About OLOHP
</Button> </Button>
</a> </a>
@@ -258,7 +276,6 @@ const History = () => {
</div> </div>
</div> </div>
</section> </section>
</main>
<PublicFooter /> <PublicFooter />
</div> </div>

View File

@@ -7,26 +7,89 @@ 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}/web_elements_tagline.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 friendships = `${process.env.PUBLIC_URL}/friendships.png`;
const InfoCard = ({ iconSrc, infoTitle, description }) => (
<Card className="relative bg-background 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-[var(--purple-deep)] 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-[var(--purple-deep)] 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-[var(--purple-deep)] 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-[var(--purple-deep)] 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-background">
<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="relative bg-gradient-to-b from-[var(--purple-deep)] to-[var(--purple-lavender)] 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 w-full">
<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"> {/* Friendships background image */}
<div className="absolute inset-0 z-0 flex overflow-hidden top-[-32rem] lg:-top-32">
<img src={friendships} alt="Friendships" className="lg:max-w-screen opacity-15 max-w-full max-h-full object-contain" />
</div>
{/* Blur Overlay */}
<div className="absolute inset-0 z-[1] bg-background/5 backdrop-blur-xs"></div>
{/* Left column Loaf Image */}
<div className="relative z-10 lg:py-20 py-7 flex flex-col gap-6 sm:gap-8 items-center justify-center w-full lg:w-[530px] 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-[370px] 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="/become-a-member" 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-[var(--neutral-800)] hover:bg-background text-[var(--purple-ink)] 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>
@@ -35,74 +98,43 @@ const Landing = () => {
LOAF is supported by the Hollyfield Foundation LOAF is supported by the Hollyfield Foundation
</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"> {/* Right Column Loaf Tagline */}
<img src={taglineImage} alt="LOAF Tagline" className="w-full max-w-[483px] h-auto object-contain" /> <div className="relative z-10 py-8 md:py-12 lg:py-16 flex items-center justify-center w-full lg:max-w-[815px] h-auto">
<img src={taglineImage} alt="LOAF Tagline" className="relative z-10 w-full h-auto object-cover" />
</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-[var(--lavender-300)] 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-[var(--purple-deep)] 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-[var(--lavender-300)] to-[var(--neutral-800)] 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-[var(--purple-amethyst)] to-[var(--purple-deep)] 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-[var(--neutral-800)] hover:bg-background text-[var(--purple-ink)] 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>

View File

@@ -55,23 +55,23 @@ const Login = () => {
}; };
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-background">
<PublicNavbar /> <PublicNavbar />
<div className="max-w-md mx-auto px-6 py-12"> <div className="max-w-md mx-auto px-6 py-12">
<div className="mb-8"> <div className="mb-8">
<Link to="/" className="inline-flex items-center text-[#664fa3] hover:text-[#ff9e77] transition-colors"> <Link to="/" className="inline-flex items-center text-brand-purple hover:text-[var(--orange-light)] transition-colors">
<ArrowLeft className="h-4 w-4 mr-2" /> <ArrowLeft className="h-4 w-4 mr-2" />
Back to Home Back to Home
</Link> </Link>
</div> </div>
<Card className="p-8 md:p-12 bg-white rounded-2xl border border-[#ddd8eb] shadow-lg"> <Card className="p-8 md:p-12 bg-background rounded-2xl border border-[var(--neutral-800)] shadow-lg">
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<h1 className="text-4xl md:text-5xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Welcome Back Welcome Back
</h1> </h1>
<p className="text-lg text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Login to access your member dashboard. Login to access your member dashboard.
</p> </p>
</div> </div>
@@ -87,7 +87,7 @@ const Login = () => {
value={formData.email} value={formData.email}
onChange={handleInputChange} onChange={handleInputChange}
placeholder="your.email@example.com" placeholder="your.email@example.com"
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 focus:border-brand-purple "
data-testid="login-email-input " data-testid="login-email-input "
/> />
</div> </div>
@@ -95,7 +95,7 @@ const Login = () => {
<div> <div>
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<Label htmlFor="password">Password</Label> <Label htmlFor="password">Password</Label>
<Link to="/forgot-password" className="text-sm text-[#ff9e77] hover:underline"> <Link to="/forgot-password" className="text-sm text-[var(--orange-light)] hover:underline">
Forgot password? Forgot password?
</Link> </Link>
</div> </div>
@@ -106,7 +106,7 @@ const Login = () => {
value={formData.password} value={formData.password}
onChange={handleInputChange} onChange={handleInputChange}
placeholder="Enter your password" placeholder="Enter your password"
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
data-testid="login-password-input" data-testid="login-password-input"
/> />
</div> </div>
@@ -114,16 +114,16 @@ const Login = () => {
<Button <Button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="w-full bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform disabled:opacity-50" className="w-full py-6 text-lg font-medium shadow-lg hover:scale-105 disabled:opacity-50 btn-lavender"
data-testid="login-submit-button" data-testid="login-submit-button"
> >
{loading ? 'Logging in...' : 'Login'} {loading ? 'Logging in...' : 'Login'}
<ArrowRight className="ml-2 h-5 w-5" /> <ArrowRight className="ml-2 h-5 w-5" />
</Button> </Button>
<p className="text-center text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-center text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Don't have an account?{' '} Don't have an account?{' '}
<Link to="/register" className="text-[#ff9e77] hover:underline font-medium"> <Link to="/register" className="text-[var(--orange-light)] hover:underline font-medium">
Register here Register here
</Link> </Link>
</p> </p>

View File

@@ -2,41 +2,41 @@ import React from 'react';
import PublicNavbar from '../components/PublicNavbar'; import PublicNavbar from '../components/PublicNavbar';
import PublicFooter from '../components/PublicFooter'; import PublicFooter from '../components/PublicFooter';
import { Card } from '../components/ui/card'; import { Card } from '../components/ui/card';
import { useThemeConfig } from '../context/ThemeConfigContext';
const MissionValues = () => { const MissionValues = () => {
const loafLogo = `${process.env.PUBLIC_URL}/loaf-logo.png`; const { getLogoUrl } = useThemeConfig();
const loafLogo = getLogoUrl();
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-background">
<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-[var(--neutral-100:)] to-[var(--neutral-800)] px-4 sm:px-6 py-8 sm:py-12 md:py-20">
<div className="max-w-6xl mx-auto"> <div className="max-w-[1400px] mx-auto">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8"> <div className="flex md:flex-row flex-col gap-10 items-stretch">
{/* 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-[var(--purple-lavender)] to-[var(--purple-deep)] p-16 rounded-2xl shadow-lg flex flex-col items-center justify-between flex-1 w-full md:w-1/2 ">
<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 LOAFs 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-background p-16 rounded-2xl shadow-lg flex-1 w-full md:w-1/2 ">
<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-[var(--purple-deep)] 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-[var(--purple-deep)]"
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>

81
src/pages/NotFound.js Normal file
View File

@@ -0,0 +1,81 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Button } from '../components/ui/button';
import { Card } from '../components/ui/card';
import { Home, ArrowLeft, Search } from 'lucide-react';
const NotFound = () => {
const navigate = useNavigate();
return (
<div className="min-h-screen bg-gradient-to-br from-[var(--lavender-700)] to-white flex items-center justify-center p-4">
<Card className="w-full max-w-2xl p-12 bg-background rounded-2xl border border-[var(--neutral-800)] text-center">
{/* 404 Illustration */}
<div className="mb-8">
<div className="relative">
<h1
className="text-[180px] font-bold text-transparent bg-clip-text bg-gradient-to-br from-[var(--neutral-800)] to-[var(--lavender-700)] leading-none"
style={{ fontFamily: "'Inter', sans-serif" }}
>
404
</h1>
<div className="absolute inset-0 flex items-center justify-center">
<Search className="h-24 w-24 text-brand-purple opacity-30" />
</div>
</div>
</div>
{/* Message */}
<h2
className="text-3xl font-semibold text-[var(--purple-ink)] mb-4"
style={{ fontFamily: "'Inter', sans-serif" }}
>
Page Not Found
</h2>
<p
className="text-lg text-brand-purple mb-8 max-w-md mx-auto"
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
Oops! The page you're looking for doesn't exist. It might have been moved or deleted.
</p>
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button
onClick={() => navigate(-1)}
variant="outline"
className="rounded-xl border-2 border-brand-purple text-brand-purple hover:bg-[var(--lavender-700)] px-6 py-6"
>
<ArrowLeft className="h-5 w-5 mr-2" />
Go Back
</Button>
<Button
onClick={() => navigate('/')}
className="rounded-xl bg-gradient-to-r from-brand-purple to-[var(--purple-ink)] hover:from-[var(--purple-ink)] hover:to-brand-purple text-white px-6 py-6"
>
<Home className="h-5 w-5 mr-2" />
Back to Home
</Button>
</div>
{/* Help Text */}
<div className="mt-8 pt-8 border-t border-[var(--neutral-800)]">
<p
className="text-sm text-brand-purple "
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
Need help? Contact us at{' '}
<a
href="mailto:support@loaftx.org"
className="text-brand-purple hover:text-[var(--purple-ink)] font-semibold underline"
>
support@loaftx.org
</a>
</p>
</div>
</Card>
</div>
);
};
export default NotFound;

View File

@@ -9,7 +9,7 @@ const PaymentCancel = () => {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-background">
<Navbar /> <Navbar />
<div className="max-w-4xl mx-auto px-6 py-12"> <div className="max-w-4xl mx-auto px-6 py-12">
@@ -22,48 +22,48 @@ const PaymentCancel = () => {
</div> </div>
{/* Cancel Message */} {/* Cancel Message */}
<h1 className="text-4xl md:text-5xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Payment Cancelled Payment Cancelled
</h1> </h1>
<p className="text-lg text-[#664fa3] max-w-2xl mx-auto mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-lg text-brand-purple max-w-2xl mx-auto mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Your payment was cancelled. No charges have been made to your account. Your payment was cancelled. No charges have been made to your account.
</p> </p>
</div> </div>
{/* Info Card */} {/* Info Card */}
<Card className="p-8 bg-white rounded-2xl border border-[#ddd8eb] shadow-lg mb-8"> <Card className="p-8 bg-background rounded-2xl border border-[var(--neutral-800)] shadow-lg mb-8">
<h2 className="text-2xl font-semibold text-[#422268] mb-6 text-center" style={{ fontFamily: "'Inter', sans-serif" }}> <h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6 text-center" style={{ fontFamily: "'Inter', sans-serif" }}>
What Happened? What Happened?
</h2> </h2>
<div className="space-y-6 mb-8"> <div className="space-y-6 mb-8">
<p className="text-[#664fa3] text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
You cancelled the payment process or closed the checkout page. Your membership has not been activated yet. You cancelled the payment process or closed the checkout page. Your membership has not been activated yet.
</p> </p>
<div className="bg-[#DDD8EB]/20 p-6 rounded-xl"> <div className="bg-[var(--neutral-800)]/20 p-6 rounded-xl">
<h3 className="text-lg font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Ready to Complete Your Membership? Ready to Complete Your Membership?
</h3> </h3>
<ul className="space-y-3"> <ul className="space-y-3">
<li className="flex items-start gap-3"> <li className="flex items-start gap-3">
<CreditCard className="h-5 w-5 text-[#664fa3] flex-shrink-0 mt-0.5" /> <CreditCard className="h-5 w-5 text-brand-purple flex-shrink-0 mt-0.5" />
<span className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <span className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Return to the plans page to complete your subscription Return to the plans page to complete your subscription
</span> </span>
</li> </li>
<li className="flex items-start gap-3"> <li className="flex items-start gap-3">
<Mail className="h-5 w-5 text-[#664fa3] flex-shrink-0 mt-0.5" /> <Mail className="h-5 w-5 text-brand-purple flex-shrink-0 mt-0.5" />
<span className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <span className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Contact us if you experienced any issues during checkout Contact us if you experienced any issues during checkout
</span> </span>
</li> </li>
</ul> </ul>
</div> </div>
<div className="bg-[#f1eef9] p-6 rounded-xl"> <div className="bg-[var(--lavender-300)] p-6 rounded-xl">
<p className="text-sm text-[#664fa3] text-center mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-sm text-brand-purple text-center mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<span className="font-medium text-[#422268]">Note:</span>{' '} <span className="font-medium text-[var(--purple-ink)]">Note:</span>{' '}
Your membership application is still validated. You can complete payment whenever you're ready. Your membership application is still validated. You can complete payment whenever you're ready.
</p> </p>
</div> </div>
@@ -73,7 +73,7 @@ const PaymentCancel = () => {
<div className="flex flex-col sm:flex-row gap-4 justify-center"> <div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button <Button
onClick={() => navigate('/plans')} onClick={() => navigate('/plans')}
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-8 py-6 text-lg font-semibold" className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background rounded-full px-8 py-6 text-lg font-semibold"
data-testid="try-again-button" data-testid="try-again-button"
> >
<CreditCard className="mr-2 h-5 w-5" /> <CreditCard className="mr-2 h-5 w-5" />
@@ -82,7 +82,7 @@ const PaymentCancel = () => {
<Button <Button
onClick={() => navigate('/dashboard')} onClick={() => navigate('/dashboard')}
variant="outline" variant="outline"
className="border-2 border-[#664fa3] text-[#664fa3] hover:bg-[#664fa3] hover:text-white rounded-full px-8 py-6 text-lg font-semibold" className="border-2 border-brand-purple text-brand-purple hover:bg-brand-purple hover:text-white rounded-full px-8 py-6 text-lg font-semibold"
data-testid="back-to-dashboard-button" data-testid="back-to-dashboard-button"
> >
<ArrowLeft className="mr-2 h-5 w-5" /> <ArrowLeft className="mr-2 h-5 w-5" />
@@ -92,17 +92,17 @@ const PaymentCancel = () => {
</Card> </Card>
{/* Support Section */} {/* Support Section */}
<Card className="p-6 bg-gradient-to-br from-[#DDD8EB]/20 to-[#f1eef9]/20 rounded-2xl border border-[#ddd8eb]"> <Card className="p-6 bg-gradient-to-br from-[var(--neutral-800)]/20 to-[var(--lavender-300)]/20 rounded-2xl border border-[var(--neutral-800)]">
<h3 className="text-lg font-semibold text-[#422268] mb-3 text-center" style={{ fontFamily: "'Inter', sans-serif" }}> <h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3 text-center" style={{ fontFamily: "'Inter', sans-serif" }}>
Need Assistance? Need Assistance?
</h3> </h3>
<p className="text-[#664fa3] text-center mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple text-center mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
If you encountered any technical issues or have questions about the payment process, our support team is here to help. If you encountered any technical issues or have questions about the payment process, our support team is here to help.
</p> </p>
<div className="text-center"> <div className="text-center">
<a <a
href="mailto:support@loaf.org" href="mailto:support@loaf.org"
className="text-[#ff9e77] hover:text-[#664fa3] font-medium text-lg" className="text-[var(--orange-light)] hover:text-brand-purple font-medium text-lg"
> >
support@loaf.org support@loaf.org
</a> </a>

View File

@@ -20,60 +20,60 @@ const PaymentSuccess = () => {
}, [refreshUser]); }, [refreshUser]);
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-background">
<Navbar /> <Navbar />
<div className="max-w-4xl mx-auto px-6 py-12"> <div className="max-w-4xl mx-auto px-6 py-12">
<div className="text-center mb-12"> <div className="text-center mb-12">
{/* Success Icon */} {/* Success Icon */}
<div className="mb-8"> <div className="mb-8">
<div className="bg-[#81B29A] rounded-full w-24 h-24 mx-auto flex items-center justify-center"> <div className="bg-[var(--green-light)] rounded-full w-24 h-24 mx-auto flex items-center justify-center">
<CheckCircle className="h-12 w-12 text-white" /> <CheckCircle className="h-12 w-12 text-white" />
</div> </div>
</div> </div>
{/* Success Message */} {/* Success Message */}
<h1 className="text-4xl md:text-5xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Payment Successful! Payment Successful!
</h1> </h1>
<p className="text-lg text-[#664fa3] max-w-2xl mx-auto mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-lg text-brand-purple max-w-2xl mx-auto mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Thank you for your payment. Your LOAF membership is now active! Thank you for your payment. Your LOAF membership is now active!
</p> </p>
</div> </div>
{/* Confirmation Card */} {/* Confirmation Card */}
<Card className="p-8 bg-white rounded-2xl border border-[#ddd8eb] shadow-lg mb-8"> <Card className="p-8 bg-background rounded-2xl border border-[var(--neutral-800)] shadow-lg mb-8">
<h2 className="text-2xl font-semibold text-[#422268] mb-6 text-center" style={{ fontFamily: "'Inter', sans-serif" }}> <h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6 text-center" style={{ fontFamily: "'Inter', sans-serif" }}>
Welcome to the LOAF Community! Welcome to the LOAF Community!
</h2> </h2>
<div className="space-y-6 mb-8"> <div className="space-y-6 mb-8">
<div className="bg-[#f1eef9] p-6 rounded-xl"> <div className="bg-[var(--lavender-300)] p-6 rounded-xl">
<h3 className="text-lg font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
What's Next? What's Next?
</h3> </h3>
<ul className="space-y-3"> <ul className="space-y-3">
<li className="flex items-start gap-3"> <li className="flex items-start gap-3">
<CheckCircle className="h-5 w-5 text-[#81B29A] flex-shrink-0 mt-0.5" /> <CheckCircle className="h-5 w-5 text-[var(--green-light)] flex-shrink-0 mt-0.5" />
<span className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <span className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Your membership is now active and you have full access to all member benefits Your membership is now active and you have full access to all member benefits
</span> </span>
</li> </li>
<li className="flex items-start gap-3"> <li className="flex items-start gap-3">
<CheckCircle className="h-5 w-5 text-[#81B29A] flex-shrink-0 mt-0.5" /> <CheckCircle className="h-5 w-5 text-[var(--green-light)] flex-shrink-0 mt-0.5" />
<span className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <span className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
You can now RSVP and attend members-only events You can now RSVP and attend members-only events
</span> </span>
</li> </li>
<li className="flex items-start gap-3"> <li className="flex items-start gap-3">
<CheckCircle className="h-5 w-5 text-[#81B29A] flex-shrink-0 mt-0.5" /> <CheckCircle className="h-5 w-5 text-[var(--green-light)] flex-shrink-0 mt-0.5" />
<span className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <span className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Access the community directory and connect with other members Access the community directory and connect with other members
</span> </span>
</li> </li>
<li className="flex items-start gap-3"> <li className="flex items-start gap-3">
<CheckCircle className="h-5 w-5 text-[#81B29A] flex-shrink-0 mt-0.5" /> <CheckCircle className="h-5 w-5 text-[var(--green-light)] flex-shrink-0 mt-0.5" />
<span className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <span className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
You'll receive our newsletter with exclusive updates and announcements You'll receive our newsletter with exclusive updates and announcements
</span> </span>
</li> </li>
@@ -81,12 +81,12 @@ const PaymentSuccess = () => {
</div> </div>
{sessionId && ( {sessionId && (
<div className="bg-[#DDD8EB]/20 p-4 rounded-xl"> <div className="bg-[var(--neutral-800)]/20 p-4 rounded-xl">
<p className="text-sm text-[#664fa3] text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-sm text-brand-purple text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<span className="font-medium text-[#422268]">Transaction ID:</span>{' '} <span className="font-medium text-[var(--purple-ink)]">Transaction ID:</span>{' '}
<span className="font-mono text-xs">{sessionId}</span> <span className="font-mono text-xs">{sessionId}</span>
</p> </p>
<p className="text-xs text-[#664fa3] text-center mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-xs text-brand-purple text-center mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
A confirmation email has been sent to your registered email address. A confirmation email has been sent to your registered email address.
</p> </p>
</div> </div>
@@ -97,7 +97,7 @@ const PaymentSuccess = () => {
<div className="flex flex-col sm:flex-row gap-4 justify-center"> <div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button <Button
onClick={() => navigate('/dashboard')} onClick={() => navigate('/dashboard')}
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-8 py-6 text-lg font-semibold" className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background rounded-full px-8 py-6 text-lg font-semibold"
data-testid="go-to-dashboard-button" data-testid="go-to-dashboard-button"
> >
<User className="mr-2 h-5 w-5" /> <User className="mr-2 h-5 w-5" />
@@ -106,7 +106,7 @@ const PaymentSuccess = () => {
<Button <Button
onClick={() => navigate('/events')} onClick={() => navigate('/events')}
variant="outline" variant="outline"
className="border-2 border-[#664fa3] text-[#664fa3] hover:bg-[#664fa3] hover:text-white rounded-full px-8 py-6 text-lg font-semibold" className="border-2 border-brand-purple text-brand-purple hover:bg-brand-purple hover:text-white rounded-full px-8 py-6 text-lg font-semibold"
data-testid="browse-events-button" data-testid="browse-events-button"
> >
<Calendar className="mr-2 h-5 w-5" /> <Calendar className="mr-2 h-5 w-5" />
@@ -117,11 +117,11 @@ const PaymentSuccess = () => {
{/* Additional Info */} {/* Additional Info */}
<div className="text-center"> <div className="text-center">
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Need help? Contact us at{' '} Need help? Contact us at{' '}
<a <a
href="mailto:support@loaf.org" href="mailto:support@loaf.org"
className="text-[#ff9e77] hover:text-[#664fa3] font-medium" className="text-[var(--orange-light)] hover:text-brand-purple font-medium"
> >
support@loaf.org support@loaf.org
</a> </a>

View File

@@ -208,36 +208,36 @@ const Plans = () => {
const breakdown = getAmountBreakdown(); const breakdown = getAmountBreakdown();
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-background">
<Navbar /> <Navbar />
<div className="max-w-7xl mx-auto px-6 py-12"> <div className="max-w-7xl mx-auto px-6 py-12">
{/* Header */} {/* Header */}
<div className="mb-12 text-center"> <div className="mb-12 text-center">
<h1 className="text-4xl md:text-5xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Membership Plans Membership Plans
</h1> </h1>
<p className="text-lg text-[#664fa3] max-w-2xl mx-auto" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-lg text-brand-purple max-w-2xl mx-auto" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Choose the membership plan that works best for you and become part of our vibrant community. Choose the membership plan that works best for you and become part of our vibrant community.
</p> </p>
</div> </div>
{/* Status Banner */} {/* Status Banner */}
{statusInfo && statusInfo.title && ( {statusInfo && statusInfo.title && (
<Card className="max-w-3xl mx-auto mb-8 p-6 bg-gradient-to-r from-[#f1eef9] to-[#DDD8EB]/30 border-2 border-[#664fa3]"> <Card className="max-w-3xl mx-auto mb-8 p-6 bg-gradient-to-r from-[var(--lavender-300)] to-[var(--neutral-800)]/30 border-2 border-brand-purple ">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<AlertCircle className="h-6 w-6 text-[#664fa3] flex-shrink-0 mt-1" /> <AlertCircle className="h-6 w-6 text-brand-purple flex-shrink-0 mt-1" />
<div className="flex-1"> <div className="flex-1">
<h3 className="text-xl font-semibold text-[#422268] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}> <h3 className="text-xl font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
{statusInfo.title} {statusInfo.title}
</h3> </h3>
<p className="text-[#664fa3] mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{statusInfo.message} {statusInfo.message}
</p> </p>
{statusInfo.action && statusInfo.actionLink && ( {statusInfo.action && statusInfo.actionLink && (
<Button <Button
onClick={() => navigate(statusInfo.actionLink)} onClick={() => navigate(statusInfo.actionLink)}
className="bg-[#664fa3] text-white hover:bg-[#422268] rounded-full" className="bg-brand-purple text-white hover:bg-[var(--purple-ink)] rounded-full"
> >
{statusInfo.action} {statusInfo.action}
</Button> </Button>
@@ -249,11 +249,16 @@ const Plans = () => {
{loading ? ( {loading ? (
<div className="text-center py-20"> <div className="text-center py-20">
<Loader2 className="h-12 w-12 text-[#664fa3] mx-auto mb-4 animate-spin" /> <Loader2 className="h-12 w-12 text-brand-purple mx-auto mb-4 animate-spin" />
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading plans...</p> <p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading plans...</p>
</div> </div>
) : plans.length > 0 ? ( ) : plans.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8 max-w-5xl mx-auto"> <div className={`grid gap-6 sm:gap-8 mx-auto ${plans.length === 1
? 'grid-cols-1 max-w-md'
: plans.length === 2
? 'grid-cols-1 sm:grid-cols-2 max-w-3xl'
: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 max-w-5xl'
}`}>
{plans.map((plan) => { {plans.map((plan) => {
const minimumPrice = plan.minimum_price_cents || plan.price_cents || 3000; const minimumPrice = plan.minimum_price_cents || plan.price_cents || 3000;
const suggestedPrice = plan.suggested_price_cents || minimumPrice; const suggestedPrice = plan.suggested_price_cents || minimumPrice;
@@ -261,19 +266,19 @@ const Plans = () => {
return ( return (
<Card <Card
key={plan.id} key={plan.id}
className="p-8 bg-white rounded-2xl border-2 border-[#ddd8eb] hover:border-[#664fa3] hover:shadow-xl transition-all" className="p-8 bg-background rounded-2xl border-2 border-[var(--neutral-800)] hover:border-brand-purple hover:shadow-xl transition-all"
data-testid={`plan-card-${plan.id}`} data-testid={`plan-card-${plan.id}`}
> >
{/* Plan Header */} {/* Plan Header */}
<div className="text-center mb-6"> <div className="text-center mb-6">
<div className="bg-[#DDD8EB]/20 p-4 rounded-full w-16 h-16 mx-auto mb-4 flex items-center justify-center"> <div className="bg-[var(--neutral-800)]/20 p-4 rounded-full w-16 h-16 mx-auto mb-4 flex items-center justify-center">
<CreditCard className="h-8 w-8 text-[#664fa3]" /> <CreditCard className="h-8 w-8 text-brand-purple " />
</div> </div>
<h2 className="text-2xl font-semibold text-[#422268] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}> <h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
{plan.name} {plan.name}
</h2> </h2>
{plan.description && ( {plan.description && (
<p className="text-sm text-[#664fa3] mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-sm text-brand-purple mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{plan.description} {plan.description}
</p> </p>
)} )}
@@ -281,22 +286,22 @@ const Plans = () => {
{/* Pricing */} {/* Pricing */}
<div className="text-center mb-8"> <div className="text-center mb-8">
<div className="text-sm text-[#664fa3] mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <div className="text-sm text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Starting at Starting at
</div> </div>
<div className="text-2xl sm:text-3xl md:text-4xl font-bold text-[#422268] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}> <div className="text-2xl sm:text-3xl md:text-4xl font-bold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
{formatPrice(minimumPrice)} {formatPrice(minimumPrice)}
</div> </div>
{suggestedPrice > minimumPrice && ( {suggestedPrice > minimumPrice && (
<div className="text-sm text-[#664fa3] mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <div className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Suggested: {formatPrice(suggestedPrice)} Suggested: {formatPrice(suggestedPrice)}
</div> </div>
)} )}
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{getBillingCycleLabel(plan.billing_cycle)} {getBillingCycleLabel(plan.billing_cycle)}
</p> </p>
{plan.allow_donation && ( {plan.allow_donation && (
<div className="mt-2 flex items-center justify-center gap-1 text-xs text-[#ff9e77]"> <div className="mt-2 flex items-center justify-center gap-1 text-xs text-[var(--orange-light)]">
<Heart className="h-3 w-3" /> <Heart className="h-3 w-3" />
<span>Donations welcome</span> <span>Donations welcome</span>
</div> </div>
@@ -306,20 +311,20 @@ const Plans = () => {
{/* Features */} {/* Features */}
<div className="space-y-3 mb-8"> <div className="space-y-3 mb-8">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<CheckCircle className="h-5 w-5 text-[#81B29A] flex-shrink-0 mt-0.5" /> <CheckCircle className="h-5 w-5 text-[var(--green-light)] flex-shrink-0 mt-0.5" />
<span className="text-sm text-[#422268]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Access to all member events</span> <span className="text-sm text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Access to all member events</span>
</div> </div>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<CheckCircle className="h-5 w-5 text-[#81B29A] flex-shrink-0 mt-0.5" /> <CheckCircle className="h-5 w-5 text-[var(--green-light)] flex-shrink-0 mt-0.5" />
<span className="text-sm text-[#422268]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Community directory access</span> <span className="text-sm text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Community directory access</span>
</div> </div>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<CheckCircle className="h-5 w-5 text-[#81B29A] flex-shrink-0 mt-0.5" /> <CheckCircle className="h-5 w-5 text-[var(--green-light)] flex-shrink-0 mt-0.5" />
<span className="text-sm text-[#422268]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Exclusive member benefits</span> <span className="text-sm text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Exclusive member benefits</span>
</div> </div>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<CheckCircle className="h-5 w-5 text-[#81B29A] flex-shrink-0 mt-0.5" /> <CheckCircle className="h-5 w-5 text-[var(--green-light)] flex-shrink-0 mt-0.5" />
<span className="text-sm text-[#422268]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Newsletter subscription</span> <span className="text-sm text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Newsletter subscription</span>
</div> </div>
</div> </div>
@@ -327,7 +332,7 @@ const Plans = () => {
<Button <Button
onClick={() => handleSelectPlan(plan)} onClick={() => handleSelectPlan(plan)}
disabled={processingPlanId === plan.id || (statusInfo && !statusInfo.canSubscribe)} disabled={processingPlanId === plan.id || (statusInfo && !statusInfo.canSubscribe)}
className="w-full bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full py-6 text-lg font-semibold disabled:opacity-50 disabled:cursor-not-allowed" className="w-full bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background rounded-full py-6 text-lg font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
data-testid={`subscribe-button-${plan.id}`} data-testid={`subscribe-button-${plan.id}`}
> >
{processingPlanId === plan.id ? ( {processingPlanId === plan.id ? (
@@ -347,11 +352,11 @@ const Plans = () => {
</div> </div>
) : ( ) : (
<div className="text-center py-20"> <div className="text-center py-20">
<CreditCard className="h-20 w-20 text-[#ddd8eb] mx-auto mb-6" /> <CreditCard className="h-20 w-20 text-[var(--neutral-800)] mx-auto mb-6" />
<h3 className="text-2xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h3 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
No Plans Available No Plans Available
</h3> </h3>
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Membership plans are not currently available. Please check back later! Membership plans are not currently available. Please check back later!
</p> </p>
</div> </div>
@@ -359,17 +364,17 @@ const Plans = () => {
{/* Info Section */} {/* Info Section */}
<div className="mt-16 max-w-3xl mx-auto"> <div className="mt-16 max-w-3xl mx-auto">
<Card className="p-8 bg-gradient-to-br from-[#DDD8EB]/20 to-[#f1eef9]/20 rounded-2xl border border-[#ddd8eb]"> <Card className="p-8 bg-gradient-to-br from-[var(--neutral-800)]/20 to-[var(--lavender-300)]/20 rounded-2xl border border-[var(--neutral-800)]">
<h3 className="text-xl font-semibold text-[#422268] mb-4 text-center" style={{ fontFamily: "'Inter', sans-serif" }}> <h3 className="text-xl font-semibold text-[var(--purple-ink)] mb-4 text-center" style={{ fontFamily: "'Inter', sans-serif" }}>
Need Help Choosing? Need Help Choosing?
</h3> </h3>
<p className="text-[#664fa3] text-center mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple text-center mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
If you have any questions about our membership plans or need assistance, please contact us. If you have any questions about our membership plans or need assistance, please contact us.
</p> </p>
<div className="text-center"> <div className="text-center">
<a <a
href="mailto:support@loaf.org" href="mailto:support@loaf.org"
className="text-[#ff9e77] hover:text-[#664fa3] font-medium" className="text-[var(--orange-light)] hover:text-brand-purple font-medium"
> >
support@loaf.org support@loaf.org
</a> </a>
@@ -382,10 +387,10 @@ const Plans = () => {
<Dialog open={amountDialogOpen} onOpenChange={setAmountDialogOpen}> <Dialog open={amountDialogOpen} onOpenChange={setAmountDialogOpen}>
<DialogContent className="sm:max-w-[500px]"> <DialogContent className="sm:max-w-[500px]">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-2xl text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}> <DialogTitle className="text-2xl text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Choose Your Amount Choose Your Amount
</DialogTitle> </DialogTitle>
<DialogDescription className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <DialogDescription className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{selectedPlan?.name} - {getBillingCycleLabel(selectedPlan?.billing_cycle)} {selectedPlan?.name} - {getBillingCycleLabel(selectedPlan?.billing_cycle)}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -393,11 +398,11 @@ const Plans = () => {
<div className="space-y-6"> <div className="space-y-6">
{/* Amount Input */} {/* Amount Input */}
<div> <div>
<Label htmlFor="amount" className="text-[#422268]"> <Label htmlFor="amount" className="text-[var(--purple-ink)]">
Amount (USD) * Amount (USD) *
</Label> </Label>
<div className="relative mt-2"> <div className="relative mt-2">
<span className="absolute left-4 top-1/2 transform -translate-y-1/2 text-[#664fa3] text-lg font-semibold"> <span className="absolute left-4 top-1/2 transform -translate-y-1/2 text-brand-purple text-lg font-semibold">
$ $
</span> </span>
<Input <Input
@@ -407,25 +412,25 @@ const Plans = () => {
min={selectedPlan ? (selectedPlan.minimum_price_cents / 100).toFixed(2) : "30.00"} min={selectedPlan ? (selectedPlan.minimum_price_cents / 100).toFixed(2) : "30.00"}
value={amountInput} value={amountInput}
onChange={(e) => setAmountInput(e.target.value)} onChange={(e) => setAmountInput(e.target.value)}
className="pl-8 h-14 text-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="pl-8 h-14 text-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
placeholder="50.00" placeholder="50.00"
/> />
</div> </div>
<p className="text-sm text-[#664fa3] mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-sm text-brand-purple mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Minimum: {selectedPlan ? formatPrice(selectedPlan.minimum_price_cents || 3000) : '$30.00'} Minimum: {selectedPlan ? formatPrice(selectedPlan.minimum_price_cents || 3000) : '$30.00'}
</p> </p>
</div> </div>
{/* Breakdown Display */} {/* Breakdown Display */}
{breakdown && breakdown.total >= breakdown.base && ( {breakdown && breakdown.total >= breakdown.base && (
<Card className="p-4 bg-[#f9f5ff] border border-[#DDD8EB]"> <Card className="p-4 bg-[var(--lavender-400)] border border-[var(--neutral-800)]">
<div className="space-y-2 text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <div className="space-y-2 text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<div className="flex justify-between text-[#422268]"> <div className="flex justify-between text-[var(--purple-ink)]">
<span>Membership Fee:</span> <span>Membership Fee:</span>
<span className="font-semibold">{formatPrice(breakdown.base)}</span> <span className="font-semibold">{formatPrice(breakdown.base)}</span>
</div> </div>
{breakdown.donation > 0 && ( {breakdown.donation > 0 && (
<div className="flex justify-between text-[#ff9e77]"> <div className="flex justify-between text-[var(--orange-light)]">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Heart className="h-4 w-4" /> <Heart className="h-4 w-4" />
Additional Donation: Additional Donation:
@@ -433,7 +438,7 @@ const Plans = () => {
<span className="font-semibold">{formatPrice(breakdown.donation)}</span> <span className="font-semibold">{formatPrice(breakdown.donation)}</span>
</div> </div>
)} )}
<div className="flex justify-between text-[#422268] font-bold text-base pt-2 border-t border-[#DDD8EB]"> <div className="flex justify-between text-[var(--purple-ink)] font-bold text-base pt-2 border-t border-[var(--neutral-800)]">
<span>Total:</span> <span>Total:</span>
<span>{formatPrice(breakdown.total)}</span> <span>{formatPrice(breakdown.total)}</span>
</div> </div>
@@ -443,8 +448,8 @@ const Plans = () => {
{/* Donation Message */} {/* Donation Message */}
{selectedPlan?.allow_donation && ( {selectedPlan?.allow_donation && (
<div className="bg-[#DDD8EB]/20 rounded-lg p-4"> <div className="bg-[var(--neutral-800)]/20 rounded-lg p-4">
<p className="text-sm text-[#422268] text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-sm text-[var(--purple-ink)] text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<strong>Thank you for supporting our community!</strong><br /> <strong>Thank you for supporting our community!</strong><br />
Your donation helps us continue our mission and provide meaningful experiences for all members. Your donation helps us continue our mission and provide meaningful experiences for all members.
</p> </p>
@@ -464,7 +469,7 @@ const Plans = () => {
<Button <Button
type="button" type="button"
onClick={handleCheckout} onClick={handleCheckout}
className="flex-1 bg-[#DDD8EB] text-[#422268] hover:bg-white" className="flex-1 bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background"
> >
Continue to Checkout Continue to Checkout
</Button> </Button>

View File

@@ -7,242 +7,190 @@ export default function PrivacyPolicy() {
return ( return (
<> <>
<PublicNavbar /> <PublicNavbar />
<div className="min-h-screen bg-[#F8F7FB]"> <main className="bg-gradient-to-bl from-[var(--neutral-100:)] to-[var(--neutral-800)] text-[var(--purple-deep)]">
<div className="max-w-4xl mx-auto px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-12"> <div className="mx-auto w-full max-w-5xl px-4 sm:px-6 lg:px-8 py-10">
{/* Header */} <header className="border-b pb-6">
<div className="mb-8"> <h1 className="text-3xl sm:text-4xl font-bold tracking-tight" style={{ fontFamily: 'Poppins' }}>
<h1 className="text-3xl sm:text-4xl md:text-5xl font-bold text-[#422268] mb-4"
style={{ fontFamily: "'Inter', sans-serif" }}>
Privacy Policy
</h1>
<p className="text-base sm:text-lg text-gray-600" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
LOAFers, Inc. Website Privacy Policy LOAFers, Inc. Website Privacy Policy
</p> </h1>
</div> </header>
{/* Content */} <div className="prose text-[var(--purple-deep)] max-w-none prose-h2:mt-10 prose-h2:scroll-mt-24 prose-h3:mt-6">
<div className="bg-white rounded-lg border border-[#ddd8eb] p-6 sm:p-8 md:p-10 space-y-8"> <section className="mt-8">
<p>
{/* Introduction */} This Privacy Policy ("Policy") applies to Membership Applications, and LOAFers, Inc. ("Company") and
<section> governs data collection and usage. The Company&apos;s application is a Membership request, Membership online
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> profile, and Consent to receive eNewsletters. By using the Company application, you consent to the data
<p className="text-gray-700 leading-relaxed"> practices described in the statement.
This Privacy Policy ("Policy") applies to Membership Applications, and LOAFers, Inc. ("Company") and governs data collection and usage. The Company's application is a Membership request, Membership online profile, and Consent to receive eNewsletters. By using the Company application, you consent to the data practices described in the statement.
</p> </p>
<p className="text-gray-700 leading-relaxed"> <p>
We reserve the right to change this policy at any given time, of which you will be promptly updated. If you want to make sure that you are up to date with the latest changes, we advise you to frequently visit this page. We reserve the right to change this policy at any given time, of which you will be promptly updated. If
you want to make sure that you are up to date with the latest changes, we advise you to frequently visit
this page.
</p> </p>
</div>
</section> </section>
{/* Section 1: What User Data We Collect */} <section id="user-data" className="scroll-mt-24">
<section> <h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">What User Data We Collect</h2>
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4 flex items-center gap-2" <p>When you visit the Site, we may collect the following data:</p>
style={{ fontFamily: "'Inter', sans-serif" }}> <ul className="list-disc pl-6 space-y-1">
<span>💻</span> What User Data We Collect <li>Your IP address.</li>
</h2> <li>Your contact information and email address.</li>
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<p className="text-gray-700 leading-relaxed">
When you visit the Site, we may collect the following data:
</p>
<ul className="list-disc list-inside space-y-2 text-gray-700 ml-4">
<li>Your <strong>IP address</strong></li>
<li>Your <strong>contact information and email address</strong></li>
</ul> </ul>
<p className="text-gray-700 leading-relaxed mt-4"> <p>When you apply for membership, we collect the following data:</p>
When you apply for membership, we collect the following data: <ul className="list-disc pl-6 space-y-1">
</p> <li>First and last name</li>
<ul className="list-disc list-inside space-y-2 text-gray-700 ml-4"> <li>Mailing address</li>
<li><strong>First and last name</strong></li> <li>Email</li>
<li><strong>Mailing address</strong></li> <li>Phone number</li>
<li><strong>Email</strong></li> <li>Birthday</li>
<li><strong>Phone number</strong></li>
<li><strong>Birthday</strong></li>
</ul> </ul>
<p className="text-gray-700 leading-relaxed mt-4"> <p>If you choose to pay your membership administrative fee online, we have access to:</p>
If you choose to pay your membership administrative fee online, we have access to: <ul className="list-disc pl-6 space-y-1">
</p> <li>Partial credit card information</li>
<ul className="list-disc list-inside space-y-2 text-gray-700 ml-4">
<li><strong>Partial credit card information</strong></li>
</ul> </ul>
<p className="text-gray-700 leading-relaxed mt-4"> <p>You may also choose to provide the following:</p>
You may also choose to provide the following: <ul className="list-disc pl-6 space-y-1">
</p> <li>Partners name</li>
<ul className="list-disc list-inside space-y-2 text-gray-700 ml-4"> <li>Photo</li>
<li><strong>Partner's name</strong></li> <li>Self-bio</li>
<li><strong>Photo</strong></li> <li>Consent to receive our eNewsletter</li>
<li><strong>Self-bio</strong></li> <li>Consent to display an online profile visible only to membership</li>
<li><strong>Consent to receive our eNewsletter</strong></li>
<li><strong>Consent to display an online profile visible only to membership</strong></li>
</ul> </ul>
</div>
</section> </section>
{/* Section 2: Why We Collect Your Data */} <section id="why-collect" className="scroll-mt-24">
<section> <h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">Why We Collect Your Data</h2>
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4 flex items-center gap-2" <ul className="list-disc pl-6 space-y-1">
style={{ fontFamily: "'Inter', sans-serif" }}> <li>
<span>🎯</span> Why We Collect Your Data To send you announcement emails containing the information about our events and information we think you
</h2> will find interesting.
<div className="prose max-w-none" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> </li>
<ul className="list-disc list-inside space-y-2 text-gray-700 ml-4"> <li>To contact you to fill out surveys about our membership</li>
<li>To send you announcement emails containing the information about our events and information we think you will find interesting.</li>
<li>To contact you to fill out surveys about our membership.</li>
<li>To customize our blog according to your online behavior and personal preferences.</li> <li>To customize our blog according to your online behavior and personal preferences.</li>
</ul> </ul>
</div>
</section> </section>
{/* Section 3: Sharing Information with Third Parties */} <section id="third-parties" className="scroll-mt-24">
<section> <h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">Sharing Information with Third Parties</h2>
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4 flex items-center gap-2" <p>The Company does not sell, rent, or lease personal data to third parties.</p>
style={{ fontFamily: "'Inter', sans-serif" }}> <p>
<span>🤝</span> Sharing Information with Third Parties The Company may share data with trusted partners to help perform statistical analysis, provide customer
</h2> support.
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<p className="text-gray-700 leading-relaxed">
The Company does not sell, rent, or lease personal data to third parties.
</p> </p>
<p className="text-gray-700 leading-relaxed"> <p>
The Company may share data with trusted partners to help perform statistical analysis, provide customer support. The Company uses Stripe to process online payments at which time users would no longer be governed by the
Company&apos;s Privacy Policy.
</p> </p>
<p className="text-gray-700 leading-relaxed"> <p>The Company may disclose your personal information, without notice, if required to do so by law.</p>
The Company uses <strong>Stripe</strong> to process online payments at which time users would no longer be governed by the Company's Privacy Policy.
</p>
<p className="text-gray-700 leading-relaxed">
The Company may disclose your personal information, without notice, if required to do so by law.
</p>
</div>
</section> </section>
{/* Section 4: Safeguarding and Securing the Data */} <section id="safeguarding" className="scroll-mt-24">
<section> <h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">Safeguarding and Securing the Data</h2>
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4 flex items-center gap-2" <p>
style={{ fontFamily: "'Inter', sans-serif" }}> LOAFers, Inc. is committed to securing your data and keeping it confidential. LOAFers, Inc. has done all
<span>🔒</span> Safeguarding and Securing the Data in its power to prevent data theft, unauthorized access, and disclosure by implementing the latest
</h2> technologies and software, which help us safeguard all the information we collect online.
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<p className="text-gray-700 leading-relaxed">
LOAFers, Inc. is committed to securing your data and keeping it confidential. LOAFers, Inc. has done all in its power to prevent data theft, unauthorized access, and disclosure by implementing the latest technologies and software, which help us safeguard all the information we collect online.
</p> </p>
</div>
</section> </section>
{/* Section 5: Our Cookie Policy */} <section id="cookies" className="scroll-mt-24">
<section> <h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">Our Cookie Policy</h2>
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4 flex items-center gap-2" <p>
style={{ fontFamily: "'Inter', sans-serif" }}> Once you agree to allow our blog to use cookies, you also agree to use the data it collects regarding your
<span>🍪</span> Our Cookie Policy online behavior (analyze web traffic, web pages you visit and spend the most time on).
</h2>
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<p className="text-gray-700 leading-relaxed">
Once you agree to allow our blog to use cookies, you also agree to use the data it collects regarding your online behavior (analyze web traffic, web pages you visit and spend the most time on).
</p> </p>
<p className="text-gray-700 leading-relaxed"> <p>
The data we collect by using cookies is used to customize our blog to your needs. After we use the data for statistical analysis, the data is completely removed from our systems. The data we collect by using cookies is used to customize our blog to your needs. After we use the data
for statistical analysis, the data is completely removed from our systems.
</p> </p>
<p className="text-gray-700 leading-relaxed"> <p>
Please note that cookies don't allow us to gain control of your computer in any way. They are strictly used to monitor which pages you find useful and which you do not so that we can provide a better experience for you. Please note that cookies don&apos;t allow us to gain control of your computer in any way. They are strictly
used to monitor which pages you find useful and which you do not so that we can provide a better
experience for you.
</p> </p>
<p className="text-gray-700 leading-relaxed"> <p>
If you want to disable cookies, you can do it by accessing the settings of your internet browser. You can visit{' '} If you want to disable cookies, you can do it by accessing the settings of your internet browser. You can
<a href="https://www.internetcookies.com" target="_blank" rel="noopener noreferrer" className="text-[#664fa3] hover:text-[#422268] font-semibold"> visit{" "}
<a className="" href="https://www.internetcookies.com">
https://www.internetcookies.com https://www.internetcookies.com
</a>, which contains comprehensive information on how to do this on a wide variety of browsers and devices. </a>
, which contains comprehensive information on how to do this on a wide variety of browsers and devices.
</p> </p>
</div>
</section> </section>
{/* Section 6: Links to Other Websites */} <section id="other-sites" className="scroll-mt-24">
<section> <h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">Links to Other Websites</h2>
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4 flex items-center gap-2" <p>
style={{ fontFamily: "'Inter', sans-serif" }}> Our blog contains links that lead to other websites. If you click on these links LOAFers, Inc. is not held
<span>🔗</span> Links to Other Websites responsible for your data and privacy protection. Visiting those websites is not governed by this privacy
</h2> policy agreement. Make sure to read the privacy policy documentation of the website you go to from our
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> website.
<p className="text-gray-700 leading-relaxed">
Our blog contains links that lead to other websites. If you click on these links LOAFers, Inc. is not held responsible for your data and privacy protection. Visiting those websites is not governed by this privacy policy agreement. Make sure to read the privacy policy documentation of the website you go to from our website.
</p> </p>
</div>
</section> </section>
{/* Section 7: Restricting the Collection of your Personal Data */} <section id="restricting" className="scroll-mt-24">
<section> <h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4 flex items-center gap-2" Restricting the Collection of your Personal Data
style={{ fontFamily: "'Inter', sans-serif" }}>
<span>🚫</span> Restricting the Collection of your Personal Data
</h2> </h2>
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p>
<p className="text-gray-700 leading-relaxed"> At some point, you might wish to restrict the use and collection of your personal data. You can achieve
At some point, you might wish to restrict the use and collection of your personal data. You can achieve this by doing the following: this by doing the following:
</p> </p>
<ul className="list-disc list-inside space-y-2 text-gray-700 ml-4"> <ul className="list-disc pl-6 space-y-1">
<li>Log in to your online profile and make any changes you wish to your profile information.</li> <li>Log in to your online profile and make any changes you wish to your profile information.</li>
<li>If you have already agreed to share your information with us, feel free to contact us via email and we will be more than happy to change this for you.</li> <li>
If you have already agreed to share your information with us, feel free to contact us via email and we
will be more than happy to change this for you.
</li>
</ul> </ul>
</div>
</section> </section>
{/* Section 8: Children Under Thirteen */} <section id="children" className="scroll-mt-24">
<section> <h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">Children Under Thirteen</h2>
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4 flex items-center gap-2" <p>The Company does not knowingly collect information from children under the age of 13.</p>
style={{ fontFamily: "'Inter', sans-serif" }}>
<span>👶</span> Children Under Thirteen
</h2>
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<p className="text-gray-700 leading-relaxed">
The Company does not knowingly collect information from children under the age of 13.
</p>
</div>
</section> </section>
{/* Section 9: Changes to this Statement */} <section id="changes" className="scroll-mt-24">
<section> <h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">Changes to this Statement</h2>
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4 flex items-center gap-2" <p>
style={{ fontFamily: "'Inter', sans-serif" }}> The Company may make changes to this Policy. When this occurs the effective date of this policy will be
<span>🗓️</span> Changes to this Statement updated.
</h2>
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<p className="text-gray-700 leading-relaxed">
The Company may make changes to this Policy. When this occurs the effective date of this policy will be updated.
</p> </p>
</div>
</section> </section>
{/* Section 10: Contact Information */} <section id="contact" className="scroll-mt-24">
<section> <h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">Contact Information</h2>
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4 flex items-center gap-2" <p>If you have any question, please contact LOAFers, Inc. at:</p>
style={{ fontFamily: "'Inter', sans-serif" }}> <div className="not-prose mt-4">
<span>📧</span> Contact Information <p className="font-semibold mb-2">LOAFers, Inc.</p>
</h2> <p className="">PO BOX 7207</p>
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="">Houston, TX 77248-7207</p>
<p className="text-gray-700 leading-relaxed"> <p className="mt-3">Or</p>
If you have any questions, please contact LOAFers, Inc. at: <p className="mt-3">
<a className="" href="mailto:info@loaftx.org">
info@loaftx.org
</a>
</p> </p>
<div className="bg-[#F8F7FB] p-4 rounded-lg border border-[#ddd8eb]">
<p className="font-semibold text-gray-800">LOAFers, Inc.</p>
<p className="text-gray-700">PO Box 7207</p>
<p className="text-gray-700">Houston, TX 77248-7207</p>
<p className="text-gray-700 mt-2">
Email: <a href="mailto:info@loaftx.org" className="text-[#664fa3] hover:text-[#422268] font-semibold">info@loaftx.org</a>
</p>
</div>
</div> </div>
</section> </section>
</div> </div>
</div>
{/* Back to Home Link */} {/* Back to Home Link */}
<div className="mt-8 text-center"> <div className="mt-8 text-center">
<Link to="/" className="text-[#664fa3] hover:text-[#422268] font-semibold transition-colors inline-flex items-center gap-2" <Link
style={{ fontFamily: "'Nunito Sans', sans-serif" }}> to="/"
className="text-[var(--purple-lavender)] hover:text-[var(--purple-ink)] font-semibold transition-colors inline-flex items-center gap-2"
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
>
<span></span> Back to Home <span></span> Back to Home
</Link> </Link>
</div> </div>
</div> </main>
</div>
<PublicFooter /> <PublicFooter />
</> </>
); );

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useRef } 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';
@@ -8,17 +8,27 @@ import { Label } from '../components/ui/label';
import { Textarea } from '../components/ui/textarea'; 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 { User, Lock, Heart, Users, Mail, BookUser, Camera, Upload, Trash2, Eye, CreditCard, Handshake, ArrowLeft } 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';
import { useNavigate } from 'react-router-dom';
const Profile = () => { const Profile = () => {
const { user } = useAuth(); const { user } = useAuth();
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);
const [maxFileSizeBytes, setMaxFileSizeBytes] = useState(52428800);
const [activeTab, setActiveTab] = useState('account');
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [initialFormData, setInitialFormData] = useState(null);
const navigate = useNavigate();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
// Personal Information
first_name: '', first_name: '',
last_name: '', last_name: '',
phone: '', phone: '',
@@ -26,19 +36,15 @@ const Profile = () => {
city: '', city: '',
state: '', state: '',
zipcode: '', zipcode: '',
// Partner Information
partner_first_name: '', partner_first_name: '',
partner_last_name: '', partner_last_name: '',
partner_is_member: false, partner_is_member: false,
partner_plan_to_become_member: false, partner_plan_to_become_member: false,
// Newsletter Preferences
newsletter_publish_name: false, newsletter_publish_name: false,
newsletter_publish_photo: false, newsletter_publish_photo: false,
newsletter_publish_birthday: false, newsletter_publish_birthday: false,
newsletter_publish_none: false, newsletter_publish_none: false,
// Volunteer Interests (array)
volunteer_interests: [], volunteer_interests: [],
// Member Directory Settings
show_in_directory: false, show_in_directory: false,
directory_email: '', directory_email: '',
directory_bio: '', directory_bio: '',
@@ -49,15 +55,35 @@ const Profile = () => {
}); });
useEffect(() => { useEffect(() => {
fetchConfig();
fetchProfile(); fetchProfile();
}, []); }, []);
// Track unsaved changes
useEffect(() => {
if (initialFormData) {
const hasChanges = JSON.stringify(formData) !== JSON.stringify(initialFormData);
setHasUnsavedChanges(hasChanges);
}
}, [formData, initialFormData]);
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 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);
setFormData({ setProfilePhotoUrl(response.data.profile_photo_url);
// Personal Information setPreviewImage(response.data.profile_photo_url);
const newFormData = {
first_name: response.data.first_name || '', first_name: response.data.first_name || '',
last_name: response.data.last_name || '', last_name: response.data.last_name || '',
phone: response.data.phone || '', phone: response.data.phone || '',
@@ -65,19 +91,15 @@ const Profile = () => {
city: response.data.city || '', city: response.data.city || '',
state: response.data.state || '', state: response.data.state || '',
zipcode: response.data.zipcode || '', zipcode: response.data.zipcode || '',
// Partner Information
partner_first_name: response.data.partner_first_name || '', partner_first_name: response.data.partner_first_name || '',
partner_last_name: response.data.partner_last_name || '', partner_last_name: response.data.partner_last_name || '',
partner_is_member: response.data.partner_is_member || false, partner_is_member: response.data.partner_is_member || false,
partner_plan_to_become_member: response.data.partner_plan_to_become_member || false, partner_plan_to_become_member: response.data.partner_plan_to_become_member || false,
// Newsletter Preferences
newsletter_publish_name: response.data.newsletter_publish_name || false, newsletter_publish_name: response.data.newsletter_publish_name || false,
newsletter_publish_photo: response.data.newsletter_publish_photo || false, newsletter_publish_photo: response.data.newsletter_publish_photo || false,
newsletter_publish_birthday: response.data.newsletter_publish_birthday || false, newsletter_publish_birthday: response.data.newsletter_publish_birthday || false,
newsletter_publish_none: response.data.newsletter_publish_none || false, newsletter_publish_none: response.data.newsletter_publish_none || false,
// Volunteer Interests
volunteer_interests: response.data.volunteer_interests || [], volunteer_interests: response.data.volunteer_interests || [],
// Member Directory Settings
show_in_directory: response.data.show_in_directory || false, show_in_directory: response.data.show_in_directory || false,
directory_email: response.data.directory_email || '', directory_email: response.data.directory_email || '',
directory_bio: response.data.directory_bio || '', directory_bio: response.data.directory_bio || '',
@@ -85,7 +107,9 @@ const Profile = () => {
directory_phone: response.data.directory_phone || '', directory_phone: response.data.directory_phone || '',
directory_dob: response.data.directory_dob || '', directory_dob: response.data.directory_dob || '',
directory_partner_name: response.data.directory_partner_name || '' directory_partner_name: response.data.directory_partner_name || ''
}); };
setFormData(newFormData);
setInitialFormData(newFormData);
} catch (error) { } catch (error) {
toast.error('Failed to load profile'); toast.error('Failed to load profile');
} }
@@ -110,7 +134,6 @@ const Profile = () => {
})); }));
}; };
// Volunteer interest options
const volunteerOptions = [ const volunteerOptions = [
'Event Planning', 'Event Planning',
'Social Media', 'Social Media',
@@ -124,6 +147,55 @@ const Profile = () => {
'Hospitality' 'Hospitality'
]; ];
const handlePhotoUpload = async (e) => {
const file = e.target.files[0];
if (!file) return;
if (!file.type.startsWith('image/')) {
toast.error('Please select an image file');
return;
}
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);
@@ -131,6 +203,8 @@ const Profile = () => {
try { try {
await api.put('/users/profile', formData); await api.put('/users/profile', formData);
toast.success('Profile updated successfully!'); toast.success('Profile updated successfully!');
setInitialFormData(formData);
setHasUnsavedChanges(false);
fetchProfile(); fetchProfile();
} catch (error) { } catch (error) {
toast.error('Failed to update profile'); toast.error('Failed to update profile');
@@ -139,79 +213,144 @@ const Profile = () => {
} }
}; };
const tabs = [
{ id: 'account', label: 'Account & Privacy', shortLabel: 'Account', icon: Lock },
{ id: 'bio', label: 'My Bio & Directory', shortLabel: 'Bio & Directory', icon: User },
{ id: 'engagement', label: 'Engagement', shortLabel: 'Engagement', icon: Handshake }
];
if (!profileData) { if (!profileData) {
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-white dark:bg-[var(--purple-deep)]">
<Navbar /> <Navbar />
<div className="flex items-center justify-center min-h-[60vh]"> <div className="flex items-center justify-center min-h-[60vh]">
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading profile...</p> <p className="text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading profile...</p>
</div> </div>
</div> </div>
); );
} }
return ( // Account & Privacy Tab Content
<div className="min-h-screen bg-white"> const AccountPrivacyContent = () => (
<Navbar /> <div className="space-y-6 ">
<div className="max-w-4xl mx-auto px-6 py-12"> <Card className="space-y-6 px-6 pb-6">
<div className="mb-8"> <div className="bg-brand-purple text-white px-4 py-3 rounded-t-xl -mx-6 -mt-6 mb-6">
<h1 className="text-4xl md:text-5xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h3 className="font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>Account & Privacy</h3>
My Profile </div>
</h1> <div>
<p className="text-lg text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-sm text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Login Email</p>
Update your personal information below. <p className="text-[var(--purple-ink)] dark:text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{profileData.email}</p>
</p>
</div> </div>
<Card className="p-8 bg-white rounded-2xl border border-[#ddd8eb] shadow-lg"> <div className="flex items-center justify-between">
{/* Read-only Information */}
<div className="mb-8 pb-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" }}>
<User className="h-6 w-6 text-[#664fa3]" />
Account Information
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
<div> <div>
<p className="text-sm text-[#664fa3] mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Email</p> <p className="text-sm text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Password</p>
<p className="text-[#422268] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{profileData.email}</p> <p className="text-[var(--purple-ink)] dark:text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}></p>
</div> </div>
<div>
<p className="text-sm text-[#664fa3] mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Status</p>
<p className="text-[#422268] font-medium capitalize" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{profileData.status.replace('_', ' ')}</p>
</div>
<div>
<p className="text-sm text-[#664fa3] mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Role</p>
<p className="text-[#422268] font-medium capitalize" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{profileData.role}</p>
</div>
<div>
<p className="text-sm text-[#664fa3] mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Date of Birth</p>
<p className="text-[#422268] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{new Date(profileData.date_of_birth).toLocaleDateString()}
</p>
</div>
</div>
<div className="mt-6">
<Button <Button
type="button" type="button"
onClick={() => setPasswordDialogOpen(true)} onClick={() => setPasswordDialogOpen(true)}
variant="outline" variant="outline"
className="border-2 border-[#664fa3] text-[#664fa3] hover:bg-[#f1eef9] rounded-full px-6 py-3" className="border-2 border-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-[var(--lavender-300)] rounded-lg px-4 py-2"
> >
<Lock className="h-4 w-4 mr-2" /> Change
Change Password
</Button> </Button>
</div> </div>
<div>
<p className="text-sm text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Membership Status</p>
<p className="text-[var(--purple-ink)] dark:text-[var(--purple-ink)] font-medium capitalize" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{profileData.status?.replace('_', ' ') || 'Active'}
</p>
</div> </div>
{/* Editable Form */} <div className="flex items-center justify-between">
<form onSubmit={handleSubmit} className="space-y-6" data-testid="profile-form"> <div>
<h2 className="text-2xl font-semibold text-[#422268] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}> <p className="text-sm text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Payment Method</p>
Personal Information <div className="flex items-center gap-2">
</h2> <CreditCard className="h-6 w-6 text-brand-purple" />
</div>
</div>
<Button
type="button"
variant="outline"
className="border-2 border-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-[var(--lavender-300)] rounded-lg px-4 py-2"
>
Manage
</Button>
</div>
</Card>
</div>
);
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6"> // My Bio & Directory Tab Content
const BioDirectoryContent = () => (
<Card className="space-y-6 px-6 pb-6">
<div className="bg-brand-purple text-white px-4 py-3 rounded-t-lg -mx-6 -mt-6 mb-6">
<h3 className="font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>My Bio & Directory</h3>
</div>
{/* Profile Photo Section */}
<div className="pb-6 border-b border-[var(--neutral-800)]">
<h4 className="text-lg font-semibold text-[var(--purple-ink)] mb-4 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<Camera className="h-5 w-5 text-brand-purple" />
Profile Photo
</h4>
<div className="flex flex-col md:flex-row items-center gap-6">
<Avatar className="h-24 w-24 border-4 border-[var(--neutral-800)]">
<AvatarImage src={previewImage} alt="Profile" />
<AvatarFallback className="bg-[var(--lavender-300)] text-brand-purple text-2xl">
{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-brand-purple text-white hover:bg-[var(--purple-ink)] rounded-full px-4 py-2"
>
<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-4 py-2"
>
<Trash2 className="h-4 w-4 mr-2" />
Delete Photo
</Button>
)}
<p className="text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Max {maxFileSizeMB}MB
</p>
</div>
</div>
</div>
{/* Personal Information */}
<div className="space-y-4">
<h4 className="text-lg font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
Personal Information
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<Label htmlFor="first_name">First Name</Label> <Label htmlFor="first_name">First Name</Label>
<Input <Input
@@ -219,7 +358,7 @@ const Profile = () => {
name="first_name" name="first_name"
value={formData.first_name} value={formData.first_name}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
data-testid="first-name-input" data-testid="first-name-input"
/> />
</div> </div>
@@ -230,7 +369,7 @@ const Profile = () => {
name="last_name" name="last_name"
value={formData.last_name} value={formData.last_name}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
data-testid="last-name-input" data-testid="last-name-input"
/> />
</div> </div>
@@ -244,7 +383,7 @@ const Profile = () => {
type="tel" type="tel"
value={formData.phone} value={formData.phone}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
data-testid="phone-input" data-testid="phone-input"
/> />
</div> </div>
@@ -256,12 +395,12 @@ const Profile = () => {
name="address" name="address"
value={formData.address} value={formData.address}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
data-testid="address-input" data-testid="address-input"
/> />
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 sm:gap-6"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div> <div>
<Label htmlFor="city">City</Label> <Label htmlFor="city">City</Label>
<Input <Input
@@ -269,7 +408,7 @@ const Profile = () => {
name="city" name="city"
value={formData.city} value={formData.city}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
data-testid="city-input" data-testid="city-input"
/> />
</div> </div>
@@ -280,7 +419,7 @@ const Profile = () => {
name="state" name="state"
value={formData.state} value={formData.state}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
data-testid="state-input" data-testid="state-input"
/> />
</div> </div>
@@ -291,196 +430,39 @@ const Profile = () => {
name="zipcode" name="zipcode"
value={formData.zipcode} value={formData.zipcode}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
data-testid="zipcode-input" data-testid="zipcode-input"
/> />
</div> </div>
</div> </div>
{/* Section 2: Partner Information */}
<div className="pt-8 mt-8 border-t border-[#ddd8eb]">
<h2 className="text-2xl font-semibold text-[#422268] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<Heart className="h-6 w-6 text-[#ff9e77]" />
Partner Information
</h2>
<div className="space-y-6">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
<div>
<Label htmlFor="partner_first_name">Partner First Name</Label>
<Input
id="partner_first_name"
name="partner_first_name"
value={formData.partner_first_name}
onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
placeholder="Optional"
/>
</div>
<div>
<Label htmlFor="partner_last_name">Partner Last Name</Label>
<Input
id="partner_last_name"
name="partner_last_name"
value={formData.partner_last_name}
onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]"
placeholder="Optional"
/>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center gap-3">
<input
type="checkbox"
id="partner_is_member"
name="partner_is_member"
checked={formData.partner_is_member}
onChange={handleCheckboxChange}
className="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]"
/>
<Label htmlFor="partner_is_member" className="cursor-pointer text-[#422268]">
My partner is a current member
</Label>
</div>
<div className="flex items-center gap-3">
<input
type="checkbox"
id="partner_plan_to_become_member"
name="partner_plan_to_become_member"
checked={formData.partner_plan_to_become_member}
onChange={handleCheckboxChange}
className="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]"
/>
<Label htmlFor="partner_plan_to_become_member" className="cursor-pointer text-[#422268]">
My partner plans to become a member
</Label>
</div>
</div>
</div>
</div> </div>
{/* Section 3: Newsletter Preferences */} {/* Member Directory Settings */}
<div className="pt-8 mt-8 border-t border-[#ddd8eb]"> <div className="pt-6 border-t border-[var(--neutral-800)] space-y-4">
<h2 className="text-2xl font-semibold text-[#422268] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}> <h4 className="text-lg font-semibold text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<Mail className="h-6 w-6 text-[#81B29A]" /> <BookUser className="h-5 w-5 text-[var(--orange-light)]" />
Newsletter Preferences
</h2>
<p className="text-[#664fa3] mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Choose what information you'd like published in our member newsletter.
</p>
<div className="space-y-3">
<div className="flex items-center gap-3">
<input
type="checkbox"
id="newsletter_publish_name"
name="newsletter_publish_name"
checked={formData.newsletter_publish_name}
onChange={handleCheckboxChange}
className="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]"
/>
<Label htmlFor="newsletter_publish_name" className="cursor-pointer text-[#422268]">
Publish my name
</Label>
</div>
<div className="flex items-center gap-3">
<input
type="checkbox"
id="newsletter_publish_photo"
name="newsletter_publish_photo"
checked={formData.newsletter_publish_photo}
onChange={handleCheckboxChange}
className="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]"
/>
<Label htmlFor="newsletter_publish_photo" className="cursor-pointer text-[#422268]">
Publish my photo
</Label>
</div>
<div className="flex items-center gap-3">
<input
type="checkbox"
id="newsletter_publish_birthday"
name="newsletter_publish_birthday"
checked={formData.newsletter_publish_birthday}
onChange={handleCheckboxChange}
className="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]"
/>
<Label htmlFor="newsletter_publish_birthday" className="cursor-pointer text-[#422268]">
Publish my birthday
</Label>
</div>
<div className="flex items-center gap-3">
<input
type="checkbox"
id="newsletter_publish_none"
name="newsletter_publish_none"
checked={formData.newsletter_publish_none}
onChange={handleCheckboxChange}
className="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]"
/>
<Label htmlFor="newsletter_publish_none" className="cursor-pointer text-[#422268]">
Do not publish any information
</Label>
</div>
</div>
</div>
{/* Section 4: Volunteer Interests */}
<div className="pt-8 mt-8 border-t border-[#ddd8eb]">
<h2 className="text-2xl font-semibold text-[#422268] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<Users className="h-6 w-6 text-[#664fa3]" />
Volunteer Interests
</h2>
<p className="text-[#664fa3] mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Select areas where you'd like to volunteer and help our community.
</p>
<div className="grid md:grid-cols-2 gap-3">
{volunteerOptions.map(option => (
<div key={option} className="flex items-center gap-3">
<input
type="checkbox"
id={`volunteer_${option.replace(/\s+/g, '_').toLowerCase()}`}
checked={formData.volunteer_interests.includes(option)}
onChange={() => handleVolunteerToggle(option)}
className="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]"
/>
<Label
htmlFor={`volunteer_${option.replace(/\s+/g, '_').toLowerCase()}`}
className="cursor-pointer text-[#422268]"
>
{option}
</Label>
</div>
))}
</div>
</div>
{/* Section 5: Member Directory Settings */}
<div className="pt-8 mt-8 border-t border-[#ddd8eb]">
<h2 className="text-2xl font-semibold text-[#422268] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<BookUser className="h-6 w-6 text-[#ff9e77]" />
Member Directory Settings Member Directory Settings
</h2> </h4>
<p className="text-[#664fa3] mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Control your visibility and information in the member directory. Control your visibility and information in the member directory.
</p> </p>
<div className="space-y-6"> <div className="flex items-center gap-3 p-4 bg-[var(--lavender-400)] rounded-lg">
<div className="flex items-center gap-3 p-4 bg-[#f9f5ff] rounded-lg">
<input <input
type="checkbox" type="checkbox"
id="show_in_directory" id="show_in_directory"
name="show_in_directory" name="show_in_directory"
checked={formData.show_in_directory} checked={formData.show_in_directory}
onChange={handleCheckboxChange} onChange={handleCheckboxChange}
className="w-5 h-5 text-[#664fa3] border-2 border-[#ddd8eb] rounded focus:ring-[#664fa3]" className="ui-checkbox"
/> />
<Label htmlFor="show_in_directory" className="cursor-pointer text-[#422268] font-medium"> <Label htmlFor="show_in_directory" className="cursor-pointer text-[var(--purple-ink)] font-medium">
Include me in the member directory Include me in the member directory
</Label> </Label>
</div> </div>
{formData.show_in_directory && ( {formData.show_in_directory && (
<div className="space-y-6 pl-4 border-l-4 border-[#DDD8EB]"> <div className="space-y-4 pl-4 border-l-4 border-[var(--neutral-800)]">
<div> <div>
<Label htmlFor="directory_email">Directory Email</Label> <Label htmlFor="directory_email">Directory Email</Label>
<Input <Input
@@ -489,7 +471,7 @@ const Profile = () => {
type="email" type="email"
value={formData.directory_email} value={formData.directory_email}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
placeholder="Optional - email to show in directory" placeholder="Optional - email to show in directory"
/> />
</div> </div>
@@ -501,7 +483,7 @@ const Profile = () => {
name="directory_bio" name="directory_bio"
value={formData.directory_bio} value={formData.directory_bio}
onChange={handleInputChange} onChange={handleInputChange}
className="rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3] min-h-[100px]" className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple min-h-[100px]"
placeholder="Tell other members about yourself..." placeholder="Tell other members about yourself..."
/> />
</div> </div>
@@ -513,7 +495,7 @@ const Profile = () => {
name="directory_address" name="directory_address"
value={formData.directory_address} value={formData.directory_address}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
placeholder="Optional - address to show in directory" placeholder="Optional - address to show in directory"
/> />
</div> </div>
@@ -526,7 +508,7 @@ const Profile = () => {
type="tel" type="tel"
value={formData.directory_phone} value={formData.directory_phone}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
placeholder="Optional - phone to show in directory" placeholder="Optional - phone to show in directory"
/> />
</div> </div>
@@ -539,7 +521,7 @@ const Profile = () => {
type="date" type="date"
value={formData.directory_dob} value={formData.directory_dob}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
/> />
</div> </div>
@@ -550,36 +532,305 @@ const Profile = () => {
name="directory_partner_name" name="directory_partner_name"
value={formData.directory_partner_name} value={formData.directory_partner_name}
onChange={handleInputChange} onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
placeholder="Optional - partner name to show in directory" placeholder="Optional - partner name to show in directory"
/> />
</div> </div>
</div> </div>
)} )}
</div> </div>
</Card>
);
// Engagement Tab Content
const EngagementContent = () => (
<Card className="space-y-6 px-6 pb-6">
<div className="bg-brand-purple text-white px-4 py-3 rounded-t-lg -mx-6 -mt-6 mb-6">
<h3 className="font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>Engagement</h3>
</div> </div>
<div className="pt-8 mt-8 border-t border-[#ddd8eb]"> {/* Partner Information */}
<div className="space-y-4">
<h4 className="text-lg font-semibold text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<Heart className="h-5 w-5 text-[var(--orange-light)]" />
Partner Information
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<Label htmlFor="partner_first_name">Partner First Name</Label>
<Input
id="partner_first_name"
name="partner_first_name"
value={formData.partner_first_name}
onChange={handleInputChange}
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
placeholder="Optional"
/>
</div>
<div>
<Label htmlFor="partner_last_name">Partner Last Name</Label>
<Input
id="partner_last_name"
name="partner_last_name"
value={formData.partner_last_name}
onChange={handleInputChange}
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
placeholder="Optional"
/>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center gap-3">
<input
type="checkbox"
id="partner_is_member"
name="partner_is_member"
checked={formData.partner_is_member}
onChange={handleCheckboxChange}
className="ui-checkbox"
/>
<Label htmlFor="partner_is_member" className="cursor-pointer text-[var(--purple-ink)]">
My partner is a current member
</Label>
</div>
<div className="flex items-center gap-3">
<input
type="checkbox"
id="partner_plan_to_become_member"
name="partner_plan_to_become_member"
checked={formData.partner_plan_to_become_member}
onChange={handleCheckboxChange}
className="ui-checkbox"
/>
<Label htmlFor="partner_plan_to_become_member" className="cursor-pointer text-[var(--purple-ink)]">
My partner plans to become a member
</Label>
</div>
</div>
</div>
{/* Newsletter Preferences */}
<div className="pt-6 border-t border-[var(--neutral-800)] space-y-4">
<h4 className="text-lg font-semibold text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<Mail className="h-5 w-5 text-[var(--green-light)]" />
Newsletter Preferences
</h4>
<p className="text-brand-purple text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Choose what information you'd like published in our member newsletter.
</p>
<div className="space-y-3">
<div className="flex items-center gap-3">
<input
type="checkbox"
id="newsletter_publish_name"
name="newsletter_publish_name"
checked={formData.newsletter_publish_name}
onChange={handleCheckboxChange}
className="ui-checkbox"
/>
<Label htmlFor="newsletter_publish_name" className="cursor-pointer text-[var(--purple-ink)]">
Publish my name
</Label>
</div>
<div className="flex items-center gap-3">
<input
type="checkbox"
id="newsletter_publish_photo"
name="newsletter_publish_photo"
checked={formData.newsletter_publish_photo}
onChange={handleCheckboxChange}
className="ui-checkbox"
/>
<Label htmlFor="newsletter_publish_photo" className="cursor-pointer text-[var(--purple-ink)]">
Publish my photo
</Label>
</div>
<div className="flex items-center gap-3">
<input
type="checkbox"
id="newsletter_publish_birthday"
name="newsletter_publish_birthday"
checked={formData.newsletter_publish_birthday}
onChange={handleCheckboxChange}
className="ui-checkbox"
/>
<Label htmlFor="newsletter_publish_birthday" className="cursor-pointer text-[var(--purple-ink)]">
Publish my birthday
</Label>
</div>
<div className="flex items-center gap-3">
<input
type="checkbox"
id="newsletter_publish_none"
name="newsletter_publish_none"
checked={formData.newsletter_publish_none}
onChange={handleCheckboxChange}
className="ui-checkbox"
/>
<Label htmlFor="newsletter_publish_none" className="cursor-pointer text-[var(--purple-ink)]">
Do not publish any information
</Label>
</div>
</div>
</div>
{/* Volunteer Interests */}
<div className="pt-6 border-t border-[var(--neutral-800)] space-y-4">
<h4 className="text-lg font-semibold text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<Users className="h-5 w-5 text-brand-purple" />
Volunteer Interests
</h4>
<p className="text-brand-purple text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Select areas where you'd like to volunteer and help our community.
</p>
<div className="grid md:grid-cols-2 gap-3">
{volunteerOptions.map(option => (
<div key={option} className="flex items-center gap-3">
<input
type="checkbox"
id={`volunteer_${option.replace(/\s+/g, '_').toLowerCase()}`}
checked={formData.volunteer_interests.includes(option)}
onChange={() => handleVolunteerToggle(option)}
className="ui-checkbox"
/>
<Label
htmlFor={`volunteer_${option.replace(/\s+/g, '_').toLowerCase()}`}
className="cursor-pointer text-[var(--purple-ink)]"
>
{option}
</Label>
</div>
))}
</div>
</div>
</Card>
);
return (
<div className="min-h-screen bg-background flex flex-col">
<Navbar />
<div className="flex-1 flex flex-col">
<div className="max-w-5xl mx-auto px-4 sm:px-6 py-8 w-full flex-1 pb-24">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className='space-y-4'>
<h1 className="text-4xl md:text-4xl font-semibold " style={{ fontFamily: "'Inter', sans-serif" }}>
My Profile
</h1>
<p className='text-brand-purple text-md'>Update your personal information below.</p>
</div>
{/* <Button
type="button"
variant="outline"
className="border-2 hover:bg-white/10 rounded-lg px-4 py-2"
>
<Eye className="h-4 w-4 mr-2 md:mr-2" />
<span className="hidden md:inline">Public Profile Preview</span>
<span className="md:hidden">Preview</span>
</Button> */}
</div>
{/* Main Content Div */}
<div className="overflow-hidden ">
<form onSubmit={handleSubmit} data-testid="profile-form">
{/* Mobile Tabs */}
<div className="md:hidden flex border-b border-[var(--neutral-800)] mb-4 gap-1 ">
{tabs.map((tab) => {
const IconComponent = tab.icon;
return (
<button
key={tab.id}
type="button"
onClick={() => setActiveTab(tab.id)}
className={`flex-1 flex flex-col items-center rounded-xl gap-1 px-3 py-3 text-xs font-medium transition-colors ${activeTab === tab.id
? 'bg-brand-purple text-white'
: 'text-[var(--purple-ink)] hover:bg-[var(--lavender-300)]'
}`}
>
<IconComponent className="h-5 w-5" />
<span className="whitespace-nowrap">{tab.shortLabel}</span>
</button>
);
})}
</div>
{/* Desktop Layout */}
<div className="flex">
{/* Desktop Sidebar Tabs */}
<div className="hidden md:flex flex-col w-64 border-[var(--neutral-800)] mr-4 gap-2">
{tabs.map((tab) => {
const IconComponent = tab.icon;
return (
<button
key={tab.id}
type="button"
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-3 px-4 py-3 rounded-xl text-left font-medium transition-colors ${activeTab === tab.id
? 'bg-brand-purple text-white'
: 'text-[var(--purple-ink)] hover:bg-[var(--lavender-300)]'
}`}
>
<IconComponent className="h-5 w-5" />
<span>{tab.label}</span>
</button>
);
})}
</div>
{/* Content Area */}
<div className="flex-1 p-6 min-h-[500px]">
{activeTab === 'account' && <AccountPrivacyContent />}
{activeTab === 'bio' && <BioDirectoryContent />}
{activeTab === 'engagement' && <EngagementContent />}
</div>
</div>
</form>
</div>
</div>
{/* Sticky Footer */}
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-[var(--neutral-800)] px-4 sm:px-6 py-4 z-50">
<div className="max-w-5xl px-6 mx-auto flex items-center justify-between">
<div className='flex gap-2 w-full lg:justify-between md:mr-5'>
<Button <Button
type="submit" onClick={() => navigate(-1)}
className="h-fit bg-brand-purple hover:bg-brand-purple/80 rounded-lg px-6 py-2 font-medium shadow-lg w-full md:w-auto">
<ArrowLeft className="h-4 w-4 mr-2" />
Back
</Button>
<div className="flex items-center gap-2">
{hasUnsavedChanges && (
<>
<span className="h-3 w-3 rounded-full bg-[var(--orange-light)]"></span>
<span className="text-sm text-[var(--purple-ink)] " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Unsaved changes
</span>
</>
)}
</div>
<Button
type="button"
onClick={handleSubmit}
disabled={loading} disabled={loading}
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-8 py-6 text-lg font-medium shadow-lg disabled:opacity-50" className="bg-brand-purple text-white hover:bg-brand-dark-lavender rounded-lg px-6 py-2 font-medium shadow-lg disabled:opacity-50 w-full md:w-auto"
data-testid="save-profile-button" data-testid="save-profile-button"
> >
<Save className="h-5 w-5 mr-2" />
{loading ? 'Saving...' : 'Save Changes'} {loading ? 'Saving...' : 'Save Changes'}
</Button> </Button>
</div> </div>
</form>
</Card> </div>
</div>
</div>
<ChangePasswordDialog <ChangePasswordDialog
open={passwordDialogOpen} open={passwordDialogOpen}
onOpenChange={setPasswordDialogOpen} onOpenChange={setPasswordDialogOpen}
/> />
</div> </div>
<MemberFooter />
</div>
); );
}; };

View File

@@ -183,23 +183,23 @@ const Register = () => {
}; };
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-background">
<PublicNavbar /> <PublicNavbar />
<div className="max-w-4xl mx-auto px-6 py-12"> <div className="max-w-4xl mx-auto px-6 py-12">
<div className="mb-8"> <div className="mb-8">
<Link to="/" className="inline-flex items-center text-[#664fa3] hover:text-[#ff9e77] transition-colors"> <Link to="/" className="inline-flex items-center text-brand-purple hover:text-[var(--orange-light)] transition-colors">
<ArrowLeft className="h-4 w-4 mr-2" /> <ArrowLeft className="h-4 w-4 mr-2" />
Back to Home Back to Home
</Link> </Link>
</div> </div>
<Card className="p-8 md:p-12 bg-white rounded-2xl border border-[#ddd8eb] shadow-lg"> <Card className="p-8 md:p-12 bg-background rounded-2xl border border-[var(--neutral-800)] shadow-lg">
<div className="mb-8"> <div className="mb-8">
<h1 className="text-4xl md:text-5xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Join Our Community Join Our Community
</h1> </h1>
<p className="text-lg text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Fill out the form below to start your membership journey. Fill out the form below to start your membership journey.
</p> </p>
</div> </div>
@@ -245,7 +245,7 @@ const Register = () => {
type="button" type="button"
onClick={handleBack} onClick={handleBack}
variant="outline" variant="outline"
className="rounded-full px-6 py-6 text-lg border-2 border-[#ddd8eb] hover:border-[#664fa3] text-[#422268]" className="rounded-full px-6 py-6 text-lg border-2 border-[var(--neutral-800)] hover:border-brand-purple text-[var(--purple-ink)]"
> >
<ArrowLeft className="mr-2 h-5 w-5" /> <ArrowLeft className="mr-2 h-5 w-5" />
Back Back
@@ -258,7 +258,7 @@ const Register = () => {
<Button <Button
type="button" type="button"
onClick={handleNext} onClick={handleNext}
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-6 py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform" className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-brand-purple hover:text-background rounded-full px-6 py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform"
> >
Next Next
<ArrowRight className="ml-2 h-5 w-5" /> <ArrowRight className="ml-2 h-5 w-5" />
@@ -267,7 +267,7 @@ const Register = () => {
<Button <Button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-6 py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform disabled:opacity-50 disabled:cursor-not-allowed" className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-brand-purple hover:text-backgroundrounded-full px-6 py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform disabled:opacity-50 disabled:cursor-not-allowed"
data-testid="submit-register-button" data-testid="submit-register-button"
> >
{loading ? 'Creating Account...' : 'Create Account'} {loading ? 'Creating Account...' : 'Create Account'}
@@ -276,9 +276,9 @@ const Register = () => {
)} )}
</div> </div>
<p className="text-center text-[#664fa3] mt-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-center text-brand-purple mt-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Already have an account?{' '} Already have an account?{' '}
<Link to="/login" className="text-[#ff9e77] hover:underline font-medium"> <Link to="/login" className="text-[var(--orange-light)] hover:underline font-medium">
Login here Login here
</Link> </Link>
</p> </p>

View File

@@ -64,19 +64,19 @@ const ResetPassword = () => {
}; };
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-background">
<PublicNavbar /> <PublicNavbar />
<div className="max-w-md mx-auto px-6 py-12"> <div className="max-w-md mx-auto px-6 py-12">
<Card className="p-8 md:p-12 bg-white rounded-2xl border border-[#ddd8eb] shadow-lg"> <Card className="p-8 md:p-12 bg-background rounded-2xl border border-[var(--neutral-800)] shadow-lg">
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-[#f1eef9] mb-4"> <div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-[var(--lavender-300)] mb-4">
<Lock className="h-8 w-8 text-[#664fa3]" /> <Lock className="h-8 w-8 text-brand-purple " />
</div> </div>
<h1 className="text-4xl md:text-5xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Reset Password Reset Password
</h1> </h1>
<p className="text-lg text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Enter your new password below. Enter your new password below.
</p> </p>
</div> </div>
@@ -92,7 +92,7 @@ const ResetPassword = () => {
value={formData.newPassword} value={formData.newPassword}
onChange={handleInputChange} onChange={handleInputChange}
placeholder="Enter new password (min. 6 characters)" placeholder="Enter new password (min. 6 characters)"
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
/> />
</div> </div>
@@ -106,15 +106,15 @@ const ResetPassword = () => {
value={formData.confirmPassword} value={formData.confirmPassword}
onChange={handleInputChange} onChange={handleInputChange}
placeholder="Re-enter new password" placeholder="Re-enter new password"
className="h-14 rounded-xl border-2 border-[#ddd8eb] focus:border-[#664fa3]" className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
/> />
</div> </div>
<div className="bg-[#f1eef9] border-l-4 border-[#664fa3] p-4 rounded-lg"> <div className="bg-[var(--lavender-300)] border-l-4 border-brand-purple p-4 rounded-lg">
<div className="flex items-start"> <div className="flex items-start">
<AlertCircle className="h-5 w-5 text-[#664fa3] mr-2 mt-0.5 flex-shrink-0" /> <AlertCircle className="h-5 w-5 text-brand-purple mr-2 mt-0.5 flex-shrink-0" />
<div className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <div className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<p className="font-medium text-[#422268] mb-1">Password Requirements:</p> <p className="font-medium text-[var(--purple-ink)] mb-1">Password Requirements:</p>
<ul className="list-disc list-inside space-y-1"> <ul className="list-disc list-inside space-y-1">
<li>At least 6 characters long</li> <li>At least 6 characters long</li>
<li>Both passwords must match</li> <li>Both passwords must match</li>
@@ -126,15 +126,15 @@ const ResetPassword = () => {
<Button <Button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="w-full bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform disabled:opacity-50" className="w-full bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background rounded-full py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform disabled:opacity-50"
> >
{loading ? 'Resetting Password...' : 'Reset Password'} {loading ? 'Resetting Password...' : 'Reset Password'}
<ArrowRight className="ml-2 h-5 w-5" /> <ArrowRight className="ml-2 h-5 w-5" />
</Button> </Button>
<p className="text-center text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-center text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Remember your password?{' '} Remember your password?{' '}
<Link to="/login" className="text-[#ff9e77] hover:underline font-medium"> <Link to="/login" className="text-[var(--orange-light)] hover:underline font-medium">
Login here Login here
</Link> </Link>
</p> </p>

View File

@@ -4,6 +4,7 @@ import PublicFooter from '../components/PublicFooter';
import { Card } from '../components/ui/card'; import { Card } from '../components/ui/card';
import { ChevronDown, ExternalLink, Phone, Mail, MapPin } from 'lucide-react'; import { ChevronDown, ExternalLink, Phone, Mail, MapPin } from 'lucide-react';
import { FaFlag, FaHeartbeat, FaUtensils } from "react-icons/fa";
const Resources = () => { const Resources = () => {
const [openAccordions, setOpenAccordions] = useState({}); const [openAccordions, setOpenAccordions] = useState({});
@@ -23,6 +24,7 @@ const Resources = () => {
const categories = [ const categories = [
{ {
title: 'General LGBTQ+', title: 'General LGBTQ+',
icon: <FaFlag />,
resources: [ resources: [
{ {
name: 'SPRY (Seniors Preparing for Rainbow Years)', name: 'SPRY (Seniors Preparing for Rainbow Years)',
@@ -52,6 +54,7 @@ const Resources = () => {
}, },
{ {
title: 'Healthcare', title: 'Healthcare',
icon: <FaHeartbeat />,
resources: [ resources: [
{ {
name: 'LHI (Lesbian Health Initiative)', name: 'LHI (Lesbian Health Initiative)',
@@ -67,6 +70,7 @@ const Resources = () => {
}, },
{ {
title: 'Food Assistance', title: 'Food Assistance',
icon: <FaUtensils />,
resources: [ resources: [
{ {
name: 'Meals on Wheels', name: 'Meals on Wheels',
@@ -93,18 +97,16 @@ const Resources = () => {
]; ];
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-background">
<PublicNavbar /> <PublicNavbar />
<main className="bg-gradient-to-b from-white via-[#f1eef9] to-[#e8e0f5] px-6 py-16"> <main className="bg-gradient-to-b from-white via-[var(--lavender-300)] to-[var(--lavender-100)] px-6 py-16">
{/* Header Section */} {/* Header Section */}
<section className="max-w-7xl mx-auto mb-12"> <section className="max-w-7xl mx-auto mb-12">
<h1 className="text-4xl md:text-5xl font-bold text-[#48286e] text-center mb-6" style={{ fontFamily: "'Inter', sans-serif" }}> <h1 className="text-[28px] font-bold text-[var(--purple-deep)] text-center mb-12" style={{ fontFamily: "'Inter', sans-serif" }}>
Resources
</h1>
<p className="text-xl text-[#48286e] text-center max-w-3xl mx-auto" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Tap or click on each purple tab below to open and read its contents Tap or click on each purple tab below to open and read its contents
</p> </h1>
</section> </section>
{/* Resources Grid */} {/* Resources Grid */}
@@ -113,7 +115,8 @@ const Resources = () => {
{categories.map((category, categoryIndex) => ( {categories.map((category, categoryIndex) => (
<div key={categoryIndex} className="space-y-6"> <div key={categoryIndex} className="space-y-6">
{/* Category Title */} {/* Category Title */}
<h2 className="text-3xl font-bold text-[#48286e] text-center mb-8" style={{ fontFamily: "'Inter', sans-serif" }}> <div className="flex justify-center text-4xl text-[var(--purple-lilac)]">{category.icon}</div>
<h2 className="text-[32px] leading-6 font-bold text-[var(--purple-deep)] text-center mb-8" style={{ fontFamily: "'Poppins', sans-serif" }}>
{category.title} {category.title}
</h2> </h2>
@@ -127,27 +130,27 @@ const Resources = () => {
{/* Accordion Button */} {/* Accordion Button */}
<button <button
onClick={() => toggleAccordion(categoryIndex, resourceIndex)} onClick={() => toggleAccordion(categoryIndex, resourceIndex)}
className="w-full bg-[#664fa3] hover:bg-[#5a4290] text-white px-6 py-4 rounded-full flex items-center justify-between transition-all shadow-lg hover:shadow-xl" className={`w-full bg-gradient-to-tr from-[var(--purple-deep)] to-[var(--purple-lavender)] hover:bg-[var(--purple-soft)] text-white px-6 py-4 rounded-3xl flex items-center justify-between transition-all ${isExpanded ? 'rounded-b-none rounded-t-3xl' : ''}`
}
> >
<span className="text-lg font-semibold text-left" style={{ fontFamily: "'Inter', sans-serif" }}> <span className="text-lg font-semibold text-left" style={{ fontFamily: "'Inter', sans-serif" }}>
{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' : '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-background rounded-b-2xl rounded-t-none border-none ">
{/* Description */} {/* Description */}
<p className="text-[#48286e] mb-4 leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-[var(--purple-deep)] mb-4 leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{resource.description} {resource.description}
</p> </p>
@@ -155,7 +158,7 @@ const Resources = () => {
<div className="space-y-3"> <div className="space-y-3">
{/* Location */} {/* Location */}
{resource.location && ( {resource.location && (
<div className="flex items-start gap-2 text-[#664fa3]"> <div className="flex items-start gap-2 text-[var(--purple-lavender)]">
<MapPin className="h-5 w-5 flex-shrink-0 mt-0.5" /> <MapPin className="h-5 w-5 flex-shrink-0 mt-0.5" />
<span className="text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <span className="text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{resource.location} {resource.location}
@@ -165,17 +168,17 @@ const Resources = () => {
{/* Contact */} {/* Contact */}
{resource.contact && ( {resource.contact && (
<div className="text-[#664fa3]"> <div className="text-[var(--purple-lavender)]">
<p className="text-sm font-medium mb-1" style={{ fontFamily: "'Inter', sans-serif" }}> <p className="text-sm font-medium mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
Contact: {resource.contact} Contact: {resource.contact}
</p> </p>
<div className="flex flex-col gap-1 ml-0"> <div className="flex flex-col gap-1 ml-0">
{resource.phone && ( {resource.phone && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Phone className="h-4 w-4" /> <Phone className="size-4" />
<a <a
href={`tel:${resource.phone.replace(/[^0-9]/g, '')}`} href={`tel:${resource.phone.replace(/[^0-9]/g, '')}`}
className="text-sm hover:text-[#48286e] transition-colors" className="text-sm hover:text-[var(--purple-deep)] transition-colors"
style={{ fontFamily: "'Nunito Sans', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
> >
{resource.phone} {resource.phone}
@@ -187,7 +190,7 @@ const Resources = () => {
<Mail className="h-4 w-4" /> <Mail className="h-4 w-4" />
<a <a
href={`mailto:${resource.email}`} href={`mailto:${resource.email}`}
className="text-sm hover:text-[#48286e] transition-colors" className="text-sm hover:text-[var(--purple-deep)] transition-colors"
style={{ fontFamily: "'Nunito Sans', sans-serif" }} style={{ fontFamily: "'Nunito Sans', sans-serif" }}
> >
{resource.email} {resource.email}
@@ -204,7 +207,7 @@ const Resources = () => {
href={resource.link} href={resource.link}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-[#ff9e77] hover:text-[#e88a63] font-medium transition-colors mt-2" className="inline-flex items-center gap-2 text-[var(--orange-light)] hover:text-[var(--orange-peach)] font-medium transition-colors mt-2"
style={{ fontFamily: "'Inter', sans-serif" }} style={{ fontFamily: "'Inter', sans-serif" }}
> >
Visit Website Visit Website
@@ -225,7 +228,7 @@ const Resources = () => {
{/* Additional Help Section */} {/* Additional Help Section */}
<section className="max-w-4xl mx-auto mt-16"> <section className="max-w-4xl mx-auto mt-16">
<Card className="p-8 bg-gradient-to-r from-[#664fa3] to-[#48286e] rounded-2xl shadow-xl text-center"> <Card className="p-8 bg-gradient-to-r from-[var(--purple-lavender)] to-[var(--purple-deep)] rounded-2xl shadow-xl text-center">
<h3 className="text-2xl font-bold text-white mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h3 className="text-2xl font-bold text-white mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Need Additional Support? Need Additional Support?
</h3> </h3>
@@ -234,7 +237,7 @@ const Resources = () => {
</p> </p>
<a <a
href="mailto:support@loaf.org" href="mailto:support@loaf.org"
className="inline-block bg-white text-[#48286e] px-8 py-3 rounded-full font-semibold hover:bg-[#f1eef9] transition-colors shadow-lg" className="inline-block bg-background text-[var(--purple-deep)] px-8 py-3 rounded-full font-semibold hover:bg-[var(--lavender-300)] transition-colors shadow-lg"
style={{ fontFamily: "'Inter', sans-serif" }} style={{ fontFamily: "'Inter', sans-serif" }}
> >
Contact Us Contact Us

View File

@@ -3,315 +3,603 @@ import { Link } from 'react-router-dom';
import PublicNavbar from '../components/PublicNavbar'; import PublicNavbar from '../components/PublicNavbar';
import PublicFooter from '../components/PublicFooter'; import PublicFooter from '../components/PublicFooter';
export default function TermsOfService() { export default function TermsOfService() {
return ( return (
<> <>
<PublicNavbar /> <PublicNavbar />
<div className="min-h-screen bg-[#F8F7FB]"> <main className="bg-gradient-to-bl from-[var(--neutral-100:)] to-[var(--neutral-800)] text-[var(--purple-deep)]">
<div className="max-w-4xl mx-auto px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-12"> <div className="mx-auto w-full max-w-5xl px-4 sm:px-6 lg:px-8 py-10">
{/* Header */} {/* Title */}
<div className="mb-8"> <header className="border-b pb-6">
<h1 className="text-3xl sm:text-4xl md:text-5xl font-bold text-[#422268] mb-4" <h1 className="text-3xl sm:text-4xl font-bold tracking-tight ">
style={{ fontFamily: "'Inter', sans-serif" }}>
Terms of Service Terms of Service
</h1> </h1>
<p className="text-base sm:text-lg text-gray-600" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="mt-3 text-sm">
Last Updated: January 2025 <span className="font-medium">Last updated March 23, 2025</span>
</p> </p>
</div> </header>
{/* Content */} {/* Body */}
<div className="bg-white rounded-lg border border-[#ddd8eb] p-6 sm:p-8 md:p-10 space-y-8"> <div className="prose text-[var(--purple-deep)] max-w-none prose-h2:mt-10 prose-h2:scroll-mt-24 prose-h3:mt-6">
{/* AGREEMENT */}
{/* Section 1: Agreement to Terms */} <section aria-labelledby="agreement" className="mt-8">
<section> <h2 id="agreement" className="text-xl sm:text-2xl text-[var(--purple-deep)] font-bold ">
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4" AGREEMENT TO OUR LEGAL TERMS
style={{ fontFamily: "'Inter', sans-serif" }}>
1. Agreement to Terms
</h2> </h2>
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<p className="text-gray-700 leading-relaxed"> <p>
These Terms of Service constitute a legally binding agreement made between you, whether personally or on behalf of an entity ("you") and LOAFers, Inc. ("Company", "we", "us", or "our"), concerning your access to and use of the https://loaftx.org website as well as any other media form, media channel, mobile website or mobile application related, linked, or otherwise connected thereto (collectively, the "Site"). We are LOAFers, Inc. ("Company," "we," "us," "our").
</p> </p>
<p className="text-gray-700 leading-relaxed">
You agree that by accessing the Site, you have read, understood, and agree to be bound by all of these Terms of Service. If you do not agree with all of these Terms of Service, then you are expressly prohibited from using the Site and you must discontinue use immediately. <p>
We operate, as well as any other related products and services that refer or link to these legal terms
(the "Legal Terms") (collectively, the "Services").
</p>
<p>
You can contact us by email at{" "}
<a className="" href="mailto:info@loaftx.com">
info@loaftx.com
</a>{" "}
or by mail to PO Box 7207, Houston, TX 77249, United States.
</p>
<p>
These Legal Terms constitute a legally binding agreement made between you, whether personally or on behalf
of an entity ("you"), and LOAFers, Inc., concerning your access to and use of the Services. You agree that
by accessing the Services, you have read, understood, and agreed to be bound by all of these Legal Terms.{" "}
<strong>
IF YOU DO NOT AGREE WITH ALL OF THESE LEGAL TERMS, THEN YOU ARE EXPRESSLY PROHIBITED FROM USING THE
SERVICES AND YOU MUST DISCONTINUE USE IMMEDIATELY.
</strong>
</p>
<p>
Supplemental terms and conditions or documents that may be posted on the Services from time to time are
hereby expressly incorporated herein by reference. We reserve the right, in our sole discretion, to make
changes or modifications to these Legal Terms at any time and for any reason. We will alert you about any
changes by updating the "Last updated" date of these Legal Terms, and you waive any right to receive
specific notice of each such change. It is your responsibility to periodically review these Legal Terms to
stay informed of updates. You will be subject to, and will be deemed to have been made aware of and to have
accepted, the changes in any revised Legal Terms by your continued use of the Services after the date such
revised Legal Terms are posted. We recommend that you print a copy of these Legal Terms for your records.
</p> </p>
</div>
</section> </section>
{/* Section 2: Intellectual Property Rights */} {/* TABLE OF CONTENTS */}
<section> <section aria-labelledby="toc" className="text-[var(--purple-deep)]">
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4" <h2 id="toc" className="text-lg sm:text-xl font-bold text-[var(--purple-deep)] m-0">
style={{ fontFamily: "'Inter', sans-serif" }}> TABLE OF CONTENTS
2. Intellectual Property Rights
</h2> </h2>
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<p className="text-gray-700 leading-relaxed"> <ol className="mt-4 list-decimal no-prose text-[var(--purple-deep)] pl-5 space-y-1">
Unless otherwise indicated, the Site is our proprietary property and all source code, databases, functionality, software, website designs, audio, video, text, photographs, and graphics on the Site (collectively, the "Content") and the trademarks, service marks, and logos contained therein (the "Marks") are owned or controlled by us or licensed to us, and are protected by copyright and trademark laws and various other intellectual property rights and unfair competition laws of the United States, foreign jurisdictions, and international conventions. <li><a className="text-[var(--purple-deep)]" href="#our-services">OUR SERVICES</a></li>
</p> <li><a className="text-[var(--purple-deep)]" href="#ipr">INTELLECTUAL PROPERTY RIGHTS</a></li>
</div> <li><a className="text-[var(--purple-deep)]" href="#user-representations">USER REPRESENTATIONS</a></li>
<li><a className="text-[var(--purple-deep)]" href="#prohibited-activities">PROHIBITED ACTIVITIES</a></li>
<li><a className="text-[var(--purple-deep)]" href="#ugc">USER GENERATED CONTRIBUTIONS</a></li>
<li><a className="text-[var(--purple-deep)]" href="#contribution-license">CONTRIBUTION LICENSE</a></li>
<li><a className="text-[var(--purple-deep)]" href="#services-management">SERVICES MANAGEMENT</a></li>
<li><a className="text-[var(--purple-deep)]" href="#term-termination">TERM AND TERMINATION</a></li>
<li><a className="text-[var(--purple-deep)]" href="#modifications">MODIFICATIONS AND INTERRUPTIONS</a></li>
<li><a className="text-[var(--purple-deep)]" href="#governing-law">GOVERNING LAW</a></li>
<li><a className="text-[var(--purple-deep)]" href="#dispute-resolution">DISPUTE RESOLUTION</a></li>
<li><a className="text-[var(--purple-deep)]" href="#corrections">CORRECTIONS</a></li>
<li><a className="text-[var(--purple-deep)]" href="#disclaimer">DISCLAIMER</a></li>
<li><a className="text-[var(--purple-deep)]" href="#limitations-liability">LIMITATIONS OF LIABILITY</a></li>
<li><a className="text-[var(--purple-deep)]" href="#indemnification">INDEMNIFICATION</a></li>
<li><a className="text-[var(--purple-deep)]" href="#user-data">USER DATA</a></li>
<li><a className="text-[var(--purple-deep)]" href="#electronic-comms">ELECTRONIC COMMUNICATIONS, TRANSACTIONS, AND SIGNATURES</a></li>
<li><a className="text-[var(--purple-deep)]" href="#miscellaneous">MISCELLANEOUS</a></li>
<li><a className="text-[var(--purple-deep)]" href="#contact-us">CONTACT US</a></li>
</ol>
</section> </section>
{/* Section 3: User Representations */} {/* 1. OUR SERVICES */}
<section> <section id="our-services" className="scroll-mt-24">
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4" <h2 className="text-xl text-[var(--purple-deep)] sm:text-2xl font-bold ">1. OUR SERVICES</h2>
style={{ fontFamily: "'Inter', sans-serif" }}> <p>
3. User Representations The information provided when using the Services is not intended for distribution to or use by any person
</h2> or entity in any jurisdiction or country where such distribution or use would be contrary to law or
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> regulation or which would subject us to any registration requirement within such jurisdiction or country.
<p className="text-gray-700 leading-relaxed"> Accordingly, those persons who choose to access the Services from other locations do so on their own
By using the Site, you represent and warrant that: initiative and are solely responsible for compliance with local laws, if and to the extent local laws are
applicable.
</p> </p>
<ul className="list-disc list-inside space-y-2 text-gray-700 ml-4"> </section>
<li>All registration information you submit will be true, accurate, current, and complete</li>
<li>You will maintain the accuracy of such information and promptly update such registration information as necessary</li> {/* 2. INTELLECTUAL PROPERTY RIGHTS */}
<li>You have the legal capacity and you agree to comply with these Terms of Service</li> <section id="ipr" className="scroll-mt-24">
<li>You are not under the age of 13</li> <h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)] ">2. INTELLECTUAL PROPERTY RIGHTS</h2>
<li>Not a minor in the jurisdiction in which you reside, or if a minor, you have received parental permission to use the Site</li>
<li>You will not access the Site through automated or non-human means</li> <h3 className="text-lg font-semibold ">Our intellectual property</h3>
<li>You will not use the Site for any illegal or unauthorized purpose</li> <p>
<li>Your use of the Site will not violate any applicable law or regulation</li> We are the owner or the licensee of all intellectual property rights in our Services, including all source
code, databases, functionality, software, website designs, audio, video, text, photographs, and graphics
in the Services (collectively, the "Content"), as well as the trademarks, service marks, and logos
contained therein (the "Marks"). Our Content and Marks are protected by copyright and trademark laws (and
various other intellectual property rights and unfair competition laws) and treaties around the world. The
Content and Marks are provided in or through the Services "AS IS" for your personal, non-commercial use or
internal business purpose only.
</p>
<h3 className="text-lg font-semibold text-[var(--purple-deep)]">Your use of our Services</h3>
<p>
Subject to your compliance with these Legal Terms, including the "PROHIBITED ACTIVITIES" section below, we
grant you a non-exclusive, non-transferable, revocable license to:
</p>
<ul className="list-disc pl-6 space-y-1">
<li>access the Services; and</li>
<li>
download or print a copy of any portion of the Content to which you have properly gained access, solely
for your personal, non-commercial use or internal business purpose.
</li>
</ul> </ul>
</div>
</section>
{/* Section 4: Prohibited Activities */} <p>
<section> Except as set out in this section or elsewhere in our Legal Terms, no part of the Services and no Content
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4" or Marks may be copied, reproduced, aggregated, republished, uploaded, posted, publicly displayed, encoded,
style={{ fontFamily: "'Inter', sans-serif" }}> translated, transmitted, distributed, sold, licensed, or otherwise exploited for any commercial purpose
4. Prohibited Activities whatsoever, without our express prior written permission. If you wish to make any use of the Services,
</h2> Content, or Marks other than as set out in this section or elsewhere in our Legal Terms, please address
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> your request to:{" "}
<p className="text-gray-700 leading-relaxed"> <a className="" href="mailto:loafhoustontx@gmail.com">
You may not access or use the Site for any purpose other than that for which we make the Site available. The Site may not be used in connection with any commercial endeavors except those that are specifically endorsed or approved by us. As a user of the Site, you agree not to: loafhoustontx@gmail.com
</a>
.
</p> </p>
<ul className="list-disc list-inside space-y-2 text-gray-700 ml-4">
<li>Systematically retrieve data or other content from the Site to create or compile, directly or indirectly, a collection, compilation, database, or directory without written permission from us</li> <p>
<li>Make any unauthorized use of the Site, including collecting usernames and/or email addresses of users by electronic or other means for the purpose of sending unsolicited email, or creating user accounts by automated means or under false pretenses</li> If we ever grant you the permission to post, reproduce, or publicly display any part of our Services or
<li>Circumvent, disable, or otherwise interfere with security-related features of the Site</li> Content, you must identify us as the owners or licensors of the Services, Content, or Marks and ensure that
<li>Engage in unauthorized framing of or linking to the Site</li> any copyright or proprietary notice appears or is visible on posting, reproducing, or displaying our
<li>Trick, defraud, or mislead us and other users, especially in any attempt to learn sensitive account information such as user passwords</li> Content. We reserve all rights not expressly granted to you in and to the Services, Content, and Marks. Any
<li>Make improper use of our support services or submit false reports of abuse or misconduct</li> breach of these Intellectual Property Rights will constitute a material breach of our Legal Terms and your
<li>Engage in any automated use of the system, such as using scripts to send comments or messages</li> right to use our Services will terminate immediately.
<li>Interfere with, disrupt, or create an undue burden on the Site or the networks or services connected to the Site</li> </p>
<h3 className="text-lg font-semibold text-[var(--purple-deep)]">Your submissions</h3>
<p>
Please review this section and the "PROHIBITED ACTIVITIES" section carefully prior to using our Services to
understand the (a) rights you give us and (b) obligations you have when you post or upload any content
through the Services.
</p>
<p>
<strong>Submissions:</strong> By directly sending us any question, comment, suggestion, idea, feedback, or
other information about the Services ("Submissions"), you agree to assign to us all intellectual property
rights in such Submission. You agree that we shall own this Submission and be entitled to its unrestricted
use and dissemination for any lawful purpose, commercial or otherwise, without acknowledgment or
compensation to you.
</p>
<p>
<strong>
You are responsible for what you post or upload: By sending us Submissions through any part of the
Services you:
</strong>
</p>
<ul className="list-disc pl-6 space-y-2">
<li>
confirm that you have read and agree with our "PROHIBITED ACTIVITIES" and will not post, send, publish,
upload, or transmit through the Services any Submission that is illegal, harassing, hateful, harmful,
defamatory, obscene, bullying, abusive, discriminatory, threatening to any person or group, sexually
explicit, false, inaccurate, deceitful, or misleading;
</li>
<li>to the extent permissible by applicable law, waive any and all moral rights to any such Submission;</li>
<li>
warrant that any such Submission are original to you or that you have the necessary rights and licenses
to submit such Submissions and that you have full authority to grant us the above-mentioned rights in
relation to your Submissions; and
</li>
<li>warrant and represent that your Submissions do not constitute confidential information.</li>
</ul> </ul>
</div>
<p>
You are solely responsible for your Submissions and you expressly agree to reimburse us for any and all
losses that we may suffer because of your breach of (a) this section, (b) any third partys intellectual
property rights, or (c) applicable law.
</p>
</section> </section>
{/* Section 5: User Generated Contributions */} {/* 3. USER REPRESENTATIONS */}
<section> <section id="user-representations" className="scroll-mt-24">
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4" <h2 className="text-xl sm:text-2xl font-bold ">3. USER REPRESENTATIONS</h2>
style={{ fontFamily: "'Inter', sans-serif" }}> <p>By using the Services, you represent and warrant that:</p>
5. User Generated Contributions <ol className="list-decimal pl-6 space-y-1">
<li>you have the legal capacity and you agree to comply with these Legal Terms;</li>
<li>you are not a minor in the jurisdiction in which you reside;</li>
<li>you will not access the Services through automated or non-human means, whether through abot, script or otherwise;</li>
<li>you will not use the Services for any illegal or unauthorized purpose; and</li>
<li>your use of the Services will not violate any applicable law or regulation.</li>
</ol>
<p>
If you provide any information that is untrue, inaccurate, not current, or incomplete, we have the right to
suspend or terminate your account and refuse any and all current or future use of the Services (or any
portion thereof).
</p>
</section>
{/* 4. PROHIBITED ACTIVITIES */}
<section id="prohibited-activities" className="scroll-mt-24">
<h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">4. PROHIBITED ACTIVITIES</h2>
<p>
You may not access or use the Services for any purpose other than that for which we make the Services
available. The Services may not be used in connection with any commercial endeavors except those that are
specifically endorsed or approved by us. As a user of the Services, you agree not to:
</p>
<ul className="list-disc pl-6 space-y-2">
<li>Systematically retrieve data or other content from the Services to create or compile, directly or indirectly, a collection, compilation, database, or directory without written permission from us.</li>
<li>Trick, defraud, or mislead us and other users, especially in any attempt to learn sensitive account information such as user passwords.</li>
<li>Circumvent, disable, or otherwise interfere with security-related features of the Services, including features that prevent or restrict the use or copying of any Content or enforce limitations on the use of the Services and/or the Content contained therein.</li>
<li>Disparage, tarnish, or otherwise harm, in our opinion, us and/or the Services.</li>
<li>Use any information obtained from the Services to harass abuse or harm another person.</li>
<li>Make improper use of our support services or submit false reports ofabuse or misconduct.</li>
<li>Use the Services in a manner inconsistent with any applicable laws or regulations.</li>
<li>Engage in unauthorized framing of or linking to the Services.</li>
<li>Upload or transmit (or attempt to upload or to transmit) viruses, Trojan horses, or other material, including excessive use of capital letters and spamming (continuous posting of repetitive text), that interferes with any partys uninterrupted use and enjoyment of the Services or modifies, impairs, disrupts, alters, or interferes with the use, features, functions, operation, or maintenance of the Services.</li>
<li>Engage in any automated use of the system, such as using scripts to send comments or messages, or using any data mining, robots, or similar data gathering and extraction tools.</li>
<li>Delete the copyright or other proprietary rights notice from any Content.</li>
<li>Attempt to impersonate another user or person or use the username of another user.</li>
<li>Upload or transmit (or attempt to upload or to transmit) any material that acts as a passive or active information collection or transmission mechanism, including without limitation, clear graphics interchange formats ("gifs"), 1×1 pixels, web bugs, cookies, or other similar devices (sometimes referred to as "spyware" or "passive collection mechanisms" or "pcms").</li>
<li>Interfere with, disrupt, or create an undue burden on the Services or the networks or services connected to the Services.</li>
<li>Harass, annoy, intimidate, or threaten any of our employees or agents engaged in providing any portion of the Services to you.</li>
<li>Attempt to bypass any measures of the Services designed to prevent or restrict access to the Services, or any portion of the Services.</li>
<li>Copy or adapt the Services' software, including but not limited to Flash, PHP, HTML, JavaScript, or other code.</li>
<li>Except as permitted by applicable law, decipher, decompile, disassemble, or reverse engineer any of the software comprising or in any way making up a part of the Services.</li>
<li>Except as may be the result of standard search engine or Internet browser usage, use, launch, develop, or distribute any automated system, including without limitation, any spider, robot, cheat utility, scraper, or offline reader that accesses the Services, or use or launch any unauthorized script or other software.</li>
<li>Use a buying agent or purchasing agent to make purchases on the Services.</li>
<li>Make any unauthorized use of the Services, including collecting usernames and/or email addresses of users by electronic or other means for the purpose of sending unsolicited email, or creating user accounts by automated means or under false pretenses.</li>
<li>Use the Services as part of any effort to compete with us or otherwise use the Services and/or the Content for any revenue-generating endeavor or commercial enterprise.</li>
</ul>
</section>
{/* 5. USER GENERATED CONTRIBUTIONS */}
<section id="ugc" className="scroll-mt-24">
<h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">5. USER GENERATED CONTRIBUTIONS</h2>
<p>
The Services does not offer users to submit or post content. We may provide you with the opportunity to
create, submit, post, display, transmit, perform, publish, distribute, or broadcast content and materials
to us or on the Services, including but not limited to text, writings, video, audio, photographs, graphics,
comments, suggestions, or personal information or other material (collectively, "Contributions").
Contributions may be viewable by other users of the Services and through third-party websites. When you
create or make available any Contributions, you thereby represent and warrant that:
</p>
</section>
{/* 6. CONTRIBUTION LICENSE */}
<section id="contribution-license" className="scroll-mt-24">
<h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">6. CONTRIBUTION LICENSE</h2>
<p>
You and Services agree that we may access, store, process, and use any information and personal data that
you provide and your choices (including settings).
</p>
<p>
By submitting suggestions or other feedback regarding the Services, you agree that we can use and share
such feedback for any purpose without compensation to you.
</p>
<p>
We do not assert any ownership over your Contributions. You retain full ownership of all of your
Contributions and any intellectual property rights, or other proprietary rights associated with your
Contributions. We are not liable for any statements or representations in your Contributions provided by
you in any area on the Services. You are solely responsible for your Contributions to the Services and you
expressly agree to exonerate us from any and all responsibility and to refrain from any legal action
against us regarding your Contributions.
</p>
</section>
{/* 7. SERVICES MANAGEMENT */}
<section id="services-management" className="scroll-mt-24">
<h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">7. SERVICES MANAGEMENT</h2>
<p>
We reserve the right, but not the obligation, to: (1) monitor the Services for violations of these Legal
Terms; (2) take appropriate legal action against anyone who, in our sole discretion, violates the law or
these Legal Terms, including without limitation, reporting such user to law enforcement authorities; (3) in
our sole discretion and without limitation, refuse, restrict access to, limit the availability of, or
disable (to the extent technologically feasible) any of your Contributions or any portion thereof; (4) in
our sole discretion and without limitation, notice, or liability, to remove from the Services or otherwise
disable all files and content that are excessive in size or are in any way burdensome to our systems; and
(5) otherwise manage the Services in a manner designed to protect our rights and property and to
facilitate the proper functioning of the Services.
</p>
</section>
{/* 8. TERM AND TERMINATION */}
<section id="term-termination" className="scroll-mt-24">
<h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">8. TERM AND TERMINATION</h2>
<p>
These Legal Terms shall remain in full force and effect while you use the Services.{" "}
<strong>
WITHOUT LIMITING ANY OTHER PROVISION OF THESE LEGAL TERMS, WE RESERVE THE RIGHT TO, IN OUR SOLE
DISCRETION AND WITHOUT NOTICE OR LIABILITY, DENY ACCESS TO AND USE OF THE SERVICES (INCLUDING BLOCKING
CERTAIN IP ADDRESSES), TO ANY PERSON FOR ANY REASON OR FOR NO REASON, INCLUDING WITHOUT LIMITATION FOR
BREACH OF ANY REPRESENTATION, WARRANTY, OR COVENANT CONTAINED IN THESE LEGAL TERMS OR OF ANY APPLICABLE
LAW OR REGULATION. WE MAY TERMINATE YOUR USE OR PARTICIPATION IN THE SERVICES OR DELETE ANY CONTENT OR
INFORMATION THAT YOU POSTED AT ANY TIME, WITHOUT WARNING, IN OUR SOLE DISCRETION.
</strong>
</p>
<p>
If we terminate or suspend your account for any reason, you are prohibited from registering and creating a
new account under your name, a fake or borrowed name, or the name of any third party, even if you may be
acting on behalf of the third party. In addition to terminating or suspending your account, we reserve the
right to take appropriate legal action, including without limitation pursuing civil, criminal, and
injunctive redress.
</p>
</section>
{/* 9. MODIFICATIONS AND INTERRUPTIONS */}
<section id="modifications" className="scroll-mt-24">
<h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">9. MODIFICATIONS AND INTERRUPTIONS</h2>
<p>
We reserve the right to change, modify, or remove the contents of the Services at any time or for any
reason at our sole discretion without notice. However, we have no obligation to update any information on
our Services. We will not be liable to you or any third party for any modification, price change,
suspension, or discontinuance of the Services.
</p>
<p>
We cannot guarantee the Services will be available at all times. We may experience hardware, software, or
other problems or need to perform maintenance related to the Services, resulting in interruptions, delays,
or errors. We reserve the right to change, revise, update, suspend, discontinue, or otherwise modify the
Services at any time or for any reason without notice to you.
</p>
<p>
You agree that we have no liability whatsoever for any loss, damage, or inconvenience caused by your
inability to access or use the Services during any downtime or discontinuance of the Services. Nothing in
these Legal Terms will be construed to obligate us to maintain and support the Services or to supply any
corrections, updates, or releases in connection therewith.
</p>
</section>
{/* 10. GOVERNING LAW */}
<section id="governing-law" className="scroll-mt-24">
<h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">10. GOVERNING LAW</h2>
<p>
These Legal Terms shall be governed by and defined following the laws of Texas. LOAFers, Inc. and yourself
irrevocably consent that the courts of Houston shall have exclusive jurisdiction to resolve any dispute
which may arise in connection with these Legal Terms.
</p>
</section>
{/* 11. DISPUTE RESOLUTION */}
<section id="dispute-resolution" className="scroll-mt-24">
<h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">11. DISPUTE RESOLUTION</h2>
<h3 className="text-lg font-semibold ">Informal Negotiations</h3>
<p>
To expedite resolution and control the cost of any dispute, controversy, or claim related to these Legal
Terms (each a "Dispute" and collectively, the "Disputes") brought by either you or us (individually, a
"Party" and collectively, the "Parties"), the Parties agree to first attempt to negotiate any Dispute
(except those Disputes expressly provided below) informally for at least ___60_____ days before initiating
arbitration. Such informal negotiations commence upon written notice from one Party to the other Party.
</p>
<h3 className="text-lg font-semibold text-[var(--purple-deep)]">Binding Arbitration</h3>
<p>
Any dispute arising out of or in connection with these Legal Terms, including any question regarding its
existence, validity, or termination, shall be referred to and finally resolved by the Disputy Resolution
Center of Harris County{" "}
<a
className=""
href="https://drc.harriscountytx.gov/"
target="_blank"
rel="noreferrer"
>
https://drc.harriscountytx.gov/
</a>
</p>
<h3 className="text-lg font-semibold text-[var(--purple-deep)]">Restrictions</h3>
<p>
The Parties agree that any arbitration shall be limited to the Dispute between the Parties individually.
To the full extent permitted by law, (a) no arbitration shall be joined with any other proceeding; (b)
there is no right or authority for any Dispute to be arbitrated on a class-action basis or to utilize class
action procedures; and (c) there is no right or authority for any Dispute to be brought in a purported
representative capacity on behalf of the general public or any other persons.
</p>
<h3 className="text-lg font-semibold text-[var(--purple-deep)]">Exceptions to Informal Negotiations and Arbitration</h3>
<p>
The Parties agree that the following Disputes are not subject to the above provisions concerning informal
negotiations binding arbitration: (a) any Disputes seeking to enforce or protect, or concerning the validity
of, any of the intellectual property rights of a Party; (b) any Dispute related to, or arising from,
allegations of theft, piracy, invasion of privacy, or unauthorized use; and (c) any claim for injunctive
relief.
</p>
<p>
If this provision is found to be illegal or unenforceable, then neither Party will elect to arbitrate any
Dispute falling within that portion of this provision found to be illegal or unenforceable and such Dispute
shall be decided by a court of competent jurisdiction within the courts listed for jurisdiction above, and
the Parties agree to submit to the personal jurisdiction of that court.
</p>
</section>
{/* 12. CORRECTIONS */}
<section id="corrections" className="scroll-mt-24">
<h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">12. CORRECTIONS</h2>
<p>
There may be information on the Services that contains typographical errors, inaccuracies, or omissions,
including descriptions, pricing, availability, and various other information. We reserve the right to
correct any errors, inaccuracies, or omissions and to change or update the information on the Services at
any time, without prior notice.
</p>
</section>
{/* 13. DISCLAIMER */}
<section id="disclaimer" className="scroll-mt-24">
<h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">13. DISCLAIMER</h2>
<p className="font-semibold">
THE SERVICES ARE PROVIDED ON AN AS-IS AND AS-AVAILABLE BASIS. YOU AGREE THAT YOUR USE OF THE SERVICES WILL
BE AT YOUR SOLE RISK. TO THE FULLEST EXTENT PERMITTED BY LAW, WE DISCLAIM ALL WARRANTIES, EXPRESS OR
IMPLIED, IN CONNECTION WITH THE SERVICES AND YOUR USE THEREOF, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
</p>
<p className="font-semibold">
WE MAKE NO WARRANTIES OR REPRESENTATIONS ABOUT THE ACCURACY OR COMPLETENESS OF THE SERVICES' CONTENT OR THE
CONTENT OF ANY WEBSITES OR MOBILE APPLICATIONS LINKED TO THE SERVICES AND WE WILL ASSUME NO LIABILITY OR
RESPONSIBILITY FOR ANY (1) ERRORS, MISTAKES, OR INACCURACIES OF CONTENT AND MATERIALS, (2) PERSONAL INJURY
OR PROPERTY DAMAGE, OF ANY NATURE WHATSOEVER, RESULTING FROM YOUR ACCESS TO AND USE OF THE SERVICES, (3)
ANY UNAUTHORIZED ACCESS TO OR USE OF OUR SECURE SERVERS AND/OR ANY AND ALL PERSONAL INFORMATION AND/OR
FINANCIAL INFORMATION STORED THEREIN, (4) ANY INTERRUPTION OR CESSATION OF TRANSMISSION TO OR FROM THE
SERVICES, (5) ANY BUGS, VIRUSES, TROJAN HORSES, OR THE LIKE WHICH MAY BE TRANSMITTED TO OR THROUGH THE
SERVICES BY ANY THIRD PARTY, AND/OR (6) ANY ERRORS OR OMISSIONS IN ANY CONTENT AND MATERIALS OR FOR ANY LOSS
OR DAMAGE OF ANY KIND INCURRED AS A RESULT OF THE USE OF ANY CONTENT POSTED, TRANSMITTED, OR OTHERWISE MADE
AVAILABLE VIA THE SERVICES.
</p>
<p className="font-semibold">
WE DO NOT WARRANT, ENDORSE, GUARANTEE, OR ASSUME RESPONSIBILITY FOR ANY PRODUCT OR SERVICE ADVERTISED OR
OFFERED BY A THIRD PARTY THROUGH THE SERVICES, ANY HYPERLINKED WEBSITE, OR ANY WEBSITE OR MOBILE APPLICATION
FEATURED IN ANY BANNER OR OTHER ADVERTISING, AND WE WILL NOT BE A PARTY TO OR IN ANY WAY BE RESPONSIBLE FOR
MONITORING ANY TRANSACTION BETWEEN YOU AND ANY THIRD-PARTY PROVIDERS OF PRODUCTS OR SERVICES. AS WITH THE
PURCHASE OF A PRODUCT OR SERVICE THROUGH ANY MEDIUM OR IN ANY ENVIRONMENT, YOU SHOULD USE YOUR BEST
JUDGMENT AND EXERCISE CAUTION WHERE APPROPRIATE.
</p>
</section>
{/* 14. LIMITATIONS OF LIABILITY */}
<section id="limitations-liability" className="scroll-mt-24">
<h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">14. LIMITATIONS OF LIABILITY</h2>
<p className="font-semibold">
IN NO EVENT WILL WE OR OUR DIRECTORS, EMPLOYEES, OR AGENTS BE LIABLE TO YOU OR ANY THIRD PARTY FOR ANY
DIRECT, INDIRECT, CONSEQUENTIAL, EXEMPLARY, INCIDENTAL, SPECIAL, OR PUNITIVE DAMAGES, INCLUDING LOST
PROFIT, LOST REVENUE, LOSS OF DATA, OR OTHER DAMAGES ARISING FROM YOUR USE OF THE SERVICES, EVEN IF WE HAVE
BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
</p>
<p className="font-semibold">
NOTWITHSTANDING ANYTHING TO THE CONTRARY CONTAINED HEREIN, OUR LIABILITY TO YOU FOR ANY CAUSE WHATSOEVER
AND REGARDLESS OF THE FORM OF THE ACTION, WILL AT ALL TIMES BE LIMITED TO THE LESSER OF THE AMOUNT PAID, IF
ANY, BY YOU TO US OR .
</p>
<p className="font-semibold">
CERTAIN US STATE LAWS AND INTERNATIONAL LAWS DO NOT ALLOW LIMITATIONS ON IMPLIED WARRANTIES OR THE
EXCLUSION OR LIMITATION OF CERTAIN DAMAGES. IF THESE LAWS APPLY TO YOU, SOME OR ALL OF THE ABOVE
DISCLAIMERS OR LIMITATIONS MAY NOT APPLY TO YOU, AND YOU MAY HAVE ADDITIONAL RIGHTS.
</p>
</section>
{/* 15. INDEMNIFICATION */}
<section id="indemnification" className="scroll-mt-24">
<h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">15. INDEMNIFICATION</h2>
<p>
You agree to defend, indemnify, and hold us harmless, including our subsidiaries, affiliates, and all of
our respective officers, agents, partners, and employees, from and against any loss, damage, liability,
claim, or demand, including reasonable attorneys fees and expenses, made by any third party due to or
arising out of: (1) use of the Services; (2) breach of these Legal Terms; (3) any breach of your
representations and warranties set forth in these Legal Terms; (4) your violation of the rights of a third
party, including but not limited to intellectual property rights; or (5) any overt harmful act toward any
other user of the Services with whom you connected via the Services.
</p>
<p>
Notwithstanding the foregoing, we reserve the right, at your expense, to assume the exclusive defense and
control of any matter for which you are required to indemnify us, and you agree to cooperate, at your
expense, with our defense of such claims. We will use reasonable efforts to notify you of any such claim,
action, or proceeding which is subject to this indemnification upon becoming aware of it.
</p>
</section>
{/* 16. USER DATA */}
<section id="user-data" className="scroll-mt-24">
<h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">16. USER DATA</h2>
<p>
We will maintain certain data that you transmit to the Services for the purpose of managing the performance
of the Services, as well as data relating to your use of the Services. Although we perform regular routine
backups of data, you are solely responsible for all data that you transmit or that relates to any activity
you have undertaken using the Services. You agree that we shall have no liability to you for any loss or
corruption of any such data, and you hereby waive any right of action against us arising from any such loss
or corruption of such data.
</p>
</section>
{/* 17. ELECTRONIC COMMUNICATIONS */}
<section id="electronic-comms" className="scroll-mt-24">
<h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">
17. ELECTRONIC COMMUNICATIONS, TRANSACTIONS, AND SIGNATURES
</h2> </h2>
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p>
<p className="text-gray-700 leading-relaxed"> Visiting the Services, sending us emails, and completing online forms constitute electronic communications.
The Site may invite you to chat, contribute to, or participate in blogs, message boards, online forums, and other functionality, and may provide you with the opportunity to create, submit, post, display, transmit, perform, publish, distribute, or broadcast content and materials to us or on the Site. You consent to receive electronic communications, and you agree that all agreements, notices, disclosures,
and other communications we provide to you electronically, via email and on the Services, satisfy any legal
requirement that such communication be in writing.
</p>
<p className="font-semibold">
YOU HEREBY AGREE TO THE USE OF ELECTRONIC SIGNATURES, CONTRACTS, ORDERS, AND OTHER RECORDS, AND TO
ELECTRONIC DELIVERY OF NOTICES, POLICIES, AND RECORDS OF TRANSACTIONS INITIATED OR COMPLETED BY US OR VIA
THE SERVICES.
</p>
<p>
You hereby waive any rights or requirements under any statutes, regulations, rules, ordinances, or other
laws in any jurisdiction which require an original signature or delivery or retention of non-electronic
records, or to payments or the granting of credits by any means other than electronic means.
</p>
</section>
{/* 18. MISCELLANEOUS */}
<section id="miscellaneous" className="scroll-mt-24">
<h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">18. MISCELLANEOUS</h2>
<p>
These Legal Terms and any policies or operating rules posted by us on the Services or in respect to the
Services constitute the entire agreement and understanding between you and us. Our failure to exercise or
enforce any right or provision of these Legal Terms shall not operate as a waiver of such right or
provision.
</p>
<p>
These Legal Terms operate to the fullest extent permissible by law. We may assign any or all of our rights
and obligations to others at any time. We shall not be responsible or liable for any loss, damage, delay,
or failure to act caused by any cause beyond our reasonable control.
</p>
<p>
If any provision or part of a provision of these Legal Terms is determined to be unlawful, void, or
unenforceable, that provision or part of the provision is deemed severable from these Legal Terms and does
not affect the validity and enforceability of any remaining provisions.
</p>
<p>
There is no joint venture, partnership, employment or agency relationship created between you and us as a
result of these Legal Terms or use of the Services. You agree that these Legal Terms will not be construed
against us by virtue of having drafted them. You hereby waive any and all defenses you may have based on
the electronic form of these Legal Terms and the lack of signing by the parties hereto to execute these
Legal Terms.
</p>
</section>
{/* 19. CONTACT US */}
<section id="contact-us" className="scroll-mt-24">
<h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">19. CONTACT US</h2>
<p>
In order to resolve a complaint regarding the Services or to receive further information regarding use of
the Services, please contact us at:
</p>
<div className="not-prose mt-4 ">
<p className="font-semibold mb-2">LOAFers, Inc.</p>
<p className="">PO Box 7207</p>
<p className="">Houston, TX 77249</p>
<p className="">United States</p>
<p className="mt-3">
<a className="" href="mailto:info@loaftx.org">
info@loaftx.org
</a>
</p> </p>
</div> </div>
</section> </section>
{/* Section 6: Contribution License */}
<section>
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
style={{ fontFamily: "'Inter', sans-serif" }}>
6. Contribution License
</h2>
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<p className="text-gray-700 leading-relaxed">
By posting your Contributions to any part of the Site, you automatically grant, and you represent and warrant that you have the right to grant, to us an unrestricted, unlimited, irrevocable, perpetual, non-exclusive, transferable, royalty-free, fully-paid, worldwide right, and license to host, use, copy, reproduce, disclose, sell, resell, publish, broadcast, retitle, archive, store, cache, publicly perform, publicly display, reformat, translate, transmit, excerpt (in whole or in part), and distribute such Contributions.
</p>
</div>
</section>
{/* Section 7: Submissions */}
<section>
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
style={{ fontFamily: "'Inter', sans-serif" }}>
7. Submissions
</h2>
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<p className="text-gray-700 leading-relaxed">
You acknowledge and agree that any questions, comments, suggestions, ideas, feedback, or other information regarding the Site ("Submissions") provided by you to us are non-confidential and shall become our sole property.
</p>
</div>
</section>
{/* Section 8: Site Management */}
<section>
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
style={{ fontFamily: "'Inter', sans-serif" }}>
8. Site Management
</h2>
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<p className="text-gray-700 leading-relaxed">
We reserve the right, but not the obligation, to: (1) monitor the Site for violations of these Terms of Service; (2) take appropriate legal action against anyone who, in our sole discretion, violates the law or these Terms of Service; (3) refuse, restrict access to, limit the availability of, or disable (to the extent technologically feasible) any of your Contributions; (4) remove from the Site or otherwise disable all files and content that are excessive in size or are in any way burdensome to our systems.
</p>
</div>
</section>
{/* Section 9: Term and Termination */}
<section>
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
style={{ fontFamily: "'Inter', sans-serif" }}>
9. Term and Termination
</h2>
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<p className="text-gray-700 leading-relaxed">
These Terms of Service shall remain in full force and effect while you use the Site. Without limiting any other provision of these Terms of Service, we reserve the right to, in our sole discretion and without notice or liability, deny access to and use of the Site to any person for any reason or for no reason.
</p>
</div>
</section>
{/* Section 10: Modifications and Interruptions */}
<section>
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
style={{ fontFamily: "'Inter', sans-serif" }}>
10. Modifications and Interruptions
</h2>
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<p className="text-gray-700 leading-relaxed">
We reserve the right to change, modify, or remove the contents of the Site at any time or for any reason at our sole discretion without notice. We also reserve the right to modify or discontinue all or part of the Site without notice at any time.
</p>
</div>
</section>
{/* Section 11: Governing Law */}
<section>
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
style={{ fontFamily: "'Inter', sans-serif" }}>
11. Governing Law
</h2>
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<p className="text-gray-700 leading-relaxed">
These Terms of Service and your use of the Site are governed by and construed in accordance with the laws of the State of Texas applicable to agreements made and to be entirely performed within the State of Texas, without regard to its conflict of law principles.
</p>
</div>
</section>
{/* Section 12: Dispute Resolution */}
<section>
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
style={{ fontFamily: "'Inter', sans-serif" }}>
12. Dispute Resolution
</h2>
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<p className="text-gray-700 leading-relaxed">
Any legal action of whatever nature brought by either you or us shall be commenced or prosecuted in the state and federal courts located in Harris County, Texas, and the parties hereby consent to, and waive all defenses of lack of personal jurisdiction and forum non conveniens with respect to venue and jurisdiction in such state and federal courts.
</p>
</div>
</section>
{/* Section 13: Corrections */}
<section>
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
style={{ fontFamily: "'Inter', sans-serif" }}>
13. Corrections
</h2>
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<p className="text-gray-700 leading-relaxed">
There may be information on the Site that contains typographical errors, inaccuracies, or omissions that may relate to the Site, including descriptions, pricing, availability, and various other information. We reserve the right to correct any errors, inaccuracies, or omissions and to change or update the information on the Site at any time, without prior notice.
</p>
</div>
</section>
{/* Section 14: Disclaimer */}
<section>
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
style={{ fontFamily: "'Inter', sans-serif" }}>
14. Disclaimer
</h2>
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<p className="text-gray-700 leading-relaxed">
The Site is provided on an as-is and as-available basis. You agree that your use of the Site and our services will be at your sole risk. To the fullest extent permitted by law, we disclaim all warranties, express or implied, in connection with the Site and your use thereof.
</p>
</div>
</section>
{/* Section 15: Limitations of Liability */}
<section>
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
style={{ fontFamily: "'Inter', sans-serif" }}>
15. Limitations of Liability
</h2>
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<p className="text-gray-700 leading-relaxed">
In no event will we or our directors, employees, or agents be liable to you or any third party for any direct, indirect, consequential, exemplary, incidental, special, or punitive damages, including lost profit, lost revenue, loss of data, or other damages arising from your use of the Site.
</p>
</div>
</section>
{/* Section 16: Indemnification */}
<section>
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
style={{ fontFamily: "'Inter', sans-serif" }}>
16. Indemnification
</h2>
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<p className="text-gray-700 leading-relaxed">
You agree to defend, indemnify, and hold us harmless, including our subsidiaries, affiliates, and all of our respective officers, agents, partners, and employees, from and against any loss, damage, liability, claim, or demand, including reasonable attorneys' fees and expenses, made by any third party due to or arising out of your use of the Site or breach of these Terms of Service.
</p>
</div>
</section>
{/* Section 17: User Data */}
<section>
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
style={{ fontFamily: "'Inter', sans-serif" }}>
17. User Data
</h2>
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<p className="text-gray-700 leading-relaxed">
We will maintain certain data that you transmit to the Site for the purpose of managing the Site, as well as data relating to your use of the Site. Although we perform regular routine backups of data, you are solely responsible for all data that you transmit or that relates to any activity you have undertaken using the Site.
</p>
</div>
</section>
{/* Section 18: Electronic Communications */}
<section>
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
style={{ fontFamily: "'Inter', sans-serif" }}>
18. Electronic Communications, Transactions, and Signatures
</h2>
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<p className="text-gray-700 leading-relaxed">
Visiting the Site, sending us emails, and completing online forms constitute electronic communications. You consent to receive electronic communications, and you agree that all agreements, notices, disclosures, and other communications we provide to you electronically, via email and on the Site, satisfy any legal requirement that such communication be in writing.
</p>
</div>
</section>
{/* Section 19: Contact Us */}
<section>
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
style={{ fontFamily: "'Inter', sans-serif" }}>
19. Contact Us
</h2>
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<p className="text-gray-700 leading-relaxed">
In order to resolve a complaint regarding the Site or to receive further information regarding use of the Site, please contact us at:
</p>
<div className="bg-[#F8F7FB] p-4 rounded-lg border border-[#ddd8eb]">
<p className="font-semibold text-gray-800">LOAFers, Inc.</p>
<p className="text-gray-700">PO Box 7207</p>
<p className="text-gray-700">Houston, TX 77249</p>
<p className="text-gray-700 mt-2">
Email: <a href="mailto:info@loaftx.org" className="text-[#664fa3] hover:text-[#422268] font-semibold">info@loaftx.org</a>
</p>
</div> </div>
</div> </div>
</section>
</div>
{/* Back to Home Link */} {/* Back to Home Link */}
<div className="mt-8 text-center"> <div className="mt-8 text-center">
<Link to="/" className="text-[#664fa3] hover:text-[#422268] font-semibold transition-colors inline-flex items-center gap-2" <Link to="/" className="text-[var(--purple-lavender)] hover:text-[var(--purple-ink)] font-semibold transition-colors inline-flex items-center gap-2"
style={{ fontFamily: "'Nunito Sans', sans-serif" }}> style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
<span></span> Back to Home <span></span> Back to Home
</Link> </Link>
</div> </div>
</div> </main>
</div>
<PublicFooter /> <PublicFooter />
</> </>
); );
} }

View File

@@ -45,18 +45,18 @@ const VerifyEmail = () => {
}, [token]); }, [token]);
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-background">
<PublicNavbar /> <PublicNavbar />
<div className="max-w-2xl mx-auto px-6 py-20"> <div className="max-w-2xl mx-auto px-6 py-20">
<Card className="p-6 sm:p-8 md:p-12 bg-white rounded-2xl border border-[#ddd8eb] shadow-lg text-center"> <Card className="p-6 sm:p-8 md:p-12 bg-background rounded-2xl border border-[var(--neutral-800)] shadow-lg text-center">
{status === 'loading' && ( {status === 'loading' && (
<> <>
<Loader2 className="h-20 w-20 text-[#664fa3] mx-auto mb-6 animate-spin" /> <Loader2 className="h-20 w-20 text-brand-purple mx-auto mb-6 animate-spin" />
<h1 className="text-3xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h1 className="text-3xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Verifying Your Email... Verifying Your Email...
</h1> </h1>
<p className="text-lg text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Please wait while we verify your email address. Please wait while we verify your email address.
</p> </p>
</> </>
@@ -64,19 +64,19 @@ const VerifyEmail = () => {
{status === 'success' && ( {status === 'success' && (
<> <>
<CheckCircle className="h-20 w-20 text-[#81B29A] mx-auto mb-6" /> <CheckCircle className="h-20 w-20 text-[var(--green-light)] mx-auto mb-6" />
<h1 className="text-3xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h1 className="text-3xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Email Verified Successfully! Email Verified Successfully!
</h1> </h1>
<p className="text-lg text-[#664fa3] mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-lg text-brand-purple mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{message} {message}
</p> </p>
<p className="text-base text-[#664fa3] mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-base text-brand-purple mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Next steps: Attend an event and meet a board member within 90 days. Once you've attended an event, our admin team will review your application. Next steps: Attend an event and meet a board member within 90 days. Once you've attended an event, our admin team will review your application.
</p> </p>
<Link to="/login"> <Link to="/login">
<Button <Button
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-12 py-6 text-lg font-medium shadow-lg" className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background rounded-full px-12 py-6 text-lg font-medium shadow-lg"
data-testid="login-redirect-button" data-testid="login-redirect-button"
> >
Go to Login Go to Login
@@ -88,15 +88,15 @@ const VerifyEmail = () => {
{status === 'error' && ( {status === 'error' && (
<> <>
<XCircle className="h-20 w-20 text-red-500 mx-auto mb-6" /> <XCircle className="h-20 w-20 text-red-500 mx-auto mb-6" />
<h1 className="text-3xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> <h1 className="text-3xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Verification Failed Verification Failed
</h1> </h1>
<p className="text-lg text-[#664fa3] mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-lg text-brand-purple mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{message} {message}
</p> </p>
<Link to="/"> <Link to="/">
<Button <Button
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-12 py-6 text-lg font-medium shadow-lg" className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background rounded-full px-12 py-6 text-lg font-medium shadow-lg"
data-testid="home-redirect-button" data-testid="home-redirect-button"
> >
Go to Home Go to Home

Some files were not shown because too many files have changed in this diff Show More