Compare commits
3 Commits
dav-prod
...
2e62bec10e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e62bec10e | ||
|
|
727c2bc710 | ||
|
|
4ba44d8997 |
15
.crossnote/config.js
Normal file
15
.crossnote/config.js
Normal file
@@ -0,0 +1,15 @@
|
||||
({
|
||||
katexConfig: {
|
||||
"macros": {}
|
||||
},
|
||||
|
||||
mathjaxConfig: {
|
||||
"tex": {},
|
||||
"options": {},
|
||||
"loader": {}
|
||||
},
|
||||
|
||||
mermaidConfig: {
|
||||
"startOnLoad": false
|
||||
},
|
||||
})
|
||||
6
.crossnote/head.html
Normal file
6
.crossnote/head.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<!-- The content below will be included at the end of the <head> element. -->
|
||||
<script type="text/javascript">
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// your code here
|
||||
});
|
||||
</script>
|
||||
12
.crossnote/parser.js
Normal file
12
.crossnote/parser.js
Normal file
@@ -0,0 +1,12 @@
|
||||
({
|
||||
// Please visit the URL below for more information:
|
||||
// https://shd101wyy.github.io/markdown-preview-enhanced/#/extend-parser
|
||||
|
||||
onWillParseMarkdown: async function(markdown) {
|
||||
return markdown;
|
||||
},
|
||||
|
||||
onDidParseMarkdown: async function(html) {
|
||||
return html;
|
||||
},
|
||||
})
|
||||
8
.crossnote/style.less
Normal file
8
.crossnote/style.less
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
/* Please visit the URL below for more information: */
|
||||
/* https://shd101wyy.github.io/markdown-preview-enhanced/#/customize-css */
|
||||
|
||||
.markdown-preview.markdown-preview {
|
||||
// modify your style here
|
||||
// eg: background-color: blue;
|
||||
}
|
||||
28
README.md
28
README.md
@@ -442,7 +442,7 @@ import {
|
||||
SelectItem,
|
||||
} from "./components/ui/select";
|
||||
|
||||
<Button className="bg-var(--purple-lavender) text-white">Click me</Button>;
|
||||
<Button className="bg-muted-foreground foreground">Click me</Button>;
|
||||
```
|
||||
|
||||
### Admin Sidebar Features
|
||||
@@ -483,9 +483,9 @@ import {
|
||||
**Usage in Tailwind:**
|
||||
|
||||
```jsx
|
||||
<div className="bg-var(--purple-ink) text-white">
|
||||
<h1 className="text-var(--orange-light)">Accent Text</h1>
|
||||
<p className="text-var(--purple-lavender)">Secondary Text</p>
|
||||
<div className="bg-primary foreground">
|
||||
<h1 className="text-accent">Accent Text</h1>
|
||||
<p className="text-muted-foreground">Secondary Text</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
@@ -512,10 +512,10 @@ text-3xl → 1.875rem (30px)
|
||||
**Usage:**
|
||||
|
||||
```jsx
|
||||
<h1 className="text-3xl font-semibold text-var(--purple-ink)" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Page Title
|
||||
</h1>
|
||||
<p className="text-base text-var(--purple-lavender)" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-base text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Body text
|
||||
</p>
|
||||
```
|
||||
@@ -538,7 +538,7 @@ gap-2, gap-4, gap-6, gap-8 (same scale for flex/grid gaps)
|
||||
**Cards:**
|
||||
|
||||
```jsx
|
||||
<Card className="p-6 bg-background rounded-2xl border-2 border-var(--neutral-800)">
|
||||
<Card className="p-6 bg-background rounded-2xl border-2 border-muted">
|
||||
{/* Content */}
|
||||
</Card>
|
||||
```
|
||||
@@ -547,17 +547,17 @@ gap-2, gap-4, gap-6, gap-8 (same scale for flex/grid gaps)
|
||||
|
||||
```jsx
|
||||
// Primary
|
||||
<Button className="bg-var(--purple-lavender) text-white hover:bg-var(--purple-ink) rounded-full px-6 py-3">
|
||||
<Button className="bg-muted-foreground foreground hover:bg-primary rounded-full px-6 py-3">
|
||||
Primary Action
|
||||
</Button>
|
||||
|
||||
// Secondary
|
||||
<Button variant="outline" className="border-2 border-var(--neutral-800) text-var(--purple-lavender) hover:bg-var(--lavender-300) rounded-full">
|
||||
<Button variant="outline" className="border-2 border-muted text-muted-foreground hover:bg-muted rounded-full">
|
||||
Secondary Action
|
||||
</Button>
|
||||
|
||||
// Destructive
|
||||
<Button className="bg-red-600 text-white hover:bg-red-700 rounded-full">
|
||||
<Button className="bg-red-600 foreground hover:bg-red-700 rounded-full">
|
||||
Delete
|
||||
</Button>
|
||||
```
|
||||
@@ -565,10 +565,10 @@ gap-2, gap-4, gap-6, gap-8 (same scale for flex/grid gaps)
|
||||
**Form Inputs:**
|
||||
|
||||
```jsx
|
||||
<Input className="rounded-xl border-2 border-var(--neutral-800) focus:border-var(--purple-lavender)" />
|
||||
<Textarea className="rounded-xl border-2 border-var(--neutral-800) min-h-[120px]" />
|
||||
<Input className="rounded-xl border-2 border-muted focus:border-muted-foreground" />
|
||||
<Textarea className="rounded-xl border-2 border-muted min-h-[120px]" />
|
||||
<Select>
|
||||
<SelectTrigger className="rounded-xl border-2 border-var(--neutral-800)">
|
||||
<SelectTrigger className="rounded-xl border-2 border-muted">
|
||||
<SelectValue placeholder="Select..." />
|
||||
</SelectTrigger>
|
||||
</Select>
|
||||
@@ -611,7 +611,7 @@ import {
|
||||
HelpCircle, // Alerts
|
||||
} from "lucide-react";
|
||||
|
||||
<User className="h-5 w-5 text-var(--purple-lavender)" />;
|
||||
<User className="h-5 w-5 text-muted-foreground" />;
|
||||
```
|
||||
|
||||
### Responsive Design
|
||||
|
||||
32
src/App.css
Normal file
32
src/App.css
Normal file
@@ -0,0 +1,32 @@
|
||||
* {
|
||||
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%);
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import AdminUserView from './pages/admin/AdminUserView';
|
||||
import AdminStaff from './pages/admin/AdminStaff';
|
||||
import AdminMembers from './pages/admin/AdminMembers';
|
||||
import AdminPermissions from './pages/admin/AdminPermissions';
|
||||
import AdminSettings from './pages/admin/AdminSettings';
|
||||
import AdminRoles from './pages/admin/AdminRoles';
|
||||
import AdminEvents from './pages/admin/AdminEvents';
|
||||
import AdminEventAttendance from './pages/admin/AdminEventAttendance';
|
||||
@@ -291,13 +290,6 @@ function App() {
|
||||
</AdminLayout>
|
||||
</PrivateRoute>
|
||||
} />
|
||||
<Route path="/admin/settings" element={
|
||||
<PrivateRoute adminOnly>
|
||||
<AdminLayout>
|
||||
<AdminSettings />
|
||||
</AdminLayout>
|
||||
</PrivateRoute>
|
||||
} />
|
||||
|
||||
{/* 404 - Catch all undefined routes */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
|
||||
@@ -117,7 +117,7 @@ export default function AddToCalendarButton({
|
||||
return (
|
||||
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<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">
|
||||
<Button variant={variant} size={size} className="gap-2">
|
||||
<Calendar className="h-4 w-4" />
|
||||
Add to Calendar
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
@@ -128,7 +128,7 @@ export default function AddToCalendarButton({
|
||||
{event && (
|
||||
<>
|
||||
{/* Single Event Export Options */}
|
||||
<div className="px-2 py-1.5 text-sm font-semibold text-[var(--purple-ink)]">
|
||||
<div className="px-2 py-1.5 text-sm font-semibold text-primary">
|
||||
Add This Event
|
||||
</div>
|
||||
|
||||
@@ -177,7 +177,7 @@ export default function AddToCalendarButton({
|
||||
{showSubscribe && (
|
||||
<>
|
||||
{/* Subscription Options */}
|
||||
<div className="px-2 py-1.5 text-sm font-semibold text-[var(--purple-ink)]">
|
||||
<div className="px-2 py-1.5 text-sm font-semibold text-primary">
|
||||
Calendar Feeds
|
||||
</div>
|
||||
|
||||
@@ -187,7 +187,7 @@ export default function AddToCalendarButton({
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
Subscribe to My Events
|
||||
<div className="text-xs text-brand-purple mt-0.5">
|
||||
<div className="text-xs text-muted-foreground mt-0.5">
|
||||
Auto-syncs your RSVP'd events
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
@@ -198,7 +198,7 @@ export default function AddToCalendarButton({
|
||||
>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Download All Events
|
||||
<div className="text-xs text-brand-purple mt-0.5">
|
||||
<div className="text-xs text-muted-foreground mt-0.5">
|
||||
One-time import of all upcoming events
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
@@ -206,7 +206,7 @@ export default function AddToCalendarButton({
|
||||
)}
|
||||
|
||||
{!event && !showSubscribe && (
|
||||
<div className="px-2 py-6 text-center text-sm text-brand-purple ">
|
||||
<div className="px-2 py-6 text-center text-sm text-muted-foreground">
|
||||
No event selected
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -175,28 +175,17 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
||||
path: '/admin/permissions',
|
||||
disabled: false,
|
||||
superadminOnly: true
|
||||
},
|
||||
{
|
||||
name: 'Settings',
|
||||
icon: Settings,
|
||||
path: '/admin/settings',
|
||||
disabled: false,
|
||||
superadminOnly: true
|
||||
}
|
||||
];
|
||||
|
||||
// Filter nav items based on user role
|
||||
const filteredNavItems = navItems.filter(item => {
|
||||
if (item.superadminOnly && user?.role !== 'superadmin') {
|
||||
console.log('Filtering out superadmin-only item:', item.name, 'User role:', user?.role);
|
||||
return false;
|
||||
}
|
||||
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) => {
|
||||
if (path === '/admin') {
|
||||
return location.pathname === '/admin';
|
||||
@@ -222,10 +211,10 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
||||
className={`
|
||||
flex items-center gap-3 px-4 py-3 rounded-lg transition-all relative
|
||||
${item.disabled
|
||||
? 'opacity-50 cursor-not-allowed text-brand-purple '
|
||||
? 'opacity-50 cursor-not-allowed text-muted-foreground'
|
||||
: active
|
||||
? 'bg-[var(--orange-light)]/10 text-[var(--purple-ink)]'
|
||||
: 'text-[var(--purple-ink)] hover:bg-[var(--neutral-800)]/20'
|
||||
? 'bg-accent/10 text-accent'
|
||||
: 'text-primary hover:bg-chart-6/20'
|
||||
}
|
||||
`}
|
||||
>
|
||||
@@ -240,7 +229,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
||||
<>
|
||||
<span className="flex-1">{item.name}</span>
|
||||
{item.disabled && (
|
||||
<Badge className="bg-[var(--neutral-800)] text-[var(--purple-ink)] text-xs px-2 py-0.5">
|
||||
<Badge className="bg-chart-6 text-primary text-xs px-2 py-0.5">
|
||||
Soon
|
||||
</Badge>
|
||||
)}
|
||||
@@ -254,7 +243,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
||||
|
||||
{/* Badge when collapsed */}
|
||||
{!isOpen && item.badge > 0 && !item.disabled && (
|
||||
<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">
|
||||
<div className="absolute -top-1 -right-1 bg-accent foreground text-xs rounded-full h-5 w-5 flex items-center justify-center font-medium">
|
||||
{item.badge}
|
||||
</div>
|
||||
)}
|
||||
@@ -276,7 +265,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
||||
{/* Sidebar */}
|
||||
<aside
|
||||
className={`
|
||||
bg-background border-r border-[var(--neutral-800)] transition-all duration-300 ease-out
|
||||
bg-background border-r border-chart-6 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'}
|
||||
@@ -284,7 +273,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
||||
`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-[var(--neutral-800)]">
|
||||
<div className="flex items-center justify-between p-4 border-b border-chart-6">
|
||||
<Link to="/" className="flex items-center gap-3 group flex-1 min-w-0">
|
||||
<img
|
||||
src={`${process.env.PUBLIC_URL}/loaf-logo.png`}
|
||||
@@ -294,7 +283,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
||||
/>
|
||||
{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" }}>
|
||||
<h2 className="text-xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Admin
|
||||
</h2>
|
||||
</div>
|
||||
@@ -302,7 +291,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
||||
</Link>
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="p-2 rounded-lg hover:bg-[var(--neutral-800)]/20 transition-colors"
|
||||
className="p-2 rounded-lg hover:bg-chart-6/20 transition-colors"
|
||||
aria-label={isOpen ? 'Collapse sidebar' : 'Expand sidebar'}
|
||||
>
|
||||
{isMobile ? (
|
||||
@@ -375,34 +364,24 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
||||
{renderNavItem(filteredNavItems.find(item => item.name === 'Bylaws'))}
|
||||
</div>
|
||||
|
||||
{/* SYSTEM Section - Superadmin only */}
|
||||
{/* Permissions - Superadmin only (no header) */}
|
||||
{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">
|
||||
<div className="mt-6">
|
||||
{renderNavItem(filteredNavItems.find(item => item.name === 'Permissions'))}
|
||||
{renderNavItem(filteredNavItems.find(item => item.name === 'Settings'))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{/* User Section */}
|
||||
<div className="border-t border-[var(--neutral-800)] p-4 space-y-2">
|
||||
<div className="border-t border-chart-6 p-4 space-y-2">
|
||||
{isOpen && user && (
|
||||
<div className="px-4 py-3 mb-2 flex justify-between items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 rounded-full bg-[var(--neutral-800)] flex items-center justify-center text-[var(--purple-ink)] font-semibold">
|
||||
<div className="h-10 w-10 rounded-full bg-chart-6 flex items-center justify-center text-primary font-semibold">
|
||||
{user.first_name?.[0]}{user.last_name?.[0]}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-primary dark:text-brand-light-lavender truncate" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="text-sm font-medium text-primary truncate" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{user.first_name} {user.last_name}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground capitalize truncate" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
@@ -410,7 +389,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Link className='dark:text-brand-lavender ' to='/profile'><Settings size={16} />
|
||||
<Link to='/profile' className='text-foreground'><Settings size={16} />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
@@ -424,7 +403,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
||||
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
|
||||
text-primary hover:bg-muted/20 transition-colors
|
||||
${!isOpen && 'justify-center'}
|
||||
`}
|
||||
>
|
||||
@@ -445,16 +424,16 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
||||
{/* Storage Usage Widget */}
|
||||
<div className="mb-2">
|
||||
{isOpen ? (
|
||||
<div className="px-4 py-3 bg-[var(--lavender-500)] rounded-lg">
|
||||
<div className="px-4 py-3 bg-chart-7 rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-primary dark:text-brand-light-lavender ">Storage Usage</span>
|
||||
<span className="text-sm font-medium text-primary">Storage Usage</span>
|
||||
<span className="text-xs text-muted-foreground">{storagePercentage}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-[var(--neutral-800)] rounded-full h-2">
|
||||
<div className="w-full bg-chart-6 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all ${storagePercentage > 90 ? 'bg-red-500' :
|
||||
storagePercentage > 75 ? 'bg-yellow-500' :
|
||||
'bg-[var(--green-light)]'
|
||||
'bg-success'
|
||||
}`}
|
||||
style={{ width: `${storagePercentage}%` }}
|
||||
/>
|
||||
|
||||
@@ -55,21 +55,21 @@ export const AttendanceDialog = ({ event, open, onOpenChange, onSuccess }) => {
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto bg-background scrollbar-dashboard">
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto bg-background">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<DialogTitle className="text-2xl text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Mark Attendance: {event?.title}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 mt-4">
|
||||
{rsvps.length === 0 ? (
|
||||
<p className="text-center text-brand-purple py-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>No RSVPs yet</p>
|
||||
<p className="text-center text-muted-foreground py-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>No RSVPs yet</p>
|
||||
) : (
|
||||
rsvps.map((rsvp) => (
|
||||
<div
|
||||
key={rsvp.user_id}
|
||||
className="flex items-center gap-3 p-4 border-2 border-[var(--neutral-800)] rounded-xl hover:border-brand-purple transition-colors"
|
||||
className="flex items-center gap-3 p-4 border-2 border-chart-6 rounded-xl hover:border-muted-foreground transition-colors"
|
||||
>
|
||||
<Checkbox
|
||||
checked={attendance[rsvp.user_id] || false}
|
||||
@@ -79,11 +79,11 @@ export const AttendanceDialog = ({ event, open, onOpenChange, onSuccess }) => {
|
||||
className="w-5 h-5"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>{rsvp.user_name}</p>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{rsvp.user_email}</p>
|
||||
<p className="font-medium text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>{rsvp.user_name}</p>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{rsvp.user_email}</p>
|
||||
</div>
|
||||
{rsvp.attended && (
|
||||
<span className="text-sm text-[var(--green-light)] font-medium">
|
||||
<span className="text-sm text-success font-medium">
|
||||
✓ Attended
|
||||
</span>
|
||||
)}
|
||||
@@ -96,14 +96,14 @@ export const AttendanceDialog = ({ event, open, onOpenChange, onSuccess }) => {
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={loading}
|
||||
className="flex-1 bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background rounded-full"
|
||||
className="flex-1 bg-chart-6 text-primary hover:bg-background rounded-full"
|
||||
>
|
||||
{loading ? 'Saving...' : 'Save Attendance'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => onOpenChange(false)}
|
||||
variant="outline"
|
||||
className="flex-1 border-2 border-[var(--neutral-800)] text-brand-purple hover:bg-background hover:text-[var(--purple-ink)] rounded-full"
|
||||
className="flex-1 border-2 border-chart-6 text-muted-foreground hover:bg-background hover:text-primary rounded-full"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
@@ -69,14 +69,14 @@ const ChangePasswordDialog = ({ open, onOpenChange }) => {
|
||||
<DialogContent className="sm:max-w-md bg-background">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<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-[var(--orange-light)]" />
|
||||
<div className="inline-flex items-center justify-center w-10 h-10 rounded-full bg-muted">
|
||||
<Lock className="h-5 w-5 text-accent" />
|
||||
</div>
|
||||
<DialogTitle className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<DialogTitle className="text-2xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Change Password
|
||||
</DialogTitle>
|
||||
</div>
|
||||
<DialogDescription className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<DialogDescription className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Update your password to keep your account secure.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
@@ -92,7 +92,7 @@ const ChangePasswordDialog = ({ open, onOpenChange }) => {
|
||||
value={formData.currentPassword}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter current password"
|
||||
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-12 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -106,7 +106,7 @@ const ChangePasswordDialog = ({ open, onOpenChange }) => {
|
||||
value={formData.newPassword}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter new password (min. 6 characters)"
|
||||
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-12 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -120,22 +120,23 @@ const ChangePasswordDialog = ({ open, onOpenChange }) => {
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Re-enter new password"
|
||||
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-12 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="mt-6">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="btn-outline mr-33"
|
||||
className="rounded-full px-6"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className=" btn-primary"
|
||||
className="bg-chart-6 text-primary hover:bg-background rounded-full px-6 disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Changing...' : 'Change Password'}
|
||||
</Button>
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
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]">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -38,8 +38,8 @@ const ConfirmationDialog = ({
|
||||
const variants = {
|
||||
warning: {
|
||||
icon: AlertTriangle,
|
||||
iconColor: 'text-[var(--orange-light)]',
|
||||
confirmButtonClass: 'bg-[var(--orange-light)] text-white hover:bg-[var(--orange-sand)] rounded-full px-6',
|
||||
iconColor: 'text-accent',
|
||||
confirmButtonClass: 'bg-accent text-white hover:bg-tangerine-dream rounded-full px-6',
|
||||
},
|
||||
danger: {
|
||||
icon: AlertTriangle,
|
||||
@@ -48,13 +48,13 @@ const ConfirmationDialog = ({
|
||||
},
|
||||
info: {
|
||||
icon: Info,
|
||||
iconColor: 'text-brand-purple ',
|
||||
confirmButtonClass: 'bg-brand-purple text-white hover:bg-[var(--purple-plum)] rounded-full px-6',
|
||||
iconColor: 'text-muted-foreground',
|
||||
confirmButtonClass: 'bg-muted-foreground text-white hover:bg-rebecca-purple rounded-full px-6',
|
||||
},
|
||||
success: {
|
||||
icon: CheckCircle,
|
||||
iconColor: 'text-[var(--green-light)]',
|
||||
confirmButtonClass: 'bg-[var(--green-light)] text-white hover:bg-[var(--green-pastel)] rounded-full px-6',
|
||||
iconColor: 'text-success',
|
||||
confirmButtonClass: 'bg-success text-white hover:bg-muted-teal-base rounded-full px-6',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -63,21 +63,21 @@ const ConfirmationDialog = ({
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent className="bg-background rounded-2xl border border-[var(--neutral-800)] p-0 overflow-hidden max-w-md">
|
||||
<AlertDialogContent className="bg-background rounded-2xl border border-chart-6 p-0 overflow-hidden max-w-md">
|
||||
<AlertDialogHeader className="p-6 pb-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`p-3 rounded-full bg-[var(--lavender-500)] ${config.iconColor}`}>
|
||||
<div className={`p-3 rounded-full bg-chart-7 ${config.iconColor}`}>
|
||||
<Icon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<AlertDialogTitle
|
||||
className="text-xl font-semibold text-[var(--purple-ink)] mb-2"
|
||||
className="text-xl font-semibold text-primary mb-2"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
{title}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription
|
||||
className="text-brand-purple text-sm leading-relaxed"
|
||||
className="text-muted-foreground text-sm leading-relaxed"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
>
|
||||
{description}
|
||||
@@ -85,9 +85,9 @@ const ConfirmationDialog = ({
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter className="p-6 pt-4 bg-[var(--lavender-500)] flex-row gap-3 justify-end">
|
||||
<AlertDialogFooter className="p-6 pt-4 bg-chart-7 flex-row gap-3 justify-end">
|
||||
<AlertDialogCancel
|
||||
className="border-2 border-[var(--neutral-800)] text-brand-purple hover:bg-background rounded-full px-6"
|
||||
className="border-2 border-chart-6 text-muted-foreground hover:bg-background rounded-full px-6"
|
||||
disabled={loading}
|
||||
>
|
||||
{cancelText}
|
||||
|
||||
@@ -31,7 +31,6 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errors, setErrors] = useState({});
|
||||
const getTodayDate = () => new Date().toISOString().slice(0, 10);
|
||||
|
||||
const handleChange = (field, value) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
@@ -85,8 +84,8 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
if (payload.date_of_birth === '') {
|
||||
delete payload.date_of_birth;
|
||||
}
|
||||
if (!payload.member_since) {
|
||||
payload.member_since = getTodayDate();
|
||||
if (payload.member_since === '') {
|
||||
delete payload.member_since;
|
||||
}
|
||||
|
||||
await api.post('/admin/users/create', payload);
|
||||
@@ -120,13 +119,13 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[700px] rounded-2xl max-h-[90vh] overflow-y-auto scrollbar-dashboard">
|
||||
<DialogContent className="sm:max-w-[700px] rounded-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<DialogTitle className="text-2xl text-primary flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<UserPlus className="h-6 w-6" />
|
||||
Create Member
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<DialogDescription className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Create a new member account with direct login access. Member will be created immediately.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
@@ -136,7 +135,7 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
{/* Email & Password Row */}
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email" className="text-[var(--purple-ink)]">
|
||||
<Label htmlFor="email" className="text-primary">
|
||||
Email <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
@@ -144,7 +143,7 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
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 "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="member@example.com"
|
||||
/>
|
||||
{errors.email && (
|
||||
@@ -153,7 +152,7 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password" className="text-[var(--purple-ink)]">
|
||||
<Label htmlFor="password" className="text-primary">
|
||||
Password <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
@@ -161,7 +160,7 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
type="password"
|
||||
value={formData.password}
|
||||
onChange={(e) => handleChange('password', e.target.value)}
|
||||
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="Minimum 8 characters"
|
||||
/>
|
||||
{errors.password && (
|
||||
@@ -173,14 +172,14 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
{/* Name Row */}
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="first_name" className="text-[var(--purple-ink)]">
|
||||
<Label htmlFor="first_name" className="text-primary">
|
||||
First Name <span className="text-red-500">*</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 "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="John"
|
||||
/>
|
||||
{errors.first_name && (
|
||||
@@ -189,14 +188,14 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="last_name" className="text-[var(--purple-ink)]">
|
||||
<Label htmlFor="last_name" className="text-primary">
|
||||
Last Name <span className="text-red-500">*</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 "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="Doe"
|
||||
/>
|
||||
{errors.last_name && (
|
||||
@@ -207,7 +206,7 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
|
||||
{/* Phone */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="phone" className="text-[var(--purple-ink)]">
|
||||
<Label htmlFor="phone" className="text-primary">
|
||||
Phone <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
@@ -215,7 +214,7 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
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 "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="(555) 123-4567"
|
||||
/>
|
||||
{errors.phone && (
|
||||
@@ -225,14 +224,14 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
|
||||
{/* Address */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="address" className="text-[var(--purple-ink)]">
|
||||
<Label htmlFor="address" className="text-primary">
|
||||
Address
|
||||
</Label>
|
||||
<Input
|
||||
id="address"
|
||||
value={formData.address}
|
||||
onChange={(e) => handleChange('address', e.target.value)}
|
||||
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="123 Main St"
|
||||
/>
|
||||
</div>
|
||||
@@ -240,35 +239,35 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
{/* City, State, Zipcode Row */}
|
||||
<div className="grid md:grid-cols-3 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="city" className="text-[var(--purple-ink)]">City</Label>
|
||||
<Label htmlFor="city" className="text-primary">City</Label>
|
||||
<Input
|
||||
id="city"
|
||||
value={formData.city}
|
||||
onChange={(e) => handleChange('city', e.target.value)}
|
||||
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="San Francisco"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="state" className="text-[var(--purple-ink)]">State</Label>
|
||||
<Label htmlFor="state" className="text-primary">State</Label>
|
||||
<Input
|
||||
id="state"
|
||||
value={formData.state}
|
||||
onChange={(e) => handleChange('state', e.target.value)}
|
||||
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="CA"
|
||||
maxLength={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="zipcode" className="text-[var(--purple-ink)]">Zipcode</Label>
|
||||
<Label htmlFor="zipcode" className="text-primary">Zipcode</Label>
|
||||
<Input
|
||||
id="zipcode"
|
||||
value={formData.zipcode}
|
||||
onChange={(e) => handleChange('zipcode', e.target.value)}
|
||||
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="94102"
|
||||
/>
|
||||
</div>
|
||||
@@ -277,24 +276,24 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
{/* Dates Row */}
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="date_of_birth" className="text-[var(--purple-ink)]">Date of Birth</Label>
|
||||
<Label htmlFor="date_of_birth" className="text-primary">Date of Birth</Label>
|
||||
<Input
|
||||
id="date_of_birth"
|
||||
type="date"
|
||||
value={formData.date_of_birth}
|
||||
onChange={(e) => handleChange('date_of_birth', e.target.value)}
|
||||
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="member_since" className="text-[var(--purple-ink)]">Member Since</Label>
|
||||
<Label htmlFor="member_since" className="text-primary">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 "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -312,7 +311,7 @@ const CreateMemberDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="rounded-xl bg-[var(--green-light)] hover:bg-[var(--green-fern)] text-white"
|
||||
className="rounded-xl bg-success hover:bg-muted-teal-balanced text-white"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
|
||||
@@ -22,12 +22,10 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
phone: '',
|
||||
member_since: '',
|
||||
role: 'admin'
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errors, setErrors] = useState({});
|
||||
const getTodayDate = () => new Date().toISOString().slice(0, 10);
|
||||
|
||||
const handleChange = (field, value) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
@@ -76,11 +74,7 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const payload = { ...formData };
|
||||
if (!payload.member_since) {
|
||||
payload.member_since = getTodayDate();
|
||||
}
|
||||
await api.post('/admin/users/create', payload);
|
||||
await api.post('/admin/users/create', formData);
|
||||
toast.success('Staff member created successfully');
|
||||
|
||||
// Reset form
|
||||
@@ -90,7 +84,6 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
phone: '',
|
||||
member_since: '',
|
||||
role: 'admin'
|
||||
});
|
||||
|
||||
@@ -108,11 +101,11 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[600px] rounded-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<DialogTitle className="text-2xl text-primary flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<UserPlus className="h-6 w-6" />
|
||||
Create Staff Member
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<DialogDescription className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Create a new staff account with direct login access. User will be created immediately.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
@@ -121,7 +114,7 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
<div className="grid gap-6 py-4">
|
||||
{/* Email */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email" className="text-[var(--purple-ink)]">
|
||||
<Label htmlFor="email" className="text-primary">
|
||||
Email <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
@@ -129,7 +122,7 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
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 "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="staff@example.com"
|
||||
/>
|
||||
{errors.email && (
|
||||
@@ -139,7 +132,7 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
|
||||
{/* Password */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password" className="text-[var(--purple-ink)]">
|
||||
<Label htmlFor="password" className="text-primary">
|
||||
Password <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
@@ -147,7 +140,7 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
type="password"
|
||||
value={formData.password}
|
||||
onChange={(e) => handleChange('password', e.target.value)}
|
||||
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="Minimum 8 characters"
|
||||
/>
|
||||
{errors.password && (
|
||||
@@ -157,14 +150,14 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
|
||||
{/* First Name */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="first_name" className="text-[var(--purple-ink)]">
|
||||
<Label htmlFor="first_name" className="text-primary">
|
||||
First Name <span className="text-red-500">*</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 "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="John"
|
||||
/>
|
||||
{errors.first_name && (
|
||||
@@ -174,14 +167,14 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
|
||||
{/* Last Name */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="last_name" className="text-[var(--purple-ink)]">
|
||||
<Label htmlFor="last_name" className="text-primary">
|
||||
Last Name <span className="text-red-500">*</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 "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="Doe"
|
||||
/>
|
||||
{errors.last_name && (
|
||||
@@ -191,7 +184,7 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
|
||||
{/* Phone */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="phone" className="text-[var(--purple-ink)]">
|
||||
<Label htmlFor="phone" className="text-primary">
|
||||
Phone <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
@@ -199,7 +192,7 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
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 "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="(555) 123-4567"
|
||||
/>
|
||||
{errors.phone && (
|
||||
@@ -207,27 +200,13 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
)}
|
||||
</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 */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="role" className="text-[var(--purple-ink)]">
|
||||
<Label htmlFor="role" className="text-primary">
|
||||
Role <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Select value={formData.role} onValueChange={(value) => handleChange('role', value)}>
|
||||
<SelectTrigger className="rounded-xl border-2 border-[var(--neutral-800)]">
|
||||
<SelectTrigger className="rounded-xl border-2 border-chart-6">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -250,7 +229,7 @@ const CreateStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="rounded-xl bg-[var(--green-light)] hover:bg-[var(--green-fern)] text-white"
|
||||
className="rounded-xl bg-success hover:bg-muted-teal-balanced text-white"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
|
||||
@@ -138,13 +138,13 @@ const ImportMembersDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-[800px] rounded-2xl max-h-[90vh] overflow-y-auto scrollbar-dashboard">
|
||||
<DialogContent className="sm:max-w-[800px] rounded-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<DialogTitle className="text-2xl text-primary flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Upload className="h-6 w-6" />
|
||||
{importResult ? 'Import Results' : 'Import Members from CSV'}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<DialogDescription className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{importResult
|
||||
? 'Review the import results below'
|
||||
: '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
|
||||
<div className="grid gap-6 py-4">
|
||||
{/* CSV Format Instructions */}
|
||||
<Alert className="border-brand-purple bg-[var(--lavender-700)]">
|
||||
<AlertDescription className="text-sm text-[var(--purple-ink)]">
|
||||
<Alert className="border-muted-foreground bg-bright-snow">
|
||||
<AlertDescription className="text-sm text-primary">
|
||||
<strong>Required columns:</strong> Email, First Name, Last Name, Phone, Role
|
||||
<br />
|
||||
<strong>Optional columns:</strong> Status, Address, City, State, Zipcode, Date of Birth, Member Since
|
||||
@@ -168,8 +168,8 @@ const ImportMembersDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
{/* File Upload Area */}
|
||||
<div
|
||||
className={`border-2 border-dashed rounded-2xl p-12 text-center transition-colors ${dragActive
|
||||
? 'border-brand-purple bg-[var(--lavender-700)]'
|
||||
: 'border-[var(--neutral-800)] hover:border-brand-purple hover:bg-[var(--lavender-700)]'
|
||||
? 'border-muted-foreground bg-bright-snow'
|
||||
: 'border-chart-6 hover:border-muted-foreground hover:bg-bright-snow'
|
||||
}`}
|
||||
onDragEnter={handleDrag}
|
||||
onDragLeave={handleDrag}
|
||||
@@ -178,12 +178,12 @@ const ImportMembersDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
>
|
||||
{file ? (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<FileUp className="h-16 w-16 text-[var(--green-light)]" />
|
||||
<FileUp className="h-16 w-16 text-success" />
|
||||
<div>
|
||||
<p className="text-lg font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="text-lg font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{file.name}
|
||||
</p>
|
||||
<p className="text-sm text-brand-purple ">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{(file.size / 1024).toFixed(2)} KB
|
||||
</p>
|
||||
</div>
|
||||
@@ -198,12 +198,12 @@ const ImportMembersDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<Upload className="h-16 w-16 text-[var(--neutral-800)]" />
|
||||
<Upload className="h-16 w-16 text-chart-6" />
|
||||
<div>
|
||||
<p className="text-lg font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="text-lg font-semibold text-primary mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Drag and drop your CSV file here
|
||||
</p>
|
||||
<p className="text-sm text-brand-purple mb-4">or</p>
|
||||
<p className="text-sm text-muted-foreground mb-4">or</p>
|
||||
<Label htmlFor="file-upload">
|
||||
<Button variant="outline" className="rounded-xl cursor-pointer" asChild>
|
||||
<span>Browse Files</span>
|
||||
@@ -222,14 +222,14 @@ const ImportMembersDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
</div>
|
||||
|
||||
{/* Options */}
|
||||
<div className="flex items-center gap-3 p-4 bg-[var(--lavender-700)] rounded-xl">
|
||||
<div className="flex items-center gap-3 p-4 bg-bright-snow rounded-xl">
|
||||
<Checkbox
|
||||
checked={updateExisting}
|
||||
onCheckedChange={setUpdateExisting}
|
||||
id="update-existing"
|
||||
className="h-5 w-5 border-2 border-brand-purple data-[state=checked]:bg-brand-purple "
|
||||
className="h-5 w-5 border-2 border-muted-foreground data-[state=checked]:bg-muted-foreground"
|
||||
/>
|
||||
<Label htmlFor="update-existing" className="text-[var(--purple-ink)] cursor-pointer">
|
||||
<Label htmlFor="update-existing" className="text-primary cursor-pointer">
|
||||
Update existing members (if email already exists)
|
||||
</Label>
|
||||
</div>
|
||||
@@ -239,9 +239,9 @@ const ImportMembersDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
<div className="grid gap-6 py-4">
|
||||
{/* Summary Cards */}
|
||||
<div className="grid md:grid-cols-4 gap-4">
|
||||
<div className="p-4 bg-background rounded-xl border border-[var(--neutral-800)] text-center">
|
||||
<p className="text-sm text-brand-purple mb-1">Total Rows</p>
|
||||
<p className="text-2xl font-semibold text-[var(--purple-ink)]">{importResult.total_rows}</p>
|
||||
<div className="p-4 bg-background rounded-xl border border-chart-6 text-center">
|
||||
<p className="text-sm text-muted-foreground mb-1">Total Rows</p>
|
||||
<p className="text-2xl font-semibold text-primary">{importResult.total_rows}</p>
|
||||
</div>
|
||||
<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>
|
||||
@@ -251,7 +251,7 @@ const ImportMembersDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
<p className="text-sm text-red-700 mb-1">Failed</p>
|
||||
<p className="text-2xl font-semibold text-red-600">{importResult.failed_rows}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-background rounded-xl border border-[var(--neutral-800)] flex items-center justify-center gap-2">
|
||||
<div className="p-4 bg-background rounded-xl border border-chart-6 flex items-center justify-center gap-2">
|
||||
{getStatusIcon(importResult.status)}
|
||||
{getStatusBadge(importResult.status)}
|
||||
</div>
|
||||
@@ -260,23 +260,23 @@ const ImportMembersDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
{/* Errors Table */}
|
||||
{importResult.errors && importResult.errors.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-lg font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Errors ({importResult.errors.length} {importResult.errors.length === 10 ? '- showing first 10' : ''})
|
||||
</h3>
|
||||
<div className="border border-[var(--neutral-800)] rounded-xl overflow-hidden">
|
||||
<div className="border border-chart-6 rounded-xl overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-[var(--neutral-800)] hover:bg-[var(--neutral-800)]">
|
||||
<TableHead className="text-[var(--purple-ink)] font-semibold">Row</TableHead>
|
||||
<TableHead className="text-[var(--purple-ink)] font-semibold">Email</TableHead>
|
||||
<TableHead className="text-[var(--purple-ink)] font-semibold">Error</TableHead>
|
||||
<TableRow className="bg-chart-6 hover:bg-chart-6">
|
||||
<TableHead className="text-primary font-semibold">Row</TableHead>
|
||||
<TableHead className="text-primary font-semibold">Email</TableHead>
|
||||
<TableHead className="text-primary font-semibold">Error</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{importResult.errors.map((error, idx) => (
|
||||
<TableRow key={idx} className="hover:bg-[var(--lavender-700)]">
|
||||
<TableCell className="font-medium text-[var(--purple-ink)]">{error.row}</TableCell>
|
||||
<TableCell className="text-brand-purple ">{error.email}</TableCell>
|
||||
<TableRow key={idx} className="hover:bg-bright-snow">
|
||||
<TableCell className="font-medium text-primary">{error.row}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{error.email}</TableCell>
|
||||
<TableCell className="text-red-600 text-sm">{error.error}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -301,7 +301,7 @@ const ImportMembersDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
className="rounded-xl bg-[var(--green-light)] hover:bg-[var(--green-fern)] text-white"
|
||||
className="rounded-xl bg-success hover:bg-muted-teal-balanced text-white"
|
||||
disabled={loading || !file}
|
||||
>
|
||||
{loading ? (
|
||||
@@ -320,7 +320,7 @@ const ImportMembersDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleClose}
|
||||
className="rounded-xl bg-[var(--green-light)] hover:bg-[var(--green-fern)] text-white"
|
||||
className="rounded-xl bg-success hover:bg-muted-teal-balanced text-white"
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
|
||||
@@ -125,11 +125,11 @@ const InviteStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-[600px] rounded-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<DialogTitle className="text-2xl text-primary flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Mail className="h-6 w-6" />
|
||||
{invitationUrl ? 'Invitation Sent' : 'Invite Staff Member'}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<DialogDescription className="text-muted-foreground" 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 staff. They will set their own password.'}
|
||||
@@ -139,16 +139,16 @@ const InviteStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
{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>
|
||||
<Label className="text-primary 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"
|
||||
className="rounded-xl border-2 border-chart-6 bg-gray-50"
|
||||
/>
|
||||
<Button
|
||||
onClick={copyToClipboard}
|
||||
className="rounded-xl bg-brand-purple hover:bg-[var(--purple-ink)] text-white flex-shrink-0"
|
||||
className="rounded-xl bg-muted-foreground hover:bg-primary text-white flex-shrink-0"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
@@ -170,7 +170,7 @@ const InviteStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
<div className="grid gap-6 py-4">
|
||||
{/* Email */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email" className="text-[var(--purple-ink)]">
|
||||
<Label htmlFor="email" className="text-primary">
|
||||
Email <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
@@ -178,7 +178,7 @@ const InviteStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
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 "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="staff@example.com"
|
||||
/>
|
||||
{errors.email && (
|
||||
@@ -188,35 +188,35 @@ const InviteStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
|
||||
{/* First Name (Optional) */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="first_name" className="text-[var(--purple-ink)]">
|
||||
<Label htmlFor="first_name" className="text-primary">
|
||||
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 "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="John"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Last Name (Optional) */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="last_name" className="text-[var(--purple-ink)]">
|
||||
<Label htmlFor="last_name" className="text-primary">
|
||||
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 "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="Doe"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Phone (Optional) */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="phone" className="text-[var(--purple-ink)]">
|
||||
<Label htmlFor="phone" className="text-primary">
|
||||
Phone <span className="text-gray-400">(Optional)</span>
|
||||
</Label>
|
||||
<Input
|
||||
@@ -224,14 +224,14 @@ const InviteStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
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 "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="(555) 123-4567"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Role */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="role" className="text-[var(--purple-ink)]">
|
||||
<Label htmlFor="role" className="text-primary">
|
||||
Role <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Select
|
||||
@@ -239,7 +239,7 @@ const InviteStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
onValueChange={(value) => handleChange('role', value)}
|
||||
disabled={loadingRoles}
|
||||
>
|
||||
<SelectTrigger className="rounded-xl border-2 border-[var(--neutral-800)]">
|
||||
<SelectTrigger className="rounded-xl border-2 border-chart-6">
|
||||
<SelectValue placeholder={loadingRoles ? "Loading roles..." : "Select a role"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -275,7 +275,7 @@ const InviteStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="rounded-xl bg-[var(--green-light)] hover:bg-[var(--green-fern)] text-white"
|
||||
className="rounded-xl bg-success hover:bg-muted-teal-balanced text-white"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
@@ -298,7 +298,7 @@ const InviteStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={handleClose}
|
||||
className="rounded-xl bg-[var(--green-light)] hover:bg-[var(--green-fern)] text-white"
|
||||
className="rounded-xl bg-success hover:bg-muted-teal-balanced text-white"
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Calendar, Users, User, BookOpen, FileText, DollarSign, Scale } from 'lu
|
||||
|
||||
const MemberFooter = () => {
|
||||
return (
|
||||
<footer className="bg-brand-dark-lavender text-white mt-auto">
|
||||
<footer className="bg-primary text-white mt-auto">
|
||||
<div className="max-w-7xl mx-auto px-6 py-12">
|
||||
<div className="grid md:grid-cols-4 gap-8">
|
||||
{/* Logo & About */}
|
||||
@@ -89,12 +89,12 @@ const MemberFooter = () => {
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/membership/contact-us" className="text-gray-300 hover:text-white transition-colors">
|
||||
<a href="/#contact" className="text-gray-300 hover:text-white transition-colors">
|
||||
Contact Us
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/membership/donate" className="text-gray-300 hover:text-white transition-colors">
|
||||
<a href="/#donate" className="text-gray-300 hover:text-white transition-colors">
|
||||
Donate
|
||||
</a>
|
||||
</li>
|
||||
@@ -104,12 +104,12 @@ const MemberFooter = () => {
|
||||
</div>
|
||||
|
||||
{/* Bottom Bar */}
|
||||
<div className="border-t border-[var(--purple-lavender)]">
|
||||
<div className="border-t border-muted-foreground">
|
||||
<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-300" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<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 gap-6">
|
||||
<a href="/membership/terms-of-service" className="hover:text-white transition-colors">Terms of Service</a>
|
||||
<a href="/membership/privacy-policy" className="hover:text-white transition-colors">Privacy Policy</a>
|
||||
<a href="/#terms" className="hover:text-white transition-colors">Terms of Service</a>
|
||||
<a href="/#privacy" className="hover:text-white transition-colors">Privacy Policy</a>
|
||||
</div>
|
||||
<p>© 2025 LOAF. All rights reserved.</p>
|
||||
</div>
|
||||
|
||||
@@ -19,8 +19,8 @@ const MemberRoute = ({ children }) => {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-[var(--neutral-200:)]">
|
||||
<p className="text-[var(--slate-muted)]">Loading...</p>
|
||||
<div className="min-h-screen flex items-center justify-center bg-ghost-white">
|
||||
<p className="text-slate-grey">Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ const Navbar = () => {
|
||||
return (
|
||||
<>
|
||||
{/* Top Header - Member Actions (Desktop Only) */}
|
||||
<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">
|
||||
<header className="hidden lg:flex bg-gradient-to-r from-dusty-grape to-foreground px-4 sm:px-8 md:px-16 py-4 justify-end items-center gap-4 sm:gap-6">
|
||||
{user && (
|
||||
<span className="text-white text-base font-medium" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
Welcome, {user.first_name}
|
||||
@@ -39,7 +39,7 @@ const Navbar = () => {
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
data-testid="admin-nav-button"
|
||||
>
|
||||
Dashboard
|
||||
Admin Panel
|
||||
</button>
|
||||
</Link>
|
||||
)}
|
||||
@@ -53,7 +53,7 @@ const Navbar = () => {
|
||||
</button>
|
||||
<Link to="/donate">
|
||||
<Button
|
||||
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]"
|
||||
className="bg-accent hover:bg-coral-glow text-foreground rounded-[25px] px-[54px] py-[10px] text-[16.5px] font-semibold h-[41px]"
|
||||
style={{ fontFamily: "'Montserrat', sans-serif" }}
|
||||
>
|
||||
Donate
|
||||
@@ -62,7 +62,7 @@ const Navbar = () => {
|
||||
</header>
|
||||
|
||||
{/* Main Header - Member Navigation */}
|
||||
<header className="bg-[var(--purple-lavender)] px-4 sm:px-8 md:px-16 py-2 flex justify-between items-center">
|
||||
<header className="bg-muted-foreground px-4 sm:px-8 md:px-16 py-2 flex justify-between items-center">
|
||||
<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" />
|
||||
</Link>
|
||||
@@ -86,19 +86,19 @@ const Navbar = () => {
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="bg-background min-w-[220px]">
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/about/history" className="w-full px-3 py-2 text-[var(--purple-deep)] hover:bg-[var(--lavender-300)] cursor-pointer"
|
||||
<Link to="/about/history" className="w-full px-3 py-2 text-foreground hover:bg-muted cursor-pointer"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
History
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/about/mission-values" className="w-full px-3 py-2 text-[var(--purple-deep)] hover:bg-[var(--lavender-300)] cursor-pointer"
|
||||
<Link to="/about/mission-values" className="w-full px-3 py-2 text-foreground hover:bg-muted cursor-pointer"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
Mission and Values
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/about/board" className="w-full px-3 py-2 text-[var(--purple-deep)] hover:bg-[var(--lavender-300)] cursor-pointer"
|
||||
<Link to="/about/board" className="w-full px-3 py-2 text-foreground hover:bg-muted cursor-pointer"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
Board of Directors
|
||||
</Link>
|
||||
@@ -110,7 +110,7 @@ const Navbar = () => {
|
||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
My Profile
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
to="/events"
|
||||
@@ -151,26 +151,33 @@ const Navbar = () => {
|
||||
</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"
|
||||
<Link to="/members/newsletters" className="w-full px-3 py-2 text-foreground hover:bg-muted cursor-pointer"
|
||||
style={{ fontFamily: "'Poppins', 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"
|
||||
<Link to="/members/financials" className="w-full px-3 py-2 text-foreground hover:bg-muted cursor-pointer"
|
||||
style={{ fontFamily: "'Poppins', 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"
|
||||
<Link to="/members/bylaws" className="w-full px-3 py-2 text-foreground hover:bg-muted cursor-pointer"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
Bylaws
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<Link
|
||||
to="/profile"
|
||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
data-testid="profile-nav-button"
|
||||
>
|
||||
Profile
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
{/* Mobile Hamburger Button */}
|
||||
@@ -193,7 +200,7 @@ const Navbar = () => {
|
||||
/>
|
||||
|
||||
{/* Drawer */}
|
||||
<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">
|
||||
<div className="fixed right-0 top-0 h-full w-[280px] bg-gradient-to-b from-muted-foreground to-foreground shadow-2xl flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-white/20">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -224,7 +231,7 @@ const Navbar = () => {
|
||||
)}
|
||||
|
||||
{/* Navigation Links */}
|
||||
<nav className="flex-1 overflow-y-auto scrollbar-dashboard py-6 px-4">
|
||||
<nav className="flex-1 overflow-y-auto py-6 px-4">
|
||||
<div className="space-y-2">
|
||||
<Link
|
||||
to="/"
|
||||
@@ -366,7 +373,7 @@ const Navbar = () => {
|
||||
className="w-full bg-background/20 hover:bg-background/30 text-white rounded-lg"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
Dashboard
|
||||
Admin Panel
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
@@ -376,7 +383,7 @@ const Navbar = () => {
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
<Button
|
||||
className="w-full bg-[var(--orange-light)] hover:bg-[var(--orange-coral)] text-[var(--purple-deep)] rounded-lg font-semibold"
|
||||
className="w-full bg-accent hover:bg-coral-glow text-foreground rounded-lg font-semibold"
|
||||
style={{ fontFamily: "'Montserrat', sans-serif" }}
|
||||
>
|
||||
Donate
|
||||
|
||||
@@ -159,10 +159,10 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[600px] bg-background rounded-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<DialogTitle className="text-2xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Activate Manual Payment
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<DialogDescription className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Record offline payment for {user.first_name} {user.last_name} ({user.email})
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
@@ -170,7 +170,7 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
<form onSubmit={handleSubmit} className="space-y-6 py-4">
|
||||
{/* Subscription Plan Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="plan_id" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Label htmlFor="plan_id" className="text-primary font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Subscription Plan
|
||||
</Label>
|
||||
<Select
|
||||
@@ -187,7 +187,7 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="rounded-xl border-2 border-[var(--neutral-800)]">
|
||||
<SelectTrigger className="rounded-xl border-2 border-chart-6">
|
||||
<SelectValue placeholder="Select subscription plan" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -203,7 +203,7 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{selectedPlan && (
|
||||
<p className="text-xs text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-xs text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{selectedPlan.description || `${selectedPlan.billing_cycle} subscription`}
|
||||
</p>
|
||||
)}
|
||||
@@ -211,7 +211,7 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
|
||||
{/* Payment Amount */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="amount" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Label htmlFor="amount" className="text-primary font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Payment Amount ($)
|
||||
</Label>
|
||||
<Input
|
||||
@@ -222,11 +222,11 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
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 "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
required
|
||||
/>
|
||||
{selectedPlan && (
|
||||
<p className="text-xs text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-xs text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Minimum: {formatPrice(selectedPlan.minimum_price_cents || selectedPlan.price_cents || 3000)}
|
||||
</p>
|
||||
)}
|
||||
@@ -234,14 +234,14 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
|
||||
{/* Breakdown Display */}
|
||||
{breakdown && breakdown.total >= breakdown.base && (
|
||||
<Card className="p-4 bg-[var(--lavender-400)] border border-[var(--neutral-800)]">
|
||||
<Card className="p-4 bg-lavender-mist border border-chart-6">
|
||||
<div className="space-y-2 text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="flex justify-between text-[var(--purple-ink)]">
|
||||
<div className="flex justify-between text-primary">
|
||||
<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)]">
|
||||
<div className="flex justify-between text-accent">
|
||||
<span className="flex items-center gap-1">
|
||||
<Heart className="h-4 w-4" />
|
||||
Additional Donation:
|
||||
@@ -249,7 +249,7 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
<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)]">
|
||||
<div className="flex justify-between text-primary font-bold text-base pt-2 border-t border-chart-6">
|
||||
<span>Total:</span>
|
||||
<span>{formatPrice(breakdown.total)}</span>
|
||||
</div>
|
||||
@@ -259,17 +259,17 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
|
||||
{/* Payment Date */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="payment_date" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Label htmlFor="payment_date" className="text-primary 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 " />
|
||||
<Calendar className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-muted-foreground" />
|
||||
<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 "
|
||||
className="pl-12 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -277,14 +277,14 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
|
||||
{/* Payment Method */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="payment_method" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Label htmlFor="payment_method" className="text-primary 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)]">
|
||||
<SelectTrigger className="rounded-xl border-2 border-chart-6">
|
||||
<SelectValue placeholder="Select payment method" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -298,7 +298,7 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
|
||||
{/* Subscription Period */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>Subscription Period</Label>
|
||||
<Label className="text-primary font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>Subscription Period</Label>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
@@ -306,9 +306,9 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
id="use_custom_period"
|
||||
checked={useCustomPeriod}
|
||||
onChange={(e) => setUseCustomPeriod(e.target.checked)}
|
||||
className="rounded border-[var(--neutral-800)]"
|
||||
className="rounded border-chart-6"
|
||||
/>
|
||||
<Label htmlFor="use_custom_period" className="text-sm text-brand-purple font-normal cursor-pointer" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Label htmlFor="use_custom_period" className="text-sm text-muted-foreground font-normal cursor-pointer" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Use custom dates instead of plan's billing cycle
|
||||
</Label>
|
||||
</div>
|
||||
@@ -316,7 +316,7 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
{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" }}>
|
||||
<Label htmlFor="custom_period_start" className="text-sm text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Start Date
|
||||
</Label>
|
||||
<Input
|
||||
@@ -324,12 +324,12 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
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 "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
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" }}>
|
||||
<Label htmlFor="custom_period_end" className="text-sm text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
End Date
|
||||
</Label>
|
||||
<Input
|
||||
@@ -337,18 +337,18 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
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 "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
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" }}>
|
||||
<div className="text-sm text-muted-foreground bg-muted 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>
|
||||
<span className="font-medium text-primary">Plan uses custom billing cycle:</span>
|
||||
<br />
|
||||
{(() => {
|
||||
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 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="notes" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Label htmlFor="notes" className="text-primary font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Notes (Optional)
|
||||
</Label>
|
||||
<Textarea
|
||||
@@ -386,7 +386,7 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
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]"
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground min-h-[100px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -395,14 +395,14 @@ const PaymentActivationDialog = ({ open, onOpenChange, user, onSuccess }) => {
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="rounded-full border-2 border-[var(--neutral-800)]"
|
||||
className="rounded-full border-2 border-chart-6"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="bg-[var(--green-light)] text-white hover:bg-[var(--green-mint)] rounded-full"
|
||||
className="bg-success text-white hover:bg-[#6FA087] rounded-full"
|
||||
>
|
||||
{loading ? 'Activating...' : 'Activate Payment'}
|
||||
</Button>
|
||||
|
||||
@@ -73,9 +73,9 @@ const PendingInvitationsTable = () => {
|
||||
|
||||
const getRoleBadge = (role) => {
|
||||
const config = {
|
||||
superadmin: { label: 'Superadmin', className: 'bg-brand-purple text-white' },
|
||||
admin: { label: 'Admin', className: 'bg-[var(--green-light)] text-white' },
|
||||
member: { label: 'Member', className: 'bg-[var(--neutral-800)] text-[var(--purple-ink)]' }
|
||||
superadmin: { label: 'Superadmin', className: 'bg-muted-foreground text-white' },
|
||||
admin: { label: 'Admin', className: 'bg-success text-white' },
|
||||
member: { label: 'Member', className: 'bg-chart-6 text-primary' }
|
||||
};
|
||||
|
||||
const roleConfig = config[role] || { label: role, className: 'bg-gray-500 text-white' };
|
||||
@@ -111,7 +111,7 @@ const PendingInvitationsTable = () => {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Loading invitations...
|
||||
</p>
|
||||
</div>
|
||||
@@ -120,12 +120,12 @@ const PendingInvitationsTable = () => {
|
||||
|
||||
if (invitations.length === 0) {
|
||||
return (
|
||||
<Card className="p-12 bg-background rounded-2xl border border-[var(--neutral-800)] text-center">
|
||||
<Mail className="h-16 w-16 text-[var(--neutral-800)] mx-auto mb-4" />
|
||||
<h3 className="text-xl font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Card className="p-12 bg-background rounded-2xl border border-chart-6 text-center">
|
||||
<Mail className="h-16 w-16 text-chart-6 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-semibold text-primary mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
No Pending Invitations
|
||||
</h3>
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
All invitations have been accepted or expired
|
||||
</p>
|
||||
</Card>
|
||||
@@ -134,37 +134,37 @@ const PendingInvitationsTable = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="bg-background rounded-2xl border border-[var(--neutral-800)] overflow-hidden">
|
||||
<Card className="bg-background rounded-2xl border border-chart-6 overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-[var(--neutral-800)] hover:bg-[var(--neutral-800)]">
|
||||
<TableHead className="text-[var(--purple-ink)] font-semibold">Email</TableHead>
|
||||
<TableHead className="text-[var(--purple-ink)] font-semibold">Name</TableHead>
|
||||
<TableHead className="text-[var(--purple-ink)] font-semibold">Role</TableHead>
|
||||
<TableHead className="text-[var(--purple-ink)] font-semibold">Invited</TableHead>
|
||||
<TableHead className="text-[var(--purple-ink)] font-semibold">Expires</TableHead>
|
||||
<TableHead className="text-[var(--purple-ink)] font-semibold text-right">Actions</TableHead>
|
||||
<TableRow className="bg-chart-6 hover:bg-chart-6">
|
||||
<TableHead className="text-primary font-semibold">Email</TableHead>
|
||||
<TableHead className="text-primary font-semibold">Name</TableHead>
|
||||
<TableHead className="text-primary font-semibold">Role</TableHead>
|
||||
<TableHead className="text-primary font-semibold">Invited</TableHead>
|
||||
<TableHead className="text-primary font-semibold">Expires</TableHead>
|
||||
<TableHead className="text-primary font-semibold text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{invitations.map((invitation) => (
|
||||
<TableRow key={invitation.id} className="hover:bg-[var(--lavender-700)]">
|
||||
<TableCell className="font-medium text-[var(--purple-ink)]">
|
||||
<TableRow key={invitation.id} className="hover:bg-bright-snow">
|
||||
<TableCell className="font-medium text-primary">
|
||||
{invitation.email}
|
||||
</TableCell>
|
||||
<TableCell className="text-brand-purple ">
|
||||
<TableCell className="text-muted-foreground">
|
||||
{invitation.first_name && invitation.last_name
|
||||
? `${invitation.first_name} ${invitation.last_name}`
|
||||
: '-'}
|
||||
</TableCell>
|
||||
<TableCell>{getRoleBadge(invitation.role)}</TableCell>
|
||||
<TableCell className="text-brand-purple ">
|
||||
<TableCell className="text-muted-foreground">
|
||||
{new Date(invitation.invited_at).toLocaleDateString()}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<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-brand-purple '}`}>
|
||||
<Clock className={`h-4 w-4 ${isExpiringSoon(invitation.expires_at) ? 'text-orange-500' : 'text-muted-foreground'}`} />
|
||||
<span className={`text-sm ${isExpiringSoon(invitation.expires_at) ? 'text-orange-500 font-semibold' : 'text-muted-foreground'}`}>
|
||||
{formatDate(invitation.expires_at)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -176,7 +176,7 @@ const PendingInvitationsTable = () => {
|
||||
size="sm"
|
||||
onClick={() => handleResend(invitation.id)}
|
||||
disabled={resending === invitation.id}
|
||||
className="rounded-xl border-[var(--green-light)] text-[var(--green-light)] hover:bg-[var(--green-light)] hover:text-white"
|
||||
className="rounded-xl border-success text-success hover:bg-success hover:text-white"
|
||||
>
|
||||
{resending === invitation.id ? (
|
||||
'Resending...'
|
||||
@@ -208,10 +208,10 @@ const PendingInvitationsTable = () => {
|
||||
<AlertDialog open={revokeDialog.open} onOpenChange={(open) => setRevokeDialog({ open, invitation: null })}>
|
||||
<AlertDialogContent className="rounded-2xl">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="text-2xl text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<AlertDialogTitle className="text-2xl text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Revoke Invitation
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<AlertDialogDescription className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Are you sure you want to revoke the invitation for{' '}
|
||||
<span className="font-semibold">{revokeDialog.invitation?.email}</span>?
|
||||
This action cannot be undone.
|
||||
|
||||
@@ -159,12 +159,12 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto scrollbar-dashboard">
|
||||
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<DialogTitle className="text-2xl text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{plan ? 'Edit Plan' : 'Create New Plan'}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<DialogDescription className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{plan ? 'Update plan details below' : 'Enter plan details to create a new subscription plan'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
@@ -197,8 +197,8 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
|
||||
</div>
|
||||
|
||||
{/* Dynamic Pricing */}
|
||||
<div className="border-2 border-[var(--neutral-800)] rounded-lg p-4 space-y-4">
|
||||
<h3 className="font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<div className="border-2 border-chart-6 rounded-lg p-4 space-y-4">
|
||||
<h3 className="font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Dynamic Pricing
|
||||
</h3>
|
||||
|
||||
@@ -216,7 +216,7 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
|
||||
required
|
||||
className="mt-2"
|
||||
/>
|
||||
<p className="text-xs text-brand-purple mt-1">Minimum $30</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">Minimum $30</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -232,7 +232,7 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
|
||||
required
|
||||
className="mt-2"
|
||||
/>
|
||||
<p className="text-xs text-brand-purple mt-1">Pre-filled amount</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">Pre-filled amount</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -240,7 +240,7 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<div>
|
||||
<Label htmlFor="allow_donation">Allow Donations</Label>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Members can pay more than minimum
|
||||
</p>
|
||||
</div>
|
||||
@@ -252,7 +252,7 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
|
||||
onChange={(e) => setFormData({ ...formData, allow_donation: e.target.checked })}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<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>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-muted-foreground/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-success"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -279,11 +279,11 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
|
||||
|
||||
{/* Custom Billing Cycle Dates */}
|
||||
{formData.billing_cycle === 'custom' && (
|
||||
<div className="border-2 border-[var(--neutral-800)] rounded-lg p-4 space-y-4">
|
||||
<h3 className="font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<div className="border-2 border-chart-6 rounded-lg p-4 space-y-4">
|
||||
<h3 className="font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Custom Billing Period
|
||||
</h3>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Set recurring date range (e.g., Jan 1 - Dec 31 for calendar year)
|
||||
</p>
|
||||
|
||||
@@ -349,8 +349,8 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[var(--lavender-400)] border border-[var(--neutral-800)] rounded p-3">
|
||||
<p className="text-sm text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="bg-lavender-mist border border-chart-6 rounded p-3">
|
||||
<p className="text-sm text-primary" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<strong>Example:</strong> Jan 1 - Dec 31 for calendar year, or Jul 1 - Jun 30 for fiscal year
|
||||
</p>
|
||||
</div>
|
||||
@@ -361,7 +361,7 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label htmlFor="active">Active Status</Label>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Inactive plans won't appear for new subscriptions
|
||||
</p>
|
||||
</div>
|
||||
@@ -373,7 +373,7 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
|
||||
onChange={(e) => setFormData({ ...formData, active: e.target.checked })}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<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>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-muted-foreground/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-success"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -389,7 +389,7 @@ const PlanDialog = ({ open, onOpenChange, plan, onSuccess }) => {
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background"
|
||||
className="bg-chart-6 text-primary hover:bg-background"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
|
||||
@@ -8,7 +8,7 @@ const PublicFooter = () => {
|
||||
return (
|
||||
<>
|
||||
{/* Main Footer */}
|
||||
<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">
|
||||
<footer className="bg-dusty-grape border-t border-[rgba(0,0,0,0.1)] py-8 px-2 lg:px-10 flex items-center justify-between">
|
||||
<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-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" />
|
||||
@@ -19,28 +19,28 @@ const PublicFooter = () => {
|
||||
<div className="pb-2 lg:pb-4">
|
||||
<p className="text-white text-xl font-medium" style={{ fontFamily: "'Poppins', sans-serif" }}>About</p>
|
||||
</div>
|
||||
<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-[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-[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>
|
||||
<Link to="/about/history" className="text-chart-6 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-chart-6 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-chart-6 text-sm sm:text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Board of Directors</Link>
|
||||
</div>
|
||||
<div className="hidden md:flex flex-col gap-2 items-start text-left w-full sm:w-auto sm:min-w-[148px]">
|
||||
<div className="pb-2 lg:pb-4">
|
||||
<p className="text-white text-xl font-medium" style={{ fontFamily: "'Poppins', sans-serif" }}>Connect</p>
|
||||
</div>
|
||||
<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-[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-[var(--neutral-800)] text-sm sm:text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Resources</Link>
|
||||
<Link to="/become-a-member" className="text-chart-6 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-chart-6 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-chart-6 text-sm sm:text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Resources</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 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">
|
||||
<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 ">
|
||||
<Button className="bg-accent hover:bg-coral-glow text-foreground rounded-full px-12 lg:px-16 py-6 text-lg sm:text-lg font-medium ">
|
||||
Donate
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<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" }}>
|
||||
<p className="text-chart-6 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
|
||||
</p>
|
||||
</div>
|
||||
@@ -49,20 +49,20 @@ const PublicFooter = () => {
|
||||
</footer>
|
||||
|
||||
{/* Bottom Footer */}
|
||||
<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">
|
||||
<footer className="bg-gradient-to-r from-foreground to-dusty-grape 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">
|
||||
<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-[var(--neutral-500)] text-sm sm:text-base font-medium hover:text-white transition-colors whitespace-nowrap" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Link to="/terms-of-service" className="text-[#c5b4e3] text-sm sm:text-base font-medium hover:text-white transition-colors whitespace-nowrap" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Terms of Service
|
||||
</Link>
|
||||
<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" }}>
|
||||
<Link to="/privacy-policy" className="text-[#c5b4e3] text-sm sm:text-base font-medium hover:text-white transition-colors whitespace-nowrap" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</nav>
|
||||
<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" }}>
|
||||
<p className="text-[#c5b4e3] text-sm sm:text-base font-medium text-center order-2 sm:order-none" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
© 2025 LOAF. All Rights Reserved.
|
||||
</p>
|
||||
<p className="text-[var(--neutral-500)] text-sm sm:text-base font-medium text-center order-3 sm:order-none" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-[#c5b4e3] text-sm sm:text-base font-medium text-center order-3 sm:order-none" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Designed and Managed by{' '}
|
||||
<a href="https://konceptkit.com/" className=" text-white transition-colors whitespace-nowrap">
|
||||
Koncept Kit
|
||||
|
||||
@@ -46,7 +46,7 @@ const PublicNavbar = () => {
|
||||
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-accent hover:text-coral-glow `;
|
||||
}
|
||||
return `${baseClasses} text-white hover:opacity-80`;
|
||||
};
|
||||
@@ -55,18 +55,18 @@ const PublicNavbar = () => {
|
||||
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} bg-accent hover:bg-coral-glow text-foreground`;
|
||||
}
|
||||
return `${baseClasses} text-white hover:bg-[var(--purple-deep)]`;
|
||||
return `${baseClasses} text-white hover:bg-foreground`;
|
||||
};
|
||||
|
||||
// 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} bg-accent hover:bg-coral-glow text-foreground`;
|
||||
}
|
||||
return `${baseClasses} text-[var(--neutral-800)] hover:bg-[var(--purple-deep)] hover:text-white`;
|
||||
return `${baseClasses} text-chart-6 hover:bg-foreground hover:text-white`;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -74,7 +74,7 @@ const PublicNavbar = () => {
|
||||
{/* Top Header - Auth Actions */}
|
||||
<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">
|
||||
<header className="bg-gradient-to-r flex-wrap from-dusty-grape to-foreground px-[20px] py-[10px] flex md:justify-end justify-between items-center gap-4 sm:gap-6">
|
||||
<div className='flex gap-4 sm:gap-6'>
|
||||
|
||||
<button
|
||||
@@ -96,7 +96,7 @@ const PublicNavbar = () => {
|
||||
</div>
|
||||
<Link to="/donate">
|
||||
<Button
|
||||
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]"
|
||||
className="bg-accent hover:bg-coral-glow text-foreground rounded-[25px] px-[50px] py-[5px] text-[16.5px] font-semibold h-[41px]"
|
||||
style={{ fontFamily: "'Montserrat', sans-serif" }}
|
||||
>
|
||||
Donate
|
||||
@@ -105,7 +105,7 @@ const PublicNavbar = () => {
|
||||
</header>
|
||||
|
||||
{/* Main Header - Navigation */}
|
||||
<header className=" bg-brand-purple px-[20px] py-2 flex justify-between items-center">
|
||||
<header className=" bg-muted-foreground px-[20px] py-2 flex justify-between items-center">
|
||||
<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" />
|
||||
</Link>
|
||||
@@ -113,7 +113,7 @@ const PublicNavbar = () => {
|
||||
{/* Mobile Menu Button */}
|
||||
<button
|
||||
onClick={() => setIsMobileMenuOpen(true)}
|
||||
className="lg:hidden p-2 text-white hover:bg-[var(--purple-deep)] rounded-md transition-colors"
|
||||
className="lg:hidden p-2 text-white hover:bg-foreground rounded-md transition-colors"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<Menu className="size-14" />
|
||||
@@ -132,7 +132,7 @@ const PublicNavbar = () => {
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
className={`${isAboutActive()
|
||||
? "text-[var(--orange-light)] hover:text-[var(--orange-coral)]"
|
||||
? "text-accent hover:text-coral-glow"
|
||||
: "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
|
||||
@@ -141,19 +141,19 @@ const PublicNavbar = () => {
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="bg-background min-w-[220px]">
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/about/history" className="w-full px-3 py-2 text-[var(--purple-deep)] hover:bg-[var(--lavender-300)] cursor-pointer"
|
||||
<Link to="/about/history" className="w-full px-3 py-2 text-foreground hover:bg-muted cursor-pointer"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
History
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/about/mission-values" className="w-full px-3 py-2 text-[var(--purple-deep)] hover:bg-[var(--lavender-300)] cursor-pointer"
|
||||
<Link to="/about/mission-values" className="w-full px-3 py-2 text-foreground hover:bg-muted cursor-pointer"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Mission and Values
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/about/board" className="w-full px-3 py-2 text-[var(--purple-deep)] hover:bg-[var(--lavender-300)] cursor-pointer"
|
||||
<Link to="/about/board" className="w-full px-3 py-2 text-foreground hover:bg-muted cursor-pointer"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Board of Directors
|
||||
</Link>
|
||||
@@ -165,7 +165,7 @@ const PublicNavbar = () => {
|
||||
className={getDesktopLinkClasses(user ? "/dashboard" : "/become-a-member")}
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
>
|
||||
{user ? 'My Profile' : 'Become a Member'}
|
||||
{user ? 'Dashboard' : 'Become a Member'}
|
||||
</Link>
|
||||
{!user && (
|
||||
<Link
|
||||
@@ -204,15 +204,15 @@ const PublicNavbar = () => {
|
||||
/>
|
||||
|
||||
{/* Drawer */}
|
||||
<div className="fixed right-0 top-0 h-full w-[280px] bg-brand-purple shadow-xl overflow-y-auto scrollbar-dashboard">
|
||||
<div className="fixed right-0 top-0 h-full w-[280px] bg-muted-foreground shadow-xl overflow-y-auto">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center p-6 border-b border-[var(--purple-deep)]">
|
||||
<div className="flex justify-between items-center p-6 border-b border-foreground">
|
||||
<span className="text-white text-lg font-semibold" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Menu
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
className="p-2 text-white hover:bg-[var(--purple-deep)] rounded-md transition-colors"
|
||||
className="p-2 text-white hover:bg-foreground rounded-md transition-colors"
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
@@ -233,7 +233,7 @@ const PublicNavbar = () => {
|
||||
{/* About Us Section */}
|
||||
<div className="space-y-2">
|
||||
<p
|
||||
className={`text-base font-semibold px-4 py-2 rounded-md ${isAboutActive() ? 'text-[var(--orange-light)]' : 'text-white'}`}
|
||||
className={`text-base font-semibold px-4 py-2 rounded-md ${isAboutActive() ? 'text-accent' : 'text-white'}`}
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
>
|
||||
About Us
|
||||
@@ -270,7 +270,7 @@ const PublicNavbar = () => {
|
||||
className={getMobileLinkClasses(user ? "/dashboard" : "/become-a-member")}
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
>
|
||||
{user ? 'My Profile' : 'Become a Member'}
|
||||
{user ? 'Dashboard' : 'Become a Member'}
|
||||
</Link>
|
||||
|
||||
{!user && (
|
||||
@@ -303,13 +303,13 @@ const PublicNavbar = () => {
|
||||
</Link>
|
||||
|
||||
{/* Auth Actions */}
|
||||
<div className="pt-4 border-t border-[var(--purple-deep)] space-y-2">
|
||||
<div className="pt-4 border-t border-foreground space-y-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
handleAuthAction();
|
||||
setIsMobileMenuOpen(false);
|
||||
}}
|
||||
className="w-full text-left text-white text-base font-medium hover:bg-[var(--purple-deep)] px-4 py-3 rounded-md transition-colors"
|
||||
className="w-full text-left text-white text-base font-medium hover:bg-foreground px-4 py-3 rounded-md transition-colors"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
>
|
||||
{user ? 'Logout' : 'Login'}
|
||||
@@ -318,7 +318,7 @@ const PublicNavbar = () => {
|
||||
<Link
|
||||
to="/register"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
className="block text-white text-base font-medium hover:bg-[var(--purple-deep)] px-4 py-3 rounded-md transition-colors"
|
||||
className="block text-white text-base font-medium hover:bg-foreground px-4 py-3 rounded-md transition-colors"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
>
|
||||
Register
|
||||
@@ -329,7 +329,7 @@ const PublicNavbar = () => {
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
className="block w-full"
|
||||
>
|
||||
<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">
|
||||
<Button className="w-full bg-accent hover:bg-coral-glow text-foreground rounded-[25px] px-6 py-3 text-base font-semibold">
|
||||
Donate
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
@@ -31,33 +31,33 @@ export default function RejectionDialog({ open, onOpenChange, onConfirm, user, l
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-[500px] rounded-2xl border-2 border-[var(--neutral-800)]">
|
||||
<DialogContent className="sm:max-w-[500px] rounded-2xl border-2 border-chart-6">
|
||||
<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" }}>
|
||||
<DialogTitle className="text-2xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Reject Application
|
||||
</DialogTitle>
|
||||
</div>
|
||||
<DialogDescription className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<DialogDescription className="text-muted-foreground" 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" }}>
|
||||
<div className="bg-lavender-mist border border-chart-6 rounded-lg p-4">
|
||||
<p className="text-sm text-muted-foreground" 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" }}>
|
||||
<p className="text-sm text-muted-foreground" 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">
|
||||
<Label htmlFor="reason" className="text-primary font-medium">
|
||||
Rejection Reason <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
@@ -68,13 +68,13 @@ export default function RejectionDialog({ open, onOpenChange, onConfirm, user, l
|
||||
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]"
|
||||
className="rounded-xl border-2 border-chart-6 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" }}>
|
||||
<p className="text-xs text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
The applicant will receive an email with this reason.
|
||||
</p>
|
||||
</div>
|
||||
@@ -85,7 +85,7 @@ export default function RejectionDialog({ open, onOpenChange, onConfirm, user, l
|
||||
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"
|
||||
className="border-2 border-chart-6 text-muted-foreground hover:bg-muted rounded-full px-6"
|
||||
disabled={loading}
|
||||
>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import React from "react";
|
||||
import { Card } from "./ui/card";
|
||||
|
||||
export const StatCard = ({
|
||||
title,
|
||||
value,
|
||||
icon: Icon,
|
||||
iconBgClass,
|
||||
dataTestId,
|
||||
}) => (
|
||||
<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 ">
|
||||
<div className={`${iconBgClass} p-3 rounded-lg `}>
|
||||
<Icon className="size-8" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
<p
|
||||
className="text-6xl font-semibold text-[var(--purple-ink)] mb-1"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
{value}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
className="text-sm text-brand-purple "
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
</Card>
|
||||
);
|
||||
@@ -1,252 +0,0 @@
|
||||
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-[var(--purple-lavender)]" />
|
||||
) : (
|
||||
<Heart className="h-5 w-5 text-[var(--orange-light)]" />
|
||||
)}
|
||||
</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;
|
||||
@@ -370,15 +370,15 @@ export default function WordPressImportWizard({ open, onOpenChange, onSuccess })
|
||||
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 ">
|
||||
<h3 className="text-lg font-semibold text-primary mb-2">Upload WordPress CSV Export</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
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)]">
|
||||
<Card className="p-6 border-2 border-dashed border-chart-6 bg-lavender-mist">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<Upload className="h-12 w-12 text-brand-purple " />
|
||||
<Upload className="h-12 w-12 text-muted-foreground" />
|
||||
<div className="text-center">
|
||||
<Input
|
||||
type="file"
|
||||
@@ -387,7 +387,7 @@ export default function WordPressImportWizard({ open, onOpenChange, onSuccess })
|
||||
className="max-w-xs"
|
||||
/>
|
||||
{uploadedFile && (
|
||||
<p className="text-sm text-brand-purple mt-2">
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
Selected: {uploadedFile.name}
|
||||
</p>
|
||||
)}
|
||||
@@ -399,7 +399,7 @@ export default function WordPressImportWizard({ open, onOpenChange, onSuccess })
|
||||
<Button
|
||||
onClick={handleUpload}
|
||||
disabled={uploading}
|
||||
className="w-full bg-brand-purple hover:bg-[var(--purple-ink)]"
|
||||
className="w-full bg-muted-foreground hover:bg-primary"
|
||||
>
|
||||
{uploading ? (
|
||||
<>
|
||||
@@ -465,8 +465,8 @@ export default function WordPressImportWizard({ open, onOpenChange, onSuccess })
|
||||
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 ">
|
||||
<h3 className="text-lg font-semibold text-primary mb-2">Field Mapping</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
WordPress fields have been automatically mapped to LOAF platform fields.
|
||||
</p>
|
||||
</div>
|
||||
@@ -537,20 +537,20 @@ export default function WordPressImportWizard({ open, onOpenChange, onSuccess })
|
||||
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 ">
|
||||
<h3 className="text-lg font-semibold text-primary mb-2">Review & Adjust User Status</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
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)]">
|
||||
<Card className="p-4 bg-lavender-mist border-chart-6">
|
||||
<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">
|
||||
<span className="text-sm text-muted-foreground font-medium">
|
||||
{selectedRows.size > 0 ? `${selectedRows.size} selected` : 'Select all'}
|
||||
</span>
|
||||
{selectedRows.size > 0 && (
|
||||
@@ -572,13 +572,13 @@ export default function WordPressImportWizard({ open, onOpenChange, onSuccess })
|
||||
{/* Data table */}
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-brand-purple " />
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-[var(--lavender-400)]">
|
||||
<TableRow className="bg-lavender-mist">
|
||||
<TableHead className="w-12">
|
||||
<Checkbox checked={false} />
|
||||
</TableHead>
|
||||
@@ -606,7 +606,7 @@ export default function WordPressImportWizard({ open, onOpenChange, onSuccess })
|
||||
{row.first_name} {row.last_name}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge className="bg-[var(--neutral-800)] text-[var(--purple-ink)]">
|
||||
<Badge className="bg-chart-6 text-primary">
|
||||
{row.wordpress_roles?.join(', ') || 'N/A'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
@@ -651,7 +651,7 @@ export default function WordPressImportWizard({ open, onOpenChange, onSuccess })
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-brand-purple ">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Page {currentPage} of {totalPages}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
@@ -689,41 +689,41 @@ export default function WordPressImportWizard({ open, onOpenChange, onSuccess })
|
||||
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 ">
|
||||
<h3 className="text-lg font-semibold text-primary mb-2">Import Preview</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
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>
|
||||
<p className="text-sm text-muted-foreground">Total Users</p>
|
||||
<p className="text-3xl font-semibold text-primary">{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>
|
||||
<p className="text-sm text-muted-foreground">Status Overrides</p>
|
||||
<p className="text-3xl font-semibold text-primary">{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>
|
||||
<p className="text-sm text-muted-foreground">Expected Imports</p>
|
||||
<p className="text-3xl font-semibold text-primary">{analysisResult?.valid_rows}</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="p-6">
|
||||
<h4 className="font-semibold text-[var(--purple-ink)] mb-4">Import Options</h4>
|
||||
<h4 className="font-semibold text-primary 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>
|
||||
<span className="text-sm text-muted-foreground">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>
|
||||
<span className="text-sm text-muted-foreground">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>
|
||||
<span className="text-sm text-muted-foreground">Full rollback capability available after import</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -748,10 +748,10 @@ export default function WordPressImportWizard({ open, onOpenChange, onSuccess })
|
||||
const Step5Execute = () => (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2">
|
||||
<h3 className="text-lg font-semibold text-primary mb-2">
|
||||
{importing ? 'Import in Progress...' : 'Ready to Import'}
|
||||
</h3>
|
||||
<p className="text-sm text-brand-purple ">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{importing
|
||||
? 'Please wait while users are imported. This may take a few minutes.'
|
||||
: 'Click "Start Import" to begin importing users.'}
|
||||
@@ -761,7 +761,7 @@ export default function WordPressImportWizard({ open, onOpenChange, onSuccess })
|
||||
{importing && (
|
||||
<div className="space-y-4">
|
||||
<Progress value={importProgress} className="w-full" />
|
||||
<p className="text-center text-sm text-brand-purple ">
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
{importProgress.toFixed(1)}% complete
|
||||
</p>
|
||||
</div>
|
||||
@@ -770,7 +770,7 @@ export default function WordPressImportWizard({ open, onOpenChange, onSuccess })
|
||||
{!importing && !importResults && (
|
||||
<Button
|
||||
onClick={handleExecuteImport}
|
||||
className="w-full bg-brand-purple hover:bg-[var(--purple-ink)] py-6 text-lg"
|
||||
className="w-full bg-muted-foreground hover:bg-primary py-6 text-lg"
|
||||
>
|
||||
<Play className="mr-2 h-5 w-5" />
|
||||
Start Import
|
||||
@@ -786,8 +786,8 @@ export default function WordPressImportWizard({ open, onOpenChange, onSuccess })
|
||||
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 ">
|
||||
<h3 className="text-lg font-semibold text-primary mb-2">Import Complete</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Review the import results and download error reports if needed.
|
||||
</p>
|
||||
</div>
|
||||
@@ -850,11 +850,11 @@ export default function WordPressImportWizard({ open, onOpenChange, onSuccess })
|
||||
<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)]">
|
||||
<DialogTitle className="text-2xl font-semibold text-primary">
|
||||
Confirm Rollback
|
||||
</DialogTitle>
|
||||
</div>
|
||||
<DialogDescription className="text-brand-purple ">
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
This will permanently delete{' '}
|
||||
<strong>{importResults?.successful_rows} users</strong> that were imported.
|
||||
This action cannot be undone.
|
||||
@@ -896,12 +896,12 @@ export default function WordPressImportWizard({ open, onOpenChange, onSuccess })
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto scrollbar-dashboard">
|
||||
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl font-semibold text-[var(--purple-ink)]">
|
||||
<DialogTitle className="text-2xl font-semibold text-primary">
|
||||
WordPress Import Wizard
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-brand-purple ">
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
Import WordPress users with interactive status review and full rollback capability
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
@@ -919,7 +919,7 @@ export default function WordPressImportWizard({ open, onOpenChange, onSuccess })
|
||||
<div
|
||||
className={`
|
||||
w-10 h-10 rounded-full flex items-center justify-center
|
||||
${isCurrent ? 'bg-brand-purple text-white' : ''}
|
||||
${isCurrent ? 'bg-muted-foreground text-white' : ''}
|
||||
${isCompleted ? 'bg-green-600 text-white' : ''}
|
||||
${!isCurrent && !isCompleted ? 'bg-gray-200 text-gray-600' : ''}
|
||||
`}
|
||||
@@ -930,7 +930,7 @@ export default function WordPressImportWizard({ open, onOpenChange, onSuccess })
|
||||
<StepIcon className="h-5 w-5" />
|
||||
)}
|
||||
</div>
|
||||
<p className={`text-xs mt-1 ${isCurrent ? 'font-semibold text-[var(--purple-ink)]' : 'text-gray-600'}`}>
|
||||
<p className={`text-xs mt-1 ${isCurrent ? 'font-semibold text-primary' : 'text-gray-600'}`}>
|
||||
{step.title}
|
||||
</p>
|
||||
</div>
|
||||
@@ -962,7 +962,7 @@ export default function WordPressImportWizard({ open, onOpenChange, onSuccess })
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
disabled={!canProceed()}
|
||||
className="bg-brand-purple hover:bg-[var(--purple-ink)]"
|
||||
className="bg-muted-foreground hover:bg-primary"
|
||||
>
|
||||
Next
|
||||
<ChevronRight className="h-4 w-4 ml-2" />
|
||||
@@ -975,7 +975,7 @@ export default function WordPressImportWizard({ open, onOpenChange, onSuccess })
|
||||
onOpenChange(false);
|
||||
if (onSuccess) onSuccess();
|
||||
}}
|
||||
className="bg-brand-purple hover:bg-[var(--purple-ink)]"
|
||||
className="bg-muted-foreground hover:bg-primary"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
|
||||
@@ -26,7 +26,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
|
||||
<div className="space-y-8">
|
||||
{/* Personal Information */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h2 className="text-2xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Personal Information
|
||||
</h2>
|
||||
|
||||
@@ -40,7 +40,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
|
||||
required
|
||||
value={formData.first_name}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
data-testid="first-name-input"
|
||||
/>
|
||||
</div>
|
||||
@@ -52,7 +52,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
|
||||
required
|
||||
value={formData.last_name}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
data-testid="last-name-input"
|
||||
/>
|
||||
</div>
|
||||
@@ -69,7 +69,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
|
||||
required
|
||||
value={formData.phone}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
data-testid="phone-input"
|
||||
/>
|
||||
</div>
|
||||
@@ -82,7 +82,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
|
||||
required
|
||||
value={formData.date_of_birth}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
data-testid="dob-input"
|
||||
/>
|
||||
</div>
|
||||
@@ -97,7 +97,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
|
||||
required
|
||||
value={formData.address}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-var[(--neutral-300)] focus:border-[var(--orange-soft)]"
|
||||
className="h-14 rounded-xl border-2 border-almond-cream focus:border-burnt-peach"
|
||||
data-testid="address-input"
|
||||
/>
|
||||
</div>
|
||||
@@ -112,7 +112,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
|
||||
required
|
||||
value={formData.city}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
data-testid="city-input"
|
||||
/>
|
||||
</div>
|
||||
@@ -124,7 +124,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
|
||||
required
|
||||
value={formData.state}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
data-testid="state-input"
|
||||
/>
|
||||
</div>
|
||||
@@ -136,7 +136,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
|
||||
required
|
||||
value={formData.zipcode}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
data-testid="zipcode-input"
|
||||
/>
|
||||
</div>
|
||||
@@ -145,7 +145,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
|
||||
|
||||
{/* How Did You Hear About Us */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h2 className="text-2xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
How Did You Hear About Us? *
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
@@ -167,7 +167,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
|
||||
|
||||
{/* Partner Information */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h2 className="text-2xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Partner Information (Optional)
|
||||
</h2>
|
||||
|
||||
@@ -179,7 +179,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
|
||||
name="partner_first_name"
|
||||
value={formData.partner_first_name}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
data-testid="partner-first-name-input"
|
||||
/>
|
||||
</div>
|
||||
@@ -190,7 +190,7 @@ const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
|
||||
name="partner_last_name"
|
||||
value={formData.partner_last_name}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
data-testid="partner-last-name-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -33,10 +33,10 @@ const RegistrationStep2 = ({ formData, setFormData, handleInputChange }) => {
|
||||
<div className="space-y-8">
|
||||
{/* Newsletter Publication Preferences */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h2 className="text-2xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Newsletter Publication Preferences *
|
||||
</h2>
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Please check what information may be published in LOAF Newsletter
|
||||
</p>
|
||||
|
||||
@@ -97,7 +97,7 @@ const RegistrationStep2 = ({ formData, setFormData, handleInputChange }) => {
|
||||
|
||||
{/* Referral */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h2 className="text-2xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Referral
|
||||
</h2>
|
||||
<div>
|
||||
@@ -110,10 +110,10 @@ const RegistrationStep2 = ({ formData, setFormData, handleInputChange }) => {
|
||||
value={formData.referred_by_member_name}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter member name or email"
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
data-testid="referral-input"
|
||||
/>
|
||||
<p className="text-sm text-brand-purple mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
If referred by a current member, you may skip the event attendance requirement.
|
||||
</p>
|
||||
</div>
|
||||
@@ -121,10 +121,10 @@ const RegistrationStep2 = ({ formData, setFormData, handleInputChange }) => {
|
||||
|
||||
{/* Volunteer Interests */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h2 className="text-2xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Volunteer Interests (Optional)
|
||||
</h2>
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" 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>
|
||||
|
||||
@@ -158,7 +158,7 @@ const RegistrationStep2 = ({ formData, setFormData, handleInputChange }) => {
|
||||
I am requesting for scholarship
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Scholarship information is kept confidential
|
||||
</p>
|
||||
|
||||
@@ -174,7 +174,7 @@ const RegistrationStep2 = ({ formData, setFormData, handleInputChange }) => {
|
||||
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 "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -23,11 +23,11 @@ const RegistrationStep3 = ({ formData, setFormData, handleInputChange }) => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h2 className="text-2xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Members Directory
|
||||
</h2>
|
||||
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" 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)
|
||||
</p>
|
||||
|
||||
@@ -37,8 +37,8 @@ const RegistrationStep3 = ({ formData, setFormData, handleInputChange }) => {
|
||||
className={`
|
||||
p-4 rounded-xl border-2 cursor-pointer transition-all
|
||||
${formData.show_in_directory
|
||||
? 'border-[var(--orange-light)] bg-[var(--orange-light)]/5'
|
||||
: 'border-[var(--neutral-800)] hover:border-brand-purple '
|
||||
? 'border-accent bg-accent/5'
|
||||
: 'border-chart-6 hover:border-muted-foreground'
|
||||
}
|
||||
`}
|
||||
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={`
|
||||
w-5 h-5 rounded-full border-2 flex items-center justify-center
|
||||
${formData.show_in_directory ? 'border-[var(--orange-light)]' : 'border-[var(--neutral-800)]'}
|
||||
${formData.show_in_directory ? 'border-accent' : 'border-chart-6'}
|
||||
`}>
|
||||
{formData.show_in_directory && (
|
||||
<div className="w-3 h-3 rounded-full bg-[var(--orange-light)]" />
|
||||
<div className="w-3 h-3 rounded-full bg-accent" />
|
||||
)}
|
||||
</div>
|
||||
<span className="font-medium text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<span className="font-medium text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Yes, include me in the Members Directory
|
||||
</span>
|
||||
</div>
|
||||
@@ -62,8 +62,8 @@ const RegistrationStep3 = ({ formData, setFormData, handleInputChange }) => {
|
||||
className={`
|
||||
p-4 rounded-xl border-2 cursor-pointer transition-all
|
||||
${!formData.show_in_directory
|
||||
? 'border-[var(--orange-light)] bg-[var(--orange-light)]/5'
|
||||
: 'border-[var(--neutral-800)] hover:border-brand-purple '
|
||||
? 'border-accent bg-accent/5'
|
||||
: 'border-chart-6 hover:border-muted-foreground'
|
||||
}
|
||||
`}
|
||||
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={`
|
||||
w-5 h-5 rounded-full border-2 flex items-center justify-center
|
||||
${!formData.show_in_directory ? 'border-[var(--orange-light)]' : 'border-[var(--neutral-800)]'}
|
||||
${!formData.show_in_directory ? 'border-accent' : 'border-chart-6'}
|
||||
`}>
|
||||
{!formData.show_in_directory && (
|
||||
<div className="w-3 h-3 rounded-full bg-[var(--orange-light)]" />
|
||||
<div className="w-3 h-3 rounded-full bg-accent" />
|
||||
)}
|
||||
</div>
|
||||
<span className="font-medium text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<span className="font-medium text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
No, don't include me in the Members Directory
|
||||
</span>
|
||||
</div>
|
||||
@@ -87,8 +87,8 @@ const RegistrationStep3 = ({ formData, setFormData, handleInputChange }) => {
|
||||
|
||||
{/* Conditional Directory Fields */}
|
||||
{formData.show_in_directory && (
|
||||
<div className="space-y-4 mt-6 p-6 bg-background rounded-xl border border-[var(--neutral-800)]">
|
||||
<p className="text-brand-purple text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="space-y-4 mt-6 p-6 bg-background rounded-xl border border-chart-6">
|
||||
<p className="text-muted-foreground text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
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)
|
||||
</p>
|
||||
@@ -101,7 +101,7 @@ const RegistrationStep3 = ({ formData, setFormData, handleInputChange }) => {
|
||||
type="email"
|
||||
value={formData.directory_email}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -114,7 +114,7 @@ const RegistrationStep3 = ({ formData, setFormData, handleInputChange }) => {
|
||||
onChange={handleInputChange}
|
||||
placeholder="Tell other members about yourself..."
|
||||
rows={4}
|
||||
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -125,7 +125,7 @@ const RegistrationStep3 = ({ formData, setFormData, handleInputChange }) => {
|
||||
name="directory_address"
|
||||
value={formData.directory_address}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -137,7 +137,7 @@ const RegistrationStep3 = ({ formData, setFormData, handleInputChange }) => {
|
||||
type="tel"
|
||||
value={formData.directory_phone}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -149,7 +149,7 @@ const RegistrationStep3 = ({ formData, setFormData, handleInputChange }) => {
|
||||
type="date"
|
||||
value={formData.directory_dob}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -162,7 +162,7 @@ const RegistrationStep3 = ({ formData, setFormData, handleInputChange }) => {
|
||||
name="directory_partner_name"
|
||||
value={formData.directory_partner_name}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ const RegistrationStep4 = ({ formData, handleInputChange }) => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h2 className="text-2xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Account Credentials
|
||||
</h2>
|
||||
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Your email is also your username that you can use to login.
|
||||
Please note you can only login after your application is validated.
|
||||
</p>
|
||||
@@ -28,7 +28,7 @@ const RegistrationStep4 = ({ formData, handleInputChange }) => {
|
||||
value={formData.email}
|
||||
onChange={handleInputChange}
|
||||
placeholder="your.email@example.com"
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
data-testid="email-input"
|
||||
/>
|
||||
</div>
|
||||
@@ -43,10 +43,10 @@ const RegistrationStep4 = ({ formData, handleInputChange }) => {
|
||||
value={formData.password}
|
||||
onChange={handleInputChange}
|
||||
placeholder="At least 6 characters"
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
data-testid="password-input"
|
||||
/>
|
||||
<p className="text-sm text-brand-purple mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Must be at least 6 characters long
|
||||
</p>
|
||||
</div>
|
||||
@@ -60,7 +60,7 @@ const RegistrationStep4 = ({ formData, handleInputChange }) => {
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Re-enter your password"
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
data-testid="confirm-password-input"
|
||||
/>
|
||||
{formData.confirmPassword && formData.password !== formData.confirmPassword && (
|
||||
@@ -71,7 +71,7 @@ const RegistrationStep4 = ({ formData, handleInputChange }) => {
|
||||
</div>
|
||||
|
||||
{/* Terms of Service Acceptance */}
|
||||
<div className="p-4 bg-[var(--lavender-500)] rounded-lg border border-[var(--neutral-800)]">
|
||||
<div className="p-4 bg-chart-7 rounded-lg border border-chart-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -79,7 +79,7 @@ const RegistrationStep4 = ({ formData, handleInputChange }) => {
|
||||
name="accepts_tos"
|
||||
checked={formData.accepts_tos || false}
|
||||
onChange={handleInputChange}
|
||||
className="mt-1 w-4 h-4 text-brand-purple border-gray-300 rounded focus:ring-brand-purple "
|
||||
className="mt-1 w-4 h-4 text-muted-foreground border-gray-300 rounded focus:ring-muted-foreground"
|
||||
required
|
||||
data-testid="tos-checkbox"
|
||||
/>
|
||||
@@ -89,7 +89,7 @@ const RegistrationStep4 = ({ formData, handleInputChange }) => {
|
||||
href="/become-a-member/terms-of-service"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-brand-purple hover:text-[var(--purple-ink)] font-semibold underline"
|
||||
className="text-muted-foreground hover:text-primary font-semibold underline"
|
||||
>
|
||||
Terms of Service
|
||||
</a>
|
||||
@@ -98,7 +98,7 @@ const RegistrationStep4 = ({ formData, handleInputChange }) => {
|
||||
href="become-a-member/privacy-policy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-brand-purple hover:text-[var(--purple-ink)] font-semibold underline"
|
||||
className="text-muted-foreground hover:text-primary font-semibold underline"
|
||||
>
|
||||
Privacy Policy
|
||||
</a>
|
||||
|
||||
@@ -20,17 +20,17 @@ const RegistrationStepIndicator = ({ currentStep, totalSteps = 4 }) => {
|
||||
w-12 h-12 rounded-full flex items-center justify-center font-semibold text-lg
|
||||
transition-all duration-300
|
||||
${currentStep === step.number
|
||||
? 'bg-[var(--orange-light)] text-white scale-110 shadow-lg'
|
||||
? 'bg-accent text-white scale-110 shadow-lg'
|
||||
: currentStep > step.number
|
||||
? 'bg-[var(--green-light)] text-white'
|
||||
: 'bg-[var(--neutral-800)] text-brand-purple '
|
||||
? 'bg-success text-white'
|
||||
: 'bg-chart-6 text-muted-foreground'
|
||||
}
|
||||
`}>
|
||||
{currentStep > step.number ? '✓' : step.number}
|
||||
</div>
|
||||
<span className={`
|
||||
text-sm mt-2 font-medium transition-colors
|
||||
${currentStep === step.number ? 'text-[var(--orange-light)]' : 'text-brand-purple '}
|
||||
${currentStep === step.number ? 'text-accent' : 'text-muted-foreground'}
|
||||
`} style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{step.title}
|
||||
</span>
|
||||
@@ -38,11 +38,11 @@ const RegistrationStepIndicator = ({ currentStep, totalSteps = 4 }) => {
|
||||
|
||||
{/* Connecting Line */}
|
||||
{index < steps.length - 1 && (
|
||||
<div className="flex-1 h-1 mx-2 relative -top-6 bg-[var(--neutral-800)]">
|
||||
<div className="flex-1 h-1 mx-2 relative -top-6 bg-chart-6">
|
||||
<div
|
||||
className={`
|
||||
h-full transition-all duration-500
|
||||
${currentStep > step.number ? 'bg-[var(--green-light)] w-full' : 'bg-transparent w-0'}
|
||||
${currentStep > step.number ? 'bg-success w-full' : 'bg-transparent w-0'}
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
@@ -52,8 +52,8 @@ const RegistrationStepIndicator = ({ currentStep, totalSteps = 4 }) => {
|
||||
</div>
|
||||
|
||||
{/* Step Counter */}
|
||||
<p className="text-center text-brand-purple mt-6 text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Step <span className="font-semibold text-[var(--orange-light)]">{currentStep}</span> of {totalSteps}
|
||||
<p className="text-center text-muted-foreground mt-6 text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Step <span className="font-semibold text-accent">{currentStep}</span> of {totalSteps}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
import * as React from "react"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
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",
|
||||
@@ -15,32 +15,20 @@ const badgeVariants = cva(
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||
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: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
function Badge({ className, variant, ...props }) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}) {
|
||||
return (<div className={cn(badgeVariants({ variant }), className)} {...props} />);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
export { Badge, badgeVariants }
|
||||
|
||||
@@ -1,46 +1,48 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva("btn", {
|
||||
const buttonVariants = cva(
|
||||
"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: {
|
||||
variant: {
|
||||
default: "btn-primary",
|
||||
secondary: "btn-secondary",
|
||||
ghost: "btn-ghost",
|
||||
outline: "btn-outline",
|
||||
"outline-destructive": "btn-outline-destructive",
|
||||
accent: "btn-accent",
|
||||
destructive: "btn-destructive",
|
||||
link: "btn-link",
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "btn-md",
|
||||
sm: "btn-sm",
|
||||
lg: "btn-lg",
|
||||
icon: "btn-icon",
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
});
|
||||
}
|
||||
)
|
||||
|
||||
const Button = React.forwardRef(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size }), className)}
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
})
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants };
|
||||
export { Button, buttonVariants }
|
||||
|
||||
@@ -1,65 +1,50 @@
|
||||
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) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-xl border bg-card text-card-foreground shadow",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Card.displayName = "Card";
|
||||
className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
|
||||
{...props} />
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardHeader.displayName = "CardHeader";
|
||||
{...props} />
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardTitle.displayName = "CardTitle";
|
||||
{...props} />
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardDescription.displayName = "CardDescription";
|
||||
{...props} />
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
));
|
||||
CardContent.displayName = "CardContent";
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardFooter.displayName = "CardFooter";
|
||||
{...props} />
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
};
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
|
||||
@@ -50,7 +50,7 @@ CommandInput.displayName = CommandPrimitive.Input.displayName
|
||||
const CommandList = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto scrollbar-dashboard scrollbar-x-dashboard overflow-x-hidden", className)}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props} />
|
||||
))
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ const ContextMenuContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ContextMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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]",
|
||||
"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]",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
|
||||
@@ -20,7 +20,7 @@ const DropdownMenuSubTrigger = React.forwardRef(({ className, inset, children, .
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
"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",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
@@ -50,7 +50,7 @@ const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 4, ...pr
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"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",
|
||||
"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",
|
||||
"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
|
||||
)}
|
||||
@@ -63,7 +63,7 @@ const DropdownMenuItem = React.forwardRef(({ className, inset, ...props }, ref)
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
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-brand-light-lavender 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-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -7,7 +7,7 @@ const Input = React.forwardRef(({ className, type, ...props }, ref) => {
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"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",
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -19,7 +19,7 @@ const PasswordInput = React.forwardRef(({ className, ...props }, ref) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
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"
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-grey hover:text-[#3D405B] transition-colors focus:outline-none"
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showPassword ? (
|
||||
|
||||
@@ -52,7 +52,7 @@ const SelectContent = React.forwardRef(({ className, children, position = "poppe
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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]",
|
||||
"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]",
|
||||
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",
|
||||
className
|
||||
|
||||
@@ -21,7 +21,7 @@ const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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",
|
||||
"inline-flex items-center justify-center whitespace-nowrap hover:bg-muted border-2 border-muted-foreground rounded-2xl px-3 py-1 text-muted-foreground 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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
250
src/index.css
250
src/index.css
@@ -1,10 +1,244 @@
|
||||
@import "./styles/App.css";
|
||||
@import "./styles/theme.css";
|
||||
@import "./styles/components.css";
|
||||
@import "./styles/base.css";
|
||||
@import "./styles/utilities.css";
|
||||
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap");
|
||||
|
||||
@import url("https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap");
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"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 {
|
||||
/* =========================
|
||||
Brand Colors
|
||||
========================= */
|
||||
--brand-dark-lavender: 267 47% 29%;
|
||||
--brand-purple: 256 35% 47%;
|
||||
--brand-lavender: 262 46% 80%;
|
||||
--brand-light-lavender: 256 32% 88%;
|
||||
--brand-white: 0 0% 100%;
|
||||
|
||||
--brand-dark-orange: 13 100% 42%;
|
||||
--brand-orange: 24 86% 55%;
|
||||
--brand-light-orange: 24 100% 67%;
|
||||
|
||||
--brand-pink: 324 55% 60%;
|
||||
--dusty-pink: 323 39% 52%;
|
||||
--dark-rose: 324 98% 32%;
|
||||
|
||||
/* ==================================
|
||||
Additional Colors (AI generated)
|
||||
================================== */
|
||||
|
||||
--tangerine-dream: 18 74% 65%;
|
||||
--rebecca-purple: 259 39% 39%;
|
||||
--dusty-grape: 257, 35%, 46%; /*#644c9f*/
|
||||
|
||||
/* Muted Teal Family */
|
||||
--muted-teal-base: 150 21% 53%;
|
||||
--muted-teal-balanced: 148 21% 53%; /* better version */
|
||||
--muted-teal-dark: 150 17% 50%; /* darker / desaturated */
|
||||
--muted-teal-light: 151 24% 60%; /* lighter shade */
|
||||
|
||||
--bright-snow: 260 27% 98%; /* bright-snow in repo close to F9F8FC = 255 33% 98%*/
|
||||
--slate-grey: 231 14% 49%;
|
||||
--coral-glow: 15 100% 70%;
|
||||
--lavender-mist: 264 100% 98%;
|
||||
--almond-cream: 31 33% 88%;
|
||||
--lavender-blush: 351 100% 96%;
|
||||
--burnt-peach: 13 68% 63%;
|
||||
--apricot-cream: 37 79% 75%;
|
||||
--ghost-white: 255 33% 98%;
|
||||
|
||||
|
||||
/*
|
||||
=========================
|
||||
End of File
|
||||
========================
|
||||
|
||||
|
||||
#664fa3,
|
||||
#644c9f,
|
||||
#FDFCF8, - porcelain
|
||||
#6FA087,
|
||||
#c5b4e3,
|
||||
#3D405B,
|
||||
#5A8F72,
|
||||
#4f378a,
|
||||
#eeebf4,
|
||||
#F9FAFB,
|
||||
#e8e0f5,
|
||||
#eaedf4,
|
||||
#865edf,
|
||||
#E8F5E9,
|
||||
#DCD7EA,
|
||||
#ddd8eb,
|
||||
#FFFFFF,
|
||||
#422268,
|
||||
#6a9a83,
|
||||
#d16b54,
|
||||
#e8bf7a,
|
||||
#4CAF50,
|
||||
#533a82,
|
||||
#C5BFD9,
|
||||
#FFF3E0,
|
||||
#f9fafb,
|
||||
#f9f7fc,
|
||||
#f1eef9,
|
||||
#fafafa,
|
||||
#DDD8EB,
|
||||
#81B29A,
|
||||
#66927e,
|
||||
#9ca3af,
|
||||
#6b7280,
|
||||
#fb923c,
|
||||
#ea580c,
|
||||
#c4bed8,
|
||||
#1877F2,
|
||||
#E4405F,
|
||||
#1DA1F2,
|
||||
#0A66C2,
|
||||
#ff8c5a,
|
||||
#664ea2,
|
||||
#5a4290,
|
||||
#e88a63
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
/* Theme */
|
||||
--background: 0 0% 100%;
|
||||
--foreground: var(--brand-dark-lavender);
|
||||
--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%;
|
||||
--success: var(--muted-teal-light);
|
||||
--success-foreground: 0 0% 100%;
|
||||
--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%;
|
||||
--chart-6: 256 32% 88%;
|
||||
--chart-7: var(--ghost-white);
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
.dark {
|
||||
--background: var(--brand-dark-lavender);
|
||||
--foreground: var(--bright-snow);
|
||||
--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: var(--brand-purple);
|
||||
--secondary-foreground: 280 47% 27%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: var(--brand-lavender);
|
||||
--accent-foreground: 280 47% 27%;
|
||||
--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%;
|
||||
--chart-6: 0 0% 14.9%;
|
||||
--chart-7: var(--ghost-white);
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
@supports selector(::-webkit-scrollbar) {
|
||||
.scrollbar-dashboard::-webkit-scrollbar {
|
||||
width: 2px;
|
||||
}
|
||||
|
||||
.scrollbar-dashboard::-webkit-scrollbar-thumb {
|
||||
background-color: #ddd8eb;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
.scrollbar-x-dashboard::-webkit-scrollbar:horizontal {
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.scrollbar-x-dashboard::-webkit-scrollbar-thumb:horizontal {
|
||||
background-color: #ddd8eb;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
.hide-scrollbar-x::-webkit-scrollbar:horizontal {
|
||||
height: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ const AdminLayout = ({ children }) => {
|
||||
)}
|
||||
|
||||
{/* Main Content Area */}
|
||||
<main className="flex-1 overflow-y-auto scrollbar-dashboard">
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
<div className="max-w-7xl mx-auto px-6 py-8">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -163,9 +163,9 @@ const AcceptInvitation = () => {
|
||||
|
||||
const getRoleBadge = (role) => {
|
||||
const config = {
|
||||
superadmin: { label: 'Superadmin', className: 'bg-brand-purple text-white' },
|
||||
admin: { label: 'Admin', className: 'bg-[var(--green-light)] text-white' },
|
||||
member: { label: 'Member', className: 'bg-[var(--neutral-800)] text-[var(--purple-ink)]' }
|
||||
superadmin: { label: 'Superadmin', className: 'bg-muted-foreground text-white' },
|
||||
admin: { label: 'Admin', className: 'bg-success text-white' },
|
||||
member: { label: 'Member', className: 'bg-chart-6 text-primary' }
|
||||
};
|
||||
|
||||
const roleConfig = config[role] || { label: role, className: 'bg-gray-500 text-white' };
|
||||
@@ -179,10 +179,10 @@ const AcceptInvitation = () => {
|
||||
|
||||
if (loading) {
|
||||
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-md p-12 bg-background rounded-2xl border border-[var(--neutral-800)] text-center">
|
||||
<Loader2 className="h-12 w-12 text-brand-purple mx-auto mb-4 animate-spin" />
|
||||
<p className="text-lg text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<div className="min-h-screen bg-gradient-to-br from-bright-snow to-white flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md p-12 bg-background rounded-2xl border border-chart-6 text-center">
|
||||
<Loader2 className="h-12 w-12 text-muted-foreground mx-auto mb-4 animate-spin" />
|
||||
<p className="text-lg text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Verifying your invitation...
|
||||
</p>
|
||||
</Card>
|
||||
@@ -192,18 +192,18 @@ const AcceptInvitation = () => {
|
||||
|
||||
if (error) {
|
||||
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-md p-12 bg-background rounded-2xl border border-[var(--neutral-800)] text-center">
|
||||
<div className="min-h-screen bg-gradient-to-br from-bright-snow to-white flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md p-12 bg-background rounded-2xl border border-chart-6 text-center">
|
||||
<XCircle className="h-16 w-16 text-red-500 mx-auto mb-6" />
|
||||
<h1 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-2xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Invalid Invitation
|
||||
</h1>
|
||||
<p className="text-brand-purple mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{error}
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => navigate('/login')}
|
||||
className="rounded-xl bg-brand-purple hover:bg-[var(--purple-ink)] text-white"
|
||||
className="rounded-xl bg-muted-foreground hover:bg-primary text-white"
|
||||
>
|
||||
Go to Login
|
||||
</Button>
|
||||
@@ -216,53 +216,53 @@ const AcceptInvitation = () => {
|
||||
const redirectPath = successUser?.role === 'admin' || successUser?.role === 'superadmin' ? '/admin' : '/dashboard';
|
||||
|
||||
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">
|
||||
<div className="min-h-screen bg-gradient-to-br from-bright-snow to-white flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-2xl p-12 bg-background rounded-2xl border border-chart-6 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">
|
||||
<div className="h-24 w-24 mx-auto rounded-full bg-gradient-to-br from-success to-muted-teal-balanced 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" }}>
|
||||
<h1 className="text-4xl font-semibold text-primary 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" }}>
|
||||
<p className="text-xl text-muted-foreground 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="mb-8 p-6 bg-gradient-to-r from-chart-6 to-bright-snow 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" }}>
|
||||
<p className="text-sm text-muted-foreground mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Name
|
||||
</p>
|
||||
<p className="font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="font-semibold text-primary" 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" }}>
|
||||
<p className="text-sm text-muted-foreground mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Email
|
||||
</p>
|
||||
<p className="font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="font-semibold text-primary" 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" }}>
|
||||
<p className="text-sm text-muted-foreground 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" }}>
|
||||
<p className="text-sm text-muted-foreground 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">
|
||||
<Badge className="bg-success text-white px-4 py-2 rounded-full text-sm">
|
||||
{successUser?.status}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -280,7 +280,7 @@ const AcceptInvitation = () => {
|
||||
{/* 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"
|
||||
className="w-full h-14 rounded-xl bg-gradient-to-r from-success to-muted-teal-balanced hover:from-muted-teal-balanced hover:to-[#5A8F72] text-white text-lg font-semibold"
|
||||
>
|
||||
Continue to Dashboard
|
||||
</Button>
|
||||
@@ -290,46 +290,46 @@ const AcceptInvitation = () => {
|
||||
}
|
||||
|
||||
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)]">
|
||||
<div className="min-h-screen bg-gradient-to-br from-bright-snow 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-chart-6">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="h-16 w-16 rounded-full bg-gradient-to-br from-brand-purple to-[var(--purple-ink)] flex items-center justify-center">
|
||||
<div className="h-16 w-16 rounded-full bg-gradient-to-br from-muted-foreground to-primary flex items-center justify-center">
|
||||
<Mail className="h-8 w-8 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-3xl md:text-4xl font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-3xl md:text-4xl font-semibold text-primary mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Welcome to LOAF!
|
||||
</h1>
|
||||
<p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Complete your profile to accept the invitation
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Invitation Details */}
|
||||
<div className="mb-8 p-6 bg-gradient-to-r from-[var(--neutral-800)] to-[var(--lavender-700)] rounded-xl">
|
||||
<div className="mb-8 p-6 bg-gradient-to-r from-chart-6 to-bright-snow rounded-xl">
|
||||
<div className="grid md:grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Email Address
|
||||
</p>
|
||||
<p className="font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{invitation?.email}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Role
|
||||
</p>
|
||||
<div>{getRoleBadge(invitation?.role)}</div>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<p className="text-brand-purple mb-1 flex items-center gap-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mb-1 flex items-center gap-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Calendar className="h-4 w-4" />
|
||||
Invitation Expires
|
||||
</p>
|
||||
<p className="font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{invitation?.expires_at ? new Date(invitation.expires_at).toLocaleString() : 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
@@ -342,7 +342,7 @@ const AcceptInvitation = () => {
|
||||
{/* Password Fields */}
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password" className="text-[var(--purple-ink)]">
|
||||
<Label htmlFor="password" className="text-primary">
|
||||
Password <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
@@ -350,7 +350,7 @@ const AcceptInvitation = () => {
|
||||
type="password"
|
||||
value={formData.password}
|
||||
onChange={(e) => handleChange('password', e.target.value)}
|
||||
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="Minimum 8 characters"
|
||||
/>
|
||||
{formErrors.password && (
|
||||
@@ -359,7 +359,7 @@ const AcceptInvitation = () => {
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="confirmPassword" className="text-[var(--purple-ink)]">
|
||||
<Label htmlFor="confirmPassword" className="text-primary">
|
||||
Confirm Password <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
@@ -367,7 +367,7 @@ const AcceptInvitation = () => {
|
||||
type="password"
|
||||
value={formData.confirmPassword}
|
||||
onChange={(e) => handleChange('confirmPassword', e.target.value)}
|
||||
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="Re-enter password"
|
||||
/>
|
||||
{formErrors.confirmPassword && (
|
||||
@@ -379,14 +379,14 @@ const AcceptInvitation = () => {
|
||||
{/* Name Fields */}
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="first_name" className="text-[var(--purple-ink)]">
|
||||
<Label htmlFor="first_name" className="text-primary">
|
||||
First Name <span className="text-red-500">*</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 "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="John"
|
||||
/>
|
||||
{formErrors.first_name && (
|
||||
@@ -395,14 +395,14 @@ const AcceptInvitation = () => {
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="last_name" className="text-[var(--purple-ink)]">
|
||||
<Label htmlFor="last_name" className="text-primary">
|
||||
Last Name <span className="text-red-500">*</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 "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="Doe"
|
||||
/>
|
||||
{formErrors.last_name && (
|
||||
@@ -413,7 +413,7 @@ const AcceptInvitation = () => {
|
||||
|
||||
{/* Phone */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="phone" className="text-[var(--purple-ink)]">
|
||||
<Label htmlFor="phone" className="text-primary">
|
||||
Phone <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
@@ -421,7 +421,7 @@ const AcceptInvitation = () => {
|
||||
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 "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="(555) 123-4567"
|
||||
/>
|
||||
{formErrors.phone && (
|
||||
@@ -432,20 +432,20 @@ const AcceptInvitation = () => {
|
||||
{/* Optional Fields Section */}
|
||||
{invitation?.role === 'member' && (
|
||||
<>
|
||||
<div className="border-t border-[var(--neutral-800)] pt-6 mt-2">
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<div className="border-t border-chart-6 pt-6 mt-2">
|
||||
<h3 className="text-lg font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Additional Information (Optional)
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Address */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="address" className="text-[var(--purple-ink)]">Address</Label>
|
||||
<Label htmlFor="address" className="text-primary">Address</Label>
|
||||
<Input
|
||||
id="address"
|
||||
value={formData.address}
|
||||
onChange={(e) => handleChange('address', e.target.value)}
|
||||
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="123 Main St"
|
||||
/>
|
||||
</div>
|
||||
@@ -453,35 +453,35 @@ const AcceptInvitation = () => {
|
||||
{/* City, State, Zipcode */}
|
||||
<div className="grid md:grid-cols-3 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="city" className="text-[var(--purple-ink)]">City</Label>
|
||||
<Label htmlFor="city" className="text-primary">City</Label>
|
||||
<Input
|
||||
id="city"
|
||||
value={formData.city}
|
||||
onChange={(e) => handleChange('city', e.target.value)}
|
||||
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="San Francisco"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="state" className="text-[var(--purple-ink)]">State</Label>
|
||||
<Label htmlFor="state" className="text-primary">State</Label>
|
||||
<Input
|
||||
id="state"
|
||||
value={formData.state}
|
||||
onChange={(e) => handleChange('state', e.target.value)}
|
||||
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="CA"
|
||||
maxLength={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="zipcode" className="text-[var(--purple-ink)]">Zipcode</Label>
|
||||
<Label htmlFor="zipcode" className="text-primary">Zipcode</Label>
|
||||
<Input
|
||||
id="zipcode"
|
||||
value={formData.zipcode}
|
||||
onChange={(e) => handleChange('zipcode', e.target.value)}
|
||||
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="94102"
|
||||
/>
|
||||
</div>
|
||||
@@ -489,13 +489,13 @@ const AcceptInvitation = () => {
|
||||
|
||||
{/* Date of Birth */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="date_of_birth" className="text-[var(--purple-ink)]">Date of Birth</Label>
|
||||
<Label htmlFor="date_of_birth" className="text-primary">Date of Birth</Label>
|
||||
<Input
|
||||
id="date_of_birth"
|
||||
type="date"
|
||||
value={formData.date_of_birth}
|
||||
onChange={(e) => handleChange('date_of_birth', e.target.value)}
|
||||
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -507,7 +507,7 @@ const AcceptInvitation = () => {
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
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"
|
||||
className="w-full h-14 rounded-xl bg-gradient-to-r from-success to-muted-teal-balanced hover:from-muted-teal-balanced hover:to-[#5A8F72] text-white text-lg font-semibold"
|
||||
>
|
||||
{submitting ? (
|
||||
<>
|
||||
@@ -526,11 +526,11 @@ const AcceptInvitation = () => {
|
||||
|
||||
{/* Footer Note */}
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Already have an account?{' '}
|
||||
<button
|
||||
onClick={() => navigate('/login')}
|
||||
className="text-brand-purple hover:text-[var(--purple-ink)] font-semibold underline"
|
||||
className="text-muted-foreground hover:text-primary font-semibold underline"
|
||||
>
|
||||
Sign in instead
|
||||
</button>
|
||||
|
||||
@@ -16,7 +16,7 @@ const BecomeMember = () => {
|
||||
|
||||
const Arrow = ({ ...props }) => (
|
||||
<div className="flex justify-center mb-2">
|
||||
<ArrowDown className="size-8 text---brand-purple font-bold" strokeWidth={2} />
|
||||
<ArrowDown className="size-8 text-[#4f378a] font-bold" strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
@@ -29,13 +29,13 @@ const BecomeMember = () => {
|
||||
<div className="relative bg-gray-50 pt-20 px-6 pb-16">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
<h1
|
||||
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]"
|
||||
className="text-3xl sm:text-4xl md:text-5xl font-bold text-foreground mb-6 sm:mb-8 leading-[1.2] tracking-[-0.96px]"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
Become a Member
|
||||
</h1>
|
||||
<p
|
||||
className="text-base sm:text-lg font-medium text-[var(--purple-deep)] max-w-2xl mx-auto leading-[1.6]"
|
||||
className="text-base sm:text-lg font-medium text-foreground max-w-2xl mx-auto leading-[1.6]"
|
||||
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
|
||||
@@ -53,15 +53,15 @@ const BecomeMember = () => {
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<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">
|
||||
<div className="flex-1 bg-[#eeebf4] rounded-[25px] px-4 py-6 sm:px-6 sm:py-7 md:px-8 md:py-8">
|
||||
<h3
|
||||
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]"
|
||||
className="text-xl sm:text-2xl md:text-[32px] font-semibold text-foreground mb-3 sm:mb-4 md:mb-5 leading-[1.49]"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
Annual Administrative Fees
|
||||
</h3>
|
||||
<p
|
||||
className="text-base sm:text-lg font-medium text-[var(--purple-deep)] leading-[1.6]"
|
||||
className="text-base sm:text-lg font-medium text-foreground leading-[1.6]"
|
||||
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).
|
||||
@@ -71,7 +71,7 @@ const BecomeMember = () => {
|
||||
</div>
|
||||
|
||||
{/* Membership Process Section */}
|
||||
<div className="relative bg-gray-50 py-32 bg-gradient-to-bl from-[var(--neutral-100:)] to-[var(--neutral-800)] ">
|
||||
<div className="relative bg-gray-50 py-32 bg-gradient-to-bl from-[#F9FAFB] to-chart-6 ">
|
||||
{/* Decorative shooting star element */}
|
||||
<div className="hidden lg:block absolute left-0 top-64 w-[195px] h-[1130px] pointer-events-none opacity-50">
|
||||
<img
|
||||
@@ -82,13 +82,13 @@ const BecomeMember = () => {
|
||||
</div>
|
||||
<div className="max-w-7xl mx-auto px-6 mb-24 text-center">
|
||||
<h2
|
||||
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]"
|
||||
className="text-2xl sm:text-3xl md:text-[40px] font-bold text-foreground mb-6 sm:mb-8 leading-[1.2] tracking-[-0.8px]"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
Membership Process
|
||||
</h2>
|
||||
<p
|
||||
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]"
|
||||
className="text-base sm:text-lg md:text-xl lg:text-2xl lg:font-semibold font-medium text-foreground max-w-2xl mx-auto leading-[1.6]"
|
||||
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:
|
||||
@@ -108,13 +108,13 @@ const BecomeMember = () => {
|
||||
</div>
|
||||
<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
|
||||
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]"
|
||||
className="text-xl sm:text-2xl md:text-[32px] font-semibold text-foreground mb-3 sm:mb-4 md:mb-5 leading-[1.49]"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
Step 1: Application & Email Confirmation
|
||||
</h3>
|
||||
<p
|
||||
className="text-base sm:text-lg font-medium text-[var(--purple-deep)] leading-[1.6]"
|
||||
className="text-base sm:text-lg font-medium text-foreground leading-[1.6]"
|
||||
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.
|
||||
@@ -138,13 +138,13 @@ const BecomeMember = () => {
|
||||
</div>
|
||||
<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
|
||||
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]"
|
||||
className="text-xl sm:text-2xl md:text-[32px] font-semibold text-foreground mb-3 sm:mb-4 md:mb-5 leading-[1.49]"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
Step 2: Attend an event and meet us!
|
||||
</h3>
|
||||
<p
|
||||
className="text-base sm:text-lg font-medium text-[var(--purple-deep)] leading-[1.6]"
|
||||
className="text-base sm:text-lg font-medium text-foreground leading-[1.6]"
|
||||
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).
|
||||
@@ -167,13 +167,13 @@ const BecomeMember = () => {
|
||||
</div>
|
||||
<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
|
||||
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]"
|
||||
className="text-xl sm:text-2xl md:text-[32px] font-semibold text-foreground mb-3 sm:mb-4 md:mb-5 leading-[1.49]"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
Step 3: Login and pay the annual fee
|
||||
</h3>
|
||||
<p
|
||||
className="text-base sm:text-lg font-medium text-[var(--purple-deep)] leading-[1.6]"
|
||||
className="text-base sm:text-lg font-medium text-foreground leading-[1.6]"
|
||||
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.
|
||||
@@ -195,7 +195,7 @@ const BecomeMember = () => {
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<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">
|
||||
<div className="flex-1 bg-gradient-to-r from-foreground to-muted-foreground rounded-[25px] px-4 py-6 sm:px-6 sm:py-7 md:px-8 md:py-8">
|
||||
<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]"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
@@ -216,14 +216,14 @@ const BecomeMember = () => {
|
||||
<div className="relative bg-gray-50 py-16 ">
|
||||
<div className="max-w-7xl mx-auto px-6 flex flex-col md:flex-row items-center justify-center gap-12 text-center">
|
||||
<h2
|
||||
className="text-2xl sm:text-3xl md:text-[40px] content-center font-bold text-[var(--purple-deep)] leading-[1.2] tracking-[-0.8px]"
|
||||
className="text-2xl sm:text-3xl md:text-[40px] content-center font-bold text-foreground leading-[1.2] tracking-[-0.8px]"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
Ready to Join Us?
|
||||
</h2>
|
||||
<Link to="/register">
|
||||
<Button
|
||||
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"
|
||||
className="bg-muted-foreground text-white hover:bg-foreground 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" }}
|
||||
>
|
||||
Register Now!
|
||||
|
||||
@@ -25,7 +25,7 @@ const BoardOfDirectors = () => {
|
||||
<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"
|
||||
className="text-2xl sm:text-4xl font-bold text-foreground text-center mb-8"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
{title}
|
||||
@@ -43,10 +43,10 @@ const BoardOfDirectors = () => {
|
||||
<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"
|
||||
className="bg-[#eeebf4] text-foreground 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)]">
|
||||
<p className="text-xl font-bold text-foreground">
|
||||
{name}
|
||||
</p>
|
||||
|
||||
@@ -69,11 +69,11 @@ const BoardOfDirectors = () => {
|
||||
<div className="min-h-screen bg-background">
|
||||
<PublicNavbar />
|
||||
|
||||
<main className="bg-gradient-to-b from-[var(--neutral-100:)] to-[var(--neutral-800)] pt-8 sm:pt-10 md:pt-12">
|
||||
<main className="bg-gradient-to-b from-[#f9fafb] to-chart-6 pt-8 sm:pt-10 md:pt-12">
|
||||
{/* Hero Section */}
|
||||
<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">
|
||||
<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" }}>
|
||||
<h1 className="text-2xl sm:text-3xl md:text-[40px] leading-[1.2] text-foreground font-bold mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
LOAF Board of Directors 2025
|
||||
</h1>
|
||||
|
||||
@@ -81,11 +81,11 @@ const BoardOfDirectors = () => {
|
||||
</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">
|
||||
<div className=" w-full text-center px-8 justify-center bg-gradient-to-r from-muted-foreground to-foreground 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{' '}
|
||||
<a href="mailto:info@loaftx.org" className="text-[var(--neutral-500)] underline font-bold hover:text-white transition-colors">
|
||||
<a href="mailto:info@loaftx.org" className="text-[#c5b4e3] underline font-bold hover:text-white transition-colors">
|
||||
info@loaftx.org
|
||||
</a>
|
||||
</p>
|
||||
@@ -105,19 +105,19 @@ const BoardOfDirectors = () => {
|
||||
<section className="py-12 bg-background mt-12 ">
|
||||
{/* content containter */}
|
||||
<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" }}>
|
||||
<h2 className="text-xl mx-auto sm:text-2xl md:text-4xl font-bold text-foreground text-center mb-8" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
Join the Board of Directors
|
||||
</h2>
|
||||
<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" }}>
|
||||
<p className="lg:text-2xl text-md md:text-lg max-w-4xl mx-auto justify-center font-bold text-foreground text-center mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
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.
|
||||
</p>
|
||||
{/* card */}
|
||||
<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" }}>
|
||||
<Card className="bg-[#eeebf4] p-8 rounded-2xl shadow-lg mx-auto border border-white/70">
|
||||
<ol className="list-decimal list-inside space-y-4 text-lg text-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<li>
|
||||
Nominations are due by November 1. Nomination Form:{' '}
|
||||
<a href="https://docs.google.com/forms/d/e/1FAIpQLSfNomination" target="_blank" rel="noopener noreferrer"
|
||||
className="text-[var(--purple-lavender)] underline hover:text-[var(--purple-deep)] transition-colors">
|
||||
className="text-muted-foreground underline hover:text-foreground transition-colors">
|
||||
Click Here
|
||||
</a>
|
||||
</li>
|
||||
@@ -139,7 +139,7 @@ const BoardOfDirectors = () => {
|
||||
href="https://loaftx.org/bylaws/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[var(--purple-deep)] underline"
|
||||
className="text-foreground underline"
|
||||
>
|
||||
https://loaftx.org/bylaws/
|
||||
</a>
|
||||
|
||||
@@ -87,15 +87,15 @@ const ChangePasswordRequired = () => {
|
||||
<Navbar />
|
||||
|
||||
<div className="max-w-md mx-auto px-6 py-12">
|
||||
<Card className="p-8 md:p-12 bg-background rounded-2xl border border-[var(--neutral-800)] shadow-lg">
|
||||
<Card className="p-8 md:p-12 bg-background rounded-2xl border border-chart-6 shadow-lg">
|
||||
<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-lavender-blush mb-4">
|
||||
<AlertTriangle className="h-8 w-8 text-orange-500" />
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Password Change Required
|
||||
</h1>
|
||||
<p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Your password was reset by an administrator. Please create a new password to continue.
|
||||
</p>
|
||||
</div>
|
||||
@@ -111,7 +111,7 @@ const ChangePasswordRequired = () => {
|
||||
value={formData.currentPassword}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter temporary password"
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -125,7 +125,7 @@ const ChangePasswordRequired = () => {
|
||||
value={formData.newPassword}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter new password (min. 6 characters)"
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -139,15 +139,15 @@ const ChangePasswordRequired = () => {
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Re-enter new password"
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-[var(--lavender-300)] border-l-4 border-brand-purple p-4 rounded-lg">
|
||||
<div className="bg-muted border-l-4 border-muted-foreground p-4 rounded-lg">
|
||||
<div className="flex items-start">
|
||||
<Lock className="h-5 w-5 text-brand-purple mr-2 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="font-medium text-[var(--purple-ink)] mb-1">Password Requirements:</p>
|
||||
<Lock className="h-5 w-5 text-muted-foreground mr-2 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="font-medium text-primary mb-1">Password Requirements:</p>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
<li>At least 6 characters long</li>
|
||||
<li>Both passwords must match</li>
|
||||
@@ -159,17 +159,17 @@ const ChangePasswordRequired = () => {
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
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"
|
||||
className="w-full bg-chart-6 text-primary 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'}
|
||||
<ArrowRight className="ml-2 h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
<div className="text-center pt-4 border-t border-[var(--neutral-800)]">
|
||||
<div className="text-center pt-4 border-t border-chart-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLogout}
|
||||
className="text-brand-purple hover:text-[var(--orange-light)] text-sm underline"
|
||||
className="text-muted-foreground hover:text-accent text-sm underline"
|
||||
>
|
||||
Logout instead
|
||||
</button>
|
||||
|
||||
@@ -99,12 +99,12 @@ const ContactUs = () => {
|
||||
<div className="min-h-screen bg-background">
|
||||
<PublicNavbar />
|
||||
|
||||
<main className="bg-gradient-to-b from-[var(--lavender-100)] to-[var(--lavender-300)] px-6 py-16">
|
||||
<main className="bg-gradient-to-b from-[#e8e0f5] to-muted px-6 py-16">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8">
|
||||
{/* Contact Form */}
|
||||
<Card className="p-8 bg-background rounded-2xl">
|
||||
<h1 className="text-2xl sm:text-[28px] leading-5 font-bold text-[var(--purple-deep)] mb-12" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
<h1 className="text-2xl sm:text-[28px] leading-5 font-bold text-foreground mb-12" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
Contact Form
|
||||
</h1>
|
||||
|
||||
@@ -112,7 +112,7 @@ const ContactUs = () => {
|
||||
{/* First Name & Last Name */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="firstName" className="text-[var(--purple-deep)] font-medium mb-2 block" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Label htmlFor="firstName" className="text-foreground font-medium mb-2 block" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
First Name <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
@@ -120,13 +120,13 @@ const ContactUs = () => {
|
||||
name="firstName"
|
||||
value={formData.firstName}
|
||||
onChange={handleChange}
|
||||
className="border-2 border-[var(--neutral-800)] bg-[var(--lavender-800)] focus:border-[var(--purple-lavender)] rounded-full h-12 px-4"
|
||||
className="border-2 border-chart-6 bg-[#eaedf4] focus:border-muted-foreground rounded-full h-12 px-4"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="lastName" className="text-[var(--purple-deep)] font-medium mb-2 block" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Label htmlFor="lastName" className="text-foreground font-medium mb-2 block" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Last Name <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
@@ -134,7 +134,7 @@ const ContactUs = () => {
|
||||
name="lastName"
|
||||
value={formData.lastName}
|
||||
onChange={handleChange}
|
||||
className="border-2 bg-[var(--lavender-800)] border-[var(--neutral-800)] focus:border-[var(--purple-lavender)] rounded-full h-12 px-4"
|
||||
className="border-2 bg-[#eaedf4] border-chart-6 focus:border-muted-foreground rounded-full h-12 px-4"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
required
|
||||
/>
|
||||
@@ -143,7 +143,7 @@ const ContactUs = () => {
|
||||
|
||||
{/* Email */}
|
||||
<div>
|
||||
<Label htmlFor="email" className="text-[var(--purple-deep)] font-medium mb-2 block" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Label htmlFor="email" className="text-foreground font-medium mb-2 block" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Email <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
@@ -152,7 +152,7 @@ const ContactUs = () => {
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
className="border-2 bg-[var(--lavender-800)] border-[var(--neutral-800)] focus:border-[var(--purple-lavender)] rounded-full h-12 px-4"
|
||||
className="border-2 bg-[#eaedf4] border-chart-6 focus:border-muted-foreground rounded-full h-12 px-4"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
required
|
||||
/>
|
||||
@@ -160,7 +160,7 @@ const ContactUs = () => {
|
||||
|
||||
{/* Subject */}
|
||||
<div>
|
||||
<Label htmlFor="subject" className="text-[var(--purple-deep)] font-medium mb-2 block" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Label htmlFor="subject" className="text-foreground font-medium mb-2 block" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Subject <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
@@ -168,7 +168,7 @@ const ContactUs = () => {
|
||||
name="subject"
|
||||
value={formData.subject}
|
||||
onChange={handleChange}
|
||||
className="border-2 bg-[var(--lavender-800)] border-[var(--neutral-800)] focus:border-[var(--purple-lavender)] rounded-full h-12 px-4"
|
||||
className="border-2 bg-[#eaedf4] border-chart-6 focus:border-muted-foreground rounded-full h-12 px-4"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
required
|
||||
/>
|
||||
@@ -176,7 +176,7 @@ const ContactUs = () => {
|
||||
|
||||
{/* Message */}
|
||||
<div>
|
||||
<Label htmlFor="message" className="text-[var(--purple-deep)] font-medium mb-2 block" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Label htmlFor="message" className="text-foreground font-medium mb-2 block" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Your Message <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
@@ -184,7 +184,7 @@ const ContactUs = () => {
|
||||
name="message"
|
||||
value={formData.message}
|
||||
onChange={handleChange}
|
||||
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"
|
||||
className="border-2 bg-[#eaedf4] border-chart-6 focus:border-muted-foreground rounded-2xl min-h-[150px] px-4 py-3 resize-none"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
required
|
||||
/>
|
||||
@@ -196,9 +196,9 @@ const ContactUs = () => {
|
||||
id="consent"
|
||||
checked={formData.consent}
|
||||
onCheckedChange={handleConsentChange}
|
||||
className="mt-1 border-2 border-[var(--neutral-800)] data-[state=checked]:bg-[var(--purple-lavender)] data-[state=checked]:border-[var(--purple-lavender)]"
|
||||
className="mt-1 border-2 border-chart-6 data-[state=checked]:bg-muted-foreground data-[state=checked]:border-muted-foreground"
|
||||
/>
|
||||
<Label htmlFor="consent" className="text-[var(--purple-deep)] text-sm font-normal cursor-pointer" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Label htmlFor="consent" className="text-foreground 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>
|
||||
</Label>
|
||||
</div>
|
||||
@@ -207,7 +207,7 @@ const ContactUs = () => {
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
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"
|
||||
className="w-full bg-muted-foreground hover:bg-foreground text-white rounded-full py-6 text-lg font-semibold disabled:opacity-50"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
{loading ? (
|
||||
@@ -225,7 +225,7 @@ const ContactUs = () => {
|
||||
{/* Contact Information */}
|
||||
<div className="space-y-6">
|
||||
{/* Message Card */}
|
||||
<Card className="p-8 bg-gradient-to-r from-[var(--purple-lavender)] to-[var(--purple-deep)] rounded-2xl shadow-lg text-white">
|
||||
<Card className="p-8 bg-gradient-to-r from-muted-foreground to-foreground rounded-2xl shadow-lg text-white">
|
||||
<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.
|
||||
</p>
|
||||
@@ -235,12 +235,12 @@ const ContactUs = () => {
|
||||
<Card className="p-6 bg-background rounded-2xl">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center justify-center flex-shrink-0">
|
||||
<Mail className="size-12 text-[var(--purple-lavender)]" />
|
||||
<Mail className="size-12 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="mailto:info@loaftx.org"
|
||||
className="text-[var(--purple-electric)] text-xl font-semibold hover:text-[var(--purple-deep)] transition-colors"
|
||||
className="text-[#865edf] text-xl font-semibold hover:text-foreground transition-colors"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
info@loaftx.org
|
||||
@@ -253,13 +253,13 @@ const ContactUs = () => {
|
||||
<Card className="p-6 bg-background rounded-2xl ">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center justify-center flex-shrink-0">
|
||||
<PiMapTrifoldBold className="size-12 text-[var(--purple-lavender)]" />
|
||||
<PiMapTrifoldBold className="size-12 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[var(--purple-deep)] text-[28px] font-semibold mb-1" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
<p className="text-foreground text-[28px] font-semibold mb-1" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
LOAF
|
||||
</p>
|
||||
<p className="text-[var(--purple-deep)] text-[28px] font-semibold leading-relaxed" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
<p className="text-foreground text-[28px] font-semibold leading-relaxed" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
P.O. Box 7207<br />
|
||||
Houston, Texas 77248-7207
|
||||
</p>
|
||||
|
||||
@@ -17,7 +17,6 @@ const Dashboard = () => {
|
||||
const [resendLoading, setResendLoading] = useState(false);
|
||||
const [eventActivity, setEventActivity] = useState(null);
|
||||
const [activityLoading, setActivityLoading] = useState(true);
|
||||
const joinedDate = user?.member_since || user?.created_at;
|
||||
|
||||
useEffect(() => {
|
||||
fetchUpcomingEvents();
|
||||
@@ -75,9 +74,9 @@ const Dashboard = () => {
|
||||
const statusConfig = {
|
||||
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' },
|
||||
pre_validated: { icon: CheckCircle, label: 'Pre-Validated', className: 'bg-[var(--green-light)] text-white' },
|
||||
pre_validated: { icon: CheckCircle, label: 'Pre-Validated', className: 'bg-success text-white' },
|
||||
payment_pending: { icon: AlertCircle, label: 'Payment Pending', className: 'bg-orange-500 text-white' },
|
||||
active: { icon: CheckCircle, label: 'Active', className: 'bg-[var(--green-light)] text-white' },
|
||||
active: { icon: CheckCircle, label: 'Active', className: 'bg-success 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' },
|
||||
expired: { icon: Clock, label: 'Expired', className: 'bg-red-500 text-white' },
|
||||
@@ -118,24 +117,24 @@ const Dashboard = () => {
|
||||
<div className="max-w-7xl mx-auto px-6 py-12">
|
||||
{/* Welcome Section */}
|
||||
<div className="mb-12">
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Welcome Back, {user?.first_name}!
|
||||
</h1>
|
||||
<p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Here's an overview of your membership status and upcoming events.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Email Verification Alert */}
|
||||
{user && !user.email_verified && (
|
||||
<Card className="p-6 bg-[var(--lavender-300)] border-2 border-brand-purple mb-8">
|
||||
<Card className="p-6 bg-muted border-2 border-muted-foreground mb-8">
|
||||
<div className="flex items-start gap-4">
|
||||
<AlertCircle className="h-6 w-6 text-brand-purple flex-shrink-0 mt-1" />
|
||||
<AlertCircle className="h-6 w-6 text-muted-foreground flex-shrink-0 mt-1" />
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-lg font-semibold text-primary mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Verify Your Email Address
|
||||
</h3>
|
||||
<p className="text-brand-purple mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Please verify your email address to complete your registration.
|
||||
Check your inbox for the verification link.
|
||||
</p>
|
||||
@@ -143,7 +142,7 @@ const Dashboard = () => {
|
||||
onClick={handleResendVerification}
|
||||
disabled={resendLoading}
|
||||
variant="outline"
|
||||
className="border-2 border-brand-purple text-brand-purple hover:bg-brand-purple hover:text-white"
|
||||
className="border-2 border-muted-foreground text-muted-foreground hover:bg-muted-foreground hover:text-white"
|
||||
>
|
||||
<Mail className="h-4 w-4 mr-2" />
|
||||
{resendLoading ? 'Sending...' : 'Resend Verification Email'}
|
||||
@@ -154,26 +153,26 @@ const Dashboard = () => {
|
||||
)}
|
||||
|
||||
{/* Status Card */}
|
||||
<Card className="p-8 bg-background rounded-2xl border border-[var(--neutral-800)] shadow-lg mb-8" data-testid="status-card">
|
||||
<Card className="p-8 bg-background rounded-2xl border border-chart-6 shadow-lg mb-8" data-testid="status-card">
|
||||
<div className="flex items-start justify-between flex-wrap gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h2 className="text-2xl font-semibold text-primary mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Membership Status
|
||||
</h2>
|
||||
<div className="mb-4">
|
||||
{getStatusBadge(user?.status)}
|
||||
</div>
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{getStatusMessage(user?.status)}
|
||||
</p>
|
||||
</div>
|
||||
<Link to="/profile">
|
||||
<Button
|
||||
className="btn-lavender"
|
||||
className="bg-chart-6 text-primary hover:bg-background rounded-full px-6"
|
||||
data-testid="view-profile-button"
|
||||
>
|
||||
<User className="h-4 w-4 mr-2" />
|
||||
Edit Profile
|
||||
View Profile
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -182,36 +181,36 @@ const Dashboard = () => {
|
||||
{/* Grid Layout */}
|
||||
<div className="grid lg:grid-cols-3 gap-8">
|
||||
{/* Quick Stats */}
|
||||
<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" }}>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6" data-testid="quick-stats-card">
|
||||
<h3 className="text-xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Quick Info
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<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>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Email</p>
|
||||
<p className="text-primary font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{user?.email}</p>
|
||||
</div>
|
||||
<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>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Role</p>
|
||||
<p className="text-primary font-medium capitalize" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{user?.role}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Member Since</p>
|
||||
<p className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Member Since</p>
|
||||
<p className="text-primary font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{user?.created_at ? new Date(user.created_at).toLocaleDateString() : 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
{user?.subscription_start_date && user?.subscription_end_date && (
|
||||
<>
|
||||
<div className="pt-4 border-t border-[var(--neutral-800)]">
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Membership Period</p>
|
||||
<p className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="pt-4 border-t border-chart-6">
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Membership Period</p>
|
||||
<p className="text-primary font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{new Date(user.subscription_start_date).toLocaleDateString()} - {new Date(user.subscription_end_date).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Days Remaining</p>
|
||||
<p className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Days Remaining</p>
|
||||
<p className="text-primary 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
|
||||
</p>
|
||||
</div>
|
||||
@@ -221,14 +220,15 @@ const Dashboard = () => {
|
||||
</Card>
|
||||
|
||||
{/* Upcoming Events */}
|
||||
<Card className="lg:col-span-2 p-6 bg-background rounded-2xl border border-[var(--neutral-800)]" data-testid="upcoming-events-card">
|
||||
<Card className="lg:col-span-2 p-6 bg-background rounded-2xl border border-chart-6" data-testid="upcoming-events-card">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h3 className="text-xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Upcoming Events
|
||||
</h3>
|
||||
<Link to="/events">
|
||||
<Button
|
||||
className="btn-lavender "
|
||||
variant="ghost"
|
||||
className="text-accent hover:text-muted-foreground"
|
||||
data-testid="view-all-events-button"
|
||||
>
|
||||
View All
|
||||
@@ -237,26 +237,26 @@ const Dashboard = () => {
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading events...</p>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading events...</p>
|
||||
) : events.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{events.map((event) => (
|
||||
<Link to={`/events/${event.id}`} key={event.id}>
|
||||
<div
|
||||
className="p-4 border border-[var(--neutral-800)] rounded-xl hover:border-brand-purple hover:shadow-md transition-all cursor-pointer"
|
||||
className="p-4 border border-chart-6 rounded-xl hover:border-muted-foreground hover:shadow-md transition-all cursor-pointer"
|
||||
data-testid={`event-card-${event.id}`}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="bg-[var(--neutral-800)]/20 p-3 rounded-lg">
|
||||
<Calendar className="h-6 w-6 text-brand-purple " />
|
||||
<div className="bg-chart-6/20 p-3 rounded-lg">
|
||||
<Calendar className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-[var(--purple-ink)] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>{event.title}</h4>
|
||||
<p className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<h4 className="font-semibold text-primary mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>{event.title}</h4>
|
||||
<p className="text-sm text-muted-foreground mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{new Date(event.start_at).toLocaleDateString()} at{' '}
|
||||
{new Date(event.start_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</p>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{event.location}</p>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{event.location}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -265,9 +265,9 @@ const Dashboard = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<Calendar className="h-16 w-16 text-[var(--neutral-800)] mx-auto mb-4" />
|
||||
<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-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Check back later for new events!</p>
|
||||
<Calendar className="h-16 w-16 text-chart-6 mx-auto mb-4" />
|
||||
<p className="text-muted-foreground mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>No upcoming events at the moment.</p>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Check back later for new events!</p>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
@@ -275,12 +275,12 @@ const Dashboard = () => {
|
||||
|
||||
{/* CTA Section */}
|
||||
{user?.status === 'pending_validation' && (
|
||||
<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)]">
|
||||
<Card className="mt-8 p-8 bg-gradient-to-br from-chart-6/20 to-muted/20 rounded-2xl border border-chart-6">
|
||||
<div className="text-center">
|
||||
<h3 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-2xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Application Under Review
|
||||
</h3>
|
||||
<p className="text-brand-purple mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Your membership application is being reviewed by our admin team. You'll be notified once validated!
|
||||
</p>
|
||||
</div>
|
||||
@@ -289,20 +289,20 @@ const Dashboard = () => {
|
||||
|
||||
{/* Payment Prompt for payment_pending status */}
|
||||
{user?.status === 'payment_pending' && (
|
||||
<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 ">
|
||||
<Card className="mt-8 p-8 bg-gradient-to-br from-chart-6/20 to-muted/20 rounded-2xl border-2 border-muted-foreground">
|
||||
<div className="text-center">
|
||||
<div className="mb-4">
|
||||
<AlertCircle className="h-16 w-16 text-brand-purple mx-auto" />
|
||||
<AlertCircle className="h-16 w-16 text-muted-foreground mx-auto" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-2xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Complete Your Payment
|
||||
</h3>
|
||||
<p className="text-brand-purple mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground 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.
|
||||
</p>
|
||||
<Link to="/plans">
|
||||
<Button
|
||||
className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background rounded-full px-8 py-6 text-lg font-semibold"
|
||||
className="bg-chart-6 text-primary hover:bg-background rounded-full px-8 py-6 text-lg font-semibold"
|
||||
data-testid="complete-payment-cta"
|
||||
>
|
||||
<CheckCircle className="mr-2 h-5 w-5" />
|
||||
@@ -316,38 +316,38 @@ const Dashboard = () => {
|
||||
{/* Event Activity Section */}
|
||||
<div className="mt-12">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h2 className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
My Event Activity
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{activityLoading ? (
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading event activity...</p>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading event activity...</p>
|
||||
) : eventActivity ? (
|
||||
<div className="space-y-8">
|
||||
{/* Stats Cards */}
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="bg-[var(--neutral-800)]/20 p-4 rounded-lg">
|
||||
<Calendar className="h-8 w-8 text-brand-purple " />
|
||||
<div className="bg-chart-6/20 p-4 rounded-lg">
|
||||
<Calendar className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total RSVPs</p>
|
||||
<p className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total RSVPs</p>
|
||||
<p className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{eventActivity.total_rsvps}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="bg-[var(--green-light)]/20 p-4 rounded-lg">
|
||||
<CheckCircle className="h-8 w-8 text-[var(--green-light)]" />
|
||||
<div className="bg-success/20 p-4 rounded-lg">
|
||||
<CheckCircle className="h-8 w-8 text-success" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Events Attended</p>
|
||||
<p className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Events Attended</p>
|
||||
<p className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{eventActivity.total_attended}
|
||||
</p>
|
||||
</div>
|
||||
@@ -357,25 +357,25 @@ const Dashboard = () => {
|
||||
|
||||
{/* Upcoming RSVP'd Events */}
|
||||
{eventActivity.upcoming_events && eventActivity.upcoming_events.length > 0 && (
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<h3 className="text-xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6">
|
||||
<h3 className="text-xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Upcoming Events (RSVP'd)
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{eventActivity.upcoming_events.map((event) => (
|
||||
<Link to={`/events/${event.id}`} key={event.id}>
|
||||
<div className="p-4 border border-[var(--neutral-800)] rounded-xl hover:border-brand-purple hover:shadow-md transition-all">
|
||||
<div className="p-4 border border-chart-6 rounded-xl hover:border-muted-foreground hover:shadow-md transition-all">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-[var(--purple-ink)] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>{event.title}</h4>
|
||||
<p className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<h4 className="font-semibold text-primary mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>{event.title}</h4>
|
||||
<p className="text-sm text-muted-foreground mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{new Date(event.start_at).toLocaleDateString()} at{' '}
|
||||
{new Date(event.start_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</p>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{event.location}</p>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{event.location}</p>
|
||||
</div>
|
||||
<Badge className={
|
||||
event.rsvp_status === 'yes' ? 'bg-[var(--green-light)] text-white' :
|
||||
event.rsvp_status === 'yes' ? 'bg-success text-white' :
|
||||
event.rsvp_status === 'maybe' ? 'bg-orange-100 text-orange-700' :
|
||||
'bg-gray-200 text-gray-700'
|
||||
}>
|
||||
@@ -392,27 +392,27 @@ const Dashboard = () => {
|
||||
|
||||
{/* Past Events & Attendance */}
|
||||
{eventActivity.past_events && eventActivity.past_events.length > 0 && (
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<h3 className="text-xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6">
|
||||
<h3 className="text-xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Past Events
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{eventActivity.past_events.slice(0, 5).map((event) => (
|
||||
<div key={event.id} className="p-4 border border-[var(--neutral-800)] rounded-xl">
|
||||
<div key={event.id} className="p-4 border border-chart-6 rounded-xl">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-[var(--purple-ink)] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>{event.title}</h4>
|
||||
<p className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<h4 className="font-semibold text-primary mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>{event.title}</h4>
|
||||
<p className="text-sm text-muted-foreground mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{new Date(event.start_at).toLocaleDateString()} at{' '}
|
||||
{new Date(event.start_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<Badge className={event.attended ? 'bg-[var(--green-light)] text-white' : 'bg-gray-200 text-gray-700'}>
|
||||
<Badge className={event.attended ? 'bg-success text-white' : 'bg-gray-200 text-gray-700'}>
|
||||
{event.attended ? 'Attended' : 'Did not attend'}
|
||||
</Badge>
|
||||
{event.attended && event.attended_at && (
|
||||
<p className="text-xs text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-xs text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Checked in: {new Date(event.attended_at).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
@@ -422,7 +422,7 @@ const Dashboard = () => {
|
||||
))}
|
||||
</div>
|
||||
{eventActivity.past_events.length > 5 && (
|
||||
<p className="text-sm text-center text-brand-purple mt-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-center text-muted-foreground mt-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Showing 5 of {eventActivity.past_events.length} past events
|
||||
</p>
|
||||
)}
|
||||
@@ -432,17 +432,17 @@ const Dashboard = () => {
|
||||
{/* No Events Message */}
|
||||
{(!eventActivity.upcoming_events || eventActivity.upcoming_events.length === 0) &&
|
||||
(!eventActivity.past_events || eventActivity.past_events.length === 0) && (
|
||||
<Card className="p-12 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<Card className="p-12 bg-background rounded-2xl border border-chart-6">
|
||||
<div className="text-center">
|
||||
<Calendar className="h-16 w-16 text-[var(--neutral-800)] mx-auto mb-4" />
|
||||
<h3 className="text-xl font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Calendar className="h-16 w-16 text-chart-6 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-semibold text-primary mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
No Event Activity Yet
|
||||
</h3>
|
||||
<p className="text-brand-purple mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Browse upcoming events and RSVP to start building your event history!
|
||||
</p>
|
||||
<Link to="/events">
|
||||
<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">
|
||||
<Button className="bg-chart-6 text-primary hover:bg-background rounded-full px-6">
|
||||
<Calendar className="h-4 w-4 mr-2" />
|
||||
Browse Events
|
||||
</Button>
|
||||
@@ -452,10 +452,10 @@ const Dashboard = () => {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Card className="p-12 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<Card className="p-12 bg-background rounded-2xl border border-chart-6">
|
||||
<div className="text-center">
|
||||
<AlertCircle className="h-16 w-16 text-[var(--neutral-800)] mx-auto mb-4" />
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<AlertCircle className="h-16 w-16 text-chart-6 mx-auto mb-4" />
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Failed to load event activity. Please try refreshing the page.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -58,17 +58,17 @@ const Donate = () => {
|
||||
<div className="min-h-screen">
|
||||
<PublicNavbar />
|
||||
|
||||
<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">
|
||||
<main className="bg-gradient-to-bl from-[#F9FAFB] to-chart-6 px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-8 sm:py-10 md:py-12">
|
||||
{/* Hero Section */}
|
||||
<section className="py-12">
|
||||
<div className="max-w-4xl mx-auto text-center h-full">
|
||||
<div className="flex justify-center mb-4">
|
||||
<img src={loafHearts} alt="Hearts" className="w-32 h-auto" onError={(e) => e.target.style.display = 'none'} />
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-4xl md:text-5xl font-bold text-[var(--purple-deep)] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-3xl sm:text-4xl md:text-5xl font-bold text-foreground mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Donate
|
||||
</h1>
|
||||
<p className="text-xl text-[var(--purple-deep)] font-medium" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
<p className="text-xl text-foreground font-medium" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
We really appreciate your donations. You can make your donation online
|
||||
or send a check by mail.
|
||||
</p>
|
||||
@@ -84,8 +84,8 @@ const Donate = () => {
|
||||
<div className="mx-auto flex-1 w-full h-full">
|
||||
<Card className="p-8 bg-background rounded-3xl w-full h-full content-center">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<CreditCard className="size-24 text-[var(--purple-lavender)]" />
|
||||
<h2 className="text-3xl font-bold text-[var(--purple-deep)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<CreditCard className="size-24 text-muted-foreground" />
|
||||
<h2 className="text-3xl font-bold text-foreground" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Select Your Donation Amount
|
||||
</h2>
|
||||
</div>
|
||||
@@ -97,7 +97,7 @@ const Donate = () => {
|
||||
key={amount}
|
||||
onClick={() => handleDonateAmount(amount * 100)}
|
||||
disabled={processingAmount === amount * 100}
|
||||
className="bg-[var(--purple-lavender)] hover:bg-[var(--purple-deep)] text-white text-xl py-8 rounded-full disabled:opacity-50"
|
||||
className="bg-muted-foreground hover:bg-foreground text-white text-xl py-8 rounded-full disabled:opacity-50"
|
||||
>
|
||||
{processingAmount === amount * 100 ? (
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
@@ -112,13 +112,13 @@ const Donate = () => {
|
||||
<Button
|
||||
onClick={() => setCustomAmountDialogOpen(true)}
|
||||
disabled={processingAmount !== null}
|
||||
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"
|
||||
className="w-full bg-muted-foreground hover:bg-foreground text-white text-xl py-8 rounded-full flex items-center justify-center gap-2"
|
||||
>
|
||||
<Heart className="h-6 w-6" />
|
||||
Donate Any Amount
|
||||
</Button>
|
||||
|
||||
<p className="text-sm text-[var(--purple-lavender)] text-center mt-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground text-center mt-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Secure donation processing powered by Stripe
|
||||
</p>
|
||||
</Card>
|
||||
@@ -131,13 +131,13 @@ const Donate = () => {
|
||||
<div className="flex flex-col gap-8 w-full">
|
||||
{/* Mail Check */}
|
||||
<Card className="p-8 bg-background rounded-3xl flex gap-4 items-center flex-1">
|
||||
<Mail className="size-24 text-[var(--purple-lavender)]" />
|
||||
<Mail className="size-24 text-muted-foreground" />
|
||||
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold text-[var(--purple-deep)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-2xl font-bold text-foreground mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Mail a Check
|
||||
</h3>
|
||||
<p className="text-lg text-[var(--purple-deep)] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-foreground leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Our mailing address for checks:<br />
|
||||
<span className="font-semibold">LOAF</span><br />
|
||||
P.O. Box 7207<br />
|
||||
@@ -153,14 +153,14 @@ const Donate = () => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold text-[var(--purple-deep)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-2xl font-bold text-foreground mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Pay with Zelle
|
||||
</h3>
|
||||
<p className="text-lg text-[var(--purple-deep)] leading-relaxed mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-foreground 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:
|
||||
</p>
|
||||
<a href="mailto:LOAFHoustonTX@gmail.com"
|
||||
className="text-[var(--purple-lavender)] text-lg font-bold underline hover:text-[var(--purple-deep)] transition-colors"
|
||||
className="text-muted-foreground text-lg font-bold underline hover:text-foreground transition-colors"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
LOAFHoustonTX@gmail.com
|
||||
</a>
|
||||
@@ -181,21 +181,21 @@ const Donate = () => {
|
||||
<Dialog open={customAmountDialogOpen} onOpenChange={setCustomAmountDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[450px] bg-background rounded-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<DialogTitle className="text-2xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Enter Donation Amount
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-[var(--purple-lavender)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<DialogDescription className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Choose how much you'd like to donate to support our community
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div>
|
||||
<Label htmlFor="customAmount" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Label htmlFor="customAmount" className="text-primary font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Amount (USD)
|
||||
</Label>
|
||||
<div className="relative mt-2">
|
||||
<span className="absolute left-4 top-1/2 transform -translate-y-1/2 text-[var(--purple-lavender)] text-xl font-semibold">
|
||||
<span className="absolute left-4 top-1/2 transform -translate-y-1/2 text-muted-foreground text-xl font-semibold">
|
||||
$
|
||||
</span>
|
||||
<Input
|
||||
@@ -206,7 +206,7 @@ const Donate = () => {
|
||||
value={customAmount}
|
||||
onChange={(e) => setCustomAmount(e.target.value)}
|
||||
placeholder="50.00"
|
||||
className="pl-10 h-14 text-xl border-2 border-[var(--neutral-800)] focus:border-[var(--purple-lavender)] rounded-xl"
|
||||
className="pl-10 h-14 text-xl border-2 border-chart-6 focus:border-muted-foreground rounded-xl"
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleCustomDonate();
|
||||
@@ -214,13 +214,13 @@ const Donate = () => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-[var(--purple-lavender)] mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Minimum donation: $1.00
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-[var(--lavender-300)] rounded-lg p-4">
|
||||
<p className="text-sm text-[var(--purple-ink)] text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="bg-muted rounded-lg p-4">
|
||||
<p className="text-sm text-primary text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<strong>Thank you for supporting LOAF!</strong><br />
|
||||
Your donation helps us continue our mission and provide meaningful experiences for our community.
|
||||
</p>
|
||||
@@ -232,14 +232,14 @@ const Donate = () => {
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setCustomAmountDialogOpen(false)}
|
||||
className="rounded-full border-2 border-[var(--neutral-800)]"
|
||||
className="rounded-full border-2 border-chart-6"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleCustomDonate}
|
||||
className="bg-[var(--purple-lavender)] text-white hover:bg-[var(--purple-deep)] rounded-full"
|
||||
className="bg-muted-foreground text-white hover:bg-foreground rounded-full"
|
||||
>
|
||||
Continue to Payment
|
||||
</Button>
|
||||
|
||||
@@ -14,9 +14,9 @@ const DonationSuccess = () => {
|
||||
<div className="min-h-screen bg-background">
|
||||
<PublicNavbar />
|
||||
|
||||
<main className="bg-gradient-to-b from-white to-[var(--lavender-300)] px-6 py-20">
|
||||
<main className="bg-gradient-to-b from-white to-muted px-6 py-20">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<Card className="p-6 sm:p-8 md:p-12 bg-background rounded-2xl border-2 border-[var(--neutral-800)] shadow-xl text-center">
|
||||
<Card className="p-6 sm:p-8 md:p-12 bg-background rounded-2xl border-2 border-chart-6 shadow-xl text-center">
|
||||
{/* Success Icon */}
|
||||
<div className="flex justify-center mb-4">
|
||||
<img
|
||||
@@ -26,34 +26,34 @@ const DonationSuccess = () => {
|
||||
onError={(e) => e.target.style.display = 'none'}
|
||||
/>
|
||||
</div>
|
||||
<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-[var(--green-light)]" />
|
||||
<div className="inline-flex items-center justify-center w-20 h-20 bg-success/10 rounded-full mb-6">
|
||||
<CheckCircle className="h-12 w-12 text-success" />
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h1 className="text-2xl sm:text-3xl md:text-4xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-2xl sm:text-3xl md:text-4xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Thank You for Your Donation!
|
||||
</h1>
|
||||
|
||||
{/* Message */}
|
||||
<div className="space-y-4 mb-8">
|
||||
<p className="text-xl text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-xl text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Your generous contribution helps support our community and continue our mission.
|
||||
</p>
|
||||
|
||||
<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-[var(--orange-light)] mb-2">
|
||||
<div className="bg-gradient-to-r from-muted to-chart-6/30 rounded-2xl p-6 border-2 border-chart-6">
|
||||
<div className="flex items-center justify-center gap-2 text-accent mb-2">
|
||||
<Heart className="h-6 w-6" />
|
||||
<span className="text-lg font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Your Support Makes a Difference
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
A receipt for your donation has been sent to your email address.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="text-base text-brand-purple pt-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-base text-muted-foreground 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.
|
||||
</p>
|
||||
</div>
|
||||
@@ -62,7 +62,7 @@ const DonationSuccess = () => {
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button
|
||||
onClick={() => navigate('/')}
|
||||
className="bg-brand-purple text-white hover:bg-[var(--purple-ink)] rounded-full px-8 py-6 text-lg font-medium shadow-lg"
|
||||
className="bg-muted-foreground text-white hover:bg-primary rounded-full px-8 py-6 text-lg font-medium shadow-lg"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
Return to Home
|
||||
@@ -70,7 +70,7 @@ const DonationSuccess = () => {
|
||||
<Button
|
||||
onClick={() => navigate('/donate')}
|
||||
variant="outline"
|
||||
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"
|
||||
className="border-2 border-muted-foreground text-muted-foreground hover:bg-chart-6/20 rounded-full px-8 py-6 text-lg font-medium"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
Make Another Donation
|
||||
@@ -80,12 +80,12 @@ const DonationSuccess = () => {
|
||||
|
||||
{/* Additional Info */}
|
||||
<div className="mt-12 text-center">
|
||||
<p className="text-sm text-brand-purple mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Have questions about your donation?
|
||||
</p>
|
||||
<a
|
||||
href="mailto:support@loaf.org"
|
||||
className="text-[var(--orange-light)] hover:text-brand-purple font-medium transition-colors"
|
||||
className="text-accent hover:text-muted-foreground font-medium transition-colors"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
Contact us at support@loaf.org
|
||||
|
||||
@@ -51,7 +51,7 @@ const EventDetails = () => {
|
||||
<div className="min-h-screen bg-background">
|
||||
<Navbar />
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading event...</p>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading event...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -68,23 +68,23 @@ const EventDetails = () => {
|
||||
<div className="max-w-4xl mx-auto px-6 py-12">
|
||||
<button
|
||||
onClick={() => navigate('/events')}
|
||||
className="inline-flex items-center text-brand-purple hover:text-[var(--orange-light)] transition-colors mb-8"
|
||||
className="inline-flex items-center text-muted-foreground hover:text-accent transition-colors mb-8"
|
||||
data-testid="back-to-events-button"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back to Events
|
||||
</button>
|
||||
|
||||
<Card className="p-8 md:p-12 bg-background rounded-2xl border border-[var(--neutral-800)] shadow-lg">
|
||||
<Card className="p-8 md:p-12 bg-background rounded-2xl border border-chart-6 shadow-lg">
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="bg-[var(--neutral-800)]/20 p-4 rounded-xl">
|
||||
<Calendar className="h-10 w-10 text-brand-purple " />
|
||||
<div className="bg-chart-6/20 p-4 rounded-xl">
|
||||
<Calendar className="h-10 w-10 text-muted-foreground" />
|
||||
</div>
|
||||
{event.user_rsvp_status && (
|
||||
<Badge
|
||||
className={`px-4 py-2 rounded-full text-sm ${event.user_rsvp_status === 'yes'
|
||||
? 'bg-[var(--green-light)] text-white'
|
||||
? 'bg-success text-white'
|
||||
: event.user_rsvp_status === 'no'
|
||||
? 'bg-gray-400 text-white'
|
||||
: 'bg-orange-100 text-orange-700'
|
||||
@@ -97,12 +97,12 @@ const EventDetails = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-primary mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{event.title}
|
||||
</h1>
|
||||
|
||||
<div className="space-y-4 text-lg">
|
||||
<div className="flex items-center gap-3 text-brand-purple ">
|
||||
<div className="flex items-center gap-3 text-muted-foreground">
|
||||
<Calendar className="h-5 w-5" />
|
||||
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{new Date(event.start_at).toLocaleDateString('en-US', {
|
||||
@@ -113,18 +113,18 @@ const EventDetails = () => {
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-brand-purple ">
|
||||
<div className="flex items-center gap-3 text-muted-foreground">
|
||||
<Calendar className="h-5 w-5" />
|
||||
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{new Date(event.start_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} -{' '}
|
||||
{new Date(event.end_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-brand-purple ">
|
||||
<div className="flex items-center gap-3 text-muted-foreground">
|
||||
<MapPin className="h-5 w-5" />
|
||||
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{event.location}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-brand-purple ">
|
||||
<div className="flex items-center gap-3 text-muted-foreground">
|
||||
<Users className="h-5 w-5" />
|
||||
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{event.rsvp_count || 0} {event.rsvp_count === 1 ? 'person' : 'people'} attending
|
||||
@@ -135,18 +135,18 @@ const EventDetails = () => {
|
||||
</div>
|
||||
|
||||
{event.description && (
|
||||
<div className="mb-8 pb-8 border-b border-[var(--neutral-800)]">
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<div className="mb-8 pb-8 border-b border-chart-6">
|
||||
<h2 className="text-2xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
About This Event
|
||||
</h2>
|
||||
<p className="text-brand-purple leading-relaxed whitespace-pre-line" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground leading-relaxed whitespace-pre-line" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{event.description}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h2 className="text-2xl font-semibold text-primary mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
RSVP to This Event
|
||||
</h2>
|
||||
<div className="flex gap-4 flex-wrap">
|
||||
@@ -154,8 +154,8 @@ const EventDetails = () => {
|
||||
onClick={() => handleRSVP('yes')}
|
||||
disabled={rsvpLoading}
|
||||
className={`rounded-full px-8 py-6 flex items-center gap-2 ${event.user_rsvp_status === 'yes'
|
||||
? 'bg-[var(--green-light)] text-white'
|
||||
: 'bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-brand-lavender'
|
||||
? 'bg-success text-white'
|
||||
: 'bg-chart-6 text-primary hover:bg-background'
|
||||
}`}
|
||||
data-testid="rsvp-yes-button"
|
||||
>
|
||||
@@ -168,7 +168,7 @@ const EventDetails = () => {
|
||||
variant="outline"
|
||||
className={`rounded-full px-8 py-6 flex items-center gap-2 border-2 ${event.user_rsvp_status === 'maybe'
|
||||
? 'border-orange-400 bg-orange-100 text-orange-700'
|
||||
: 'border-brand-purple text-brand-purple hover:bg-[var(--lavender-300)]'
|
||||
: 'border-muted-foreground text-muted-foreground hover:bg-muted'
|
||||
}`}
|
||||
data-testid="rsvp-maybe-button"
|
||||
>
|
||||
@@ -191,11 +191,11 @@ const EventDetails = () => {
|
||||
</div>
|
||||
|
||||
{/* Add to Calendar Section */}
|
||||
<div className="mt-8 pt-8 border-t border-[var(--neutral-800)]">
|
||||
<h2 className="text-xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<div className="mt-8 pt-8 border-t border-chart-6">
|
||||
<h2 className="text-xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Add to Your Calendar
|
||||
</h2>
|
||||
<p className="text-brand-purple mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Never miss this event! Add it to your calendar app for reminders.
|
||||
</p>
|
||||
<AddToCalendarButton
|
||||
|
||||
@@ -30,7 +30,7 @@ const Events = () => {
|
||||
if (!rsvpStatus) return null;
|
||||
|
||||
const config = {
|
||||
yes: { label: 'Going', className: 'bg-[var(--green-light)] text-white' },
|
||||
yes: { label: 'Going', className: 'bg-success text-white' },
|
||||
no: { label: 'Not Going', className: 'bg-gray-400 text-white' },
|
||||
maybe: { label: 'Maybe', className: 'bg-orange-100 text-orange-700' }
|
||||
};
|
||||
@@ -51,62 +51,62 @@ const Events = () => {
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6 py-12">
|
||||
<div className="mb-12">
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Upcoming Events
|
||||
</h1>
|
||||
<p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Browse and RSVP to our community events.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-20">
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading events...</p>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading events...</p>
|
||||
</div>
|
||||
) : events.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8">
|
||||
{events.map((event) => (
|
||||
<Link to={`/events/${event.id}`} key={event.id}>
|
||||
<Card
|
||||
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"
|
||||
className="p-6 bg-background rounded-2xl border border-chart-6 hover:shadow-lg hover:-translate-y-1 transition-all cursor-pointer h-full"
|
||||
data-testid={`event-card-${event.id}`}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="bg-[var(--neutral-800)]/20 p-3 rounded-lg">
|
||||
<Calendar className="h-6 w-6 text-brand-purple " />
|
||||
<div className="bg-chart-6/20 p-3 rounded-lg">
|
||||
<Calendar className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
{getRSVPBadge(event.user_rsvp_status)}
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-xl font-semibold text-primary mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{event.title}
|
||||
</h3>
|
||||
|
||||
{event.description && (
|
||||
<p className="text-brand-purple mb-4 line-clamp-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mb-4 line-clamp-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{event.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2 text-brand-purple ">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{new Date(event.start_at).toLocaleDateString()} at{' '}
|
||||
{new Date(event.start_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-brand-purple ">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<MapPin className="h-4 w-4" />
|
||||
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{event.location}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-brand-purple ">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Users className="h-4 w-4" />
|
||||
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{event.rsvp_count || 0} attending</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex items-center text-[var(--orange-light)] font-medium">
|
||||
<div className="mt-6 flex items-center text-accent font-medium">
|
||||
View Details
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</div>
|
||||
@@ -116,11 +116,11 @@ const Events = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-20">
|
||||
<Calendar className="h-20 w-20 text-[var(--neutral-800)] mx-auto mb-6" />
|
||||
<h3 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Calendar className="h-20 w-20 text-chart-6 mx-auto mb-6" />
|
||||
<h3 className="text-2xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
No Events Available
|
||||
</h3>
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
There are no upcoming events at the moment. Check back later!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -37,23 +37,23 @@ const ForgotPassword = () => {
|
||||
|
||||
<div className="max-w-md mx-auto px-6 py-12">
|
||||
<div className="mb-8">
|
||||
<Link to="/login" className="inline-flex items-center text-brand-purple hover:text-[var(--orange-light)] transition-colors">
|
||||
<Link to="/login" className="inline-flex items-center text-muted-foreground hover:text-accent transition-colors">
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back to Login
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Card className="p-8 md:p-12 bg-background rounded-2xl border border-[var(--neutral-800)] shadow-lg">
|
||||
<Card className="p-8 md:p-12 bg-background rounded-2xl border border-chart-6 shadow-lg">
|
||||
{!submitted ? (
|
||||
<>
|
||||
<div className="mb-8 text-center">
|
||||
<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-brand-purple " />
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-muted mb-4">
|
||||
<Mail className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Forgot Password?
|
||||
</h1>
|
||||
<p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
No worries! Enter your email and we'll send you reset instructions.
|
||||
</p>
|
||||
</div>
|
||||
@@ -69,22 +69,22 @@ const ForgotPassword = () => {
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="your.email@example.com"
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
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"
|
||||
className="w-full bg-chart-6 text-primary 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'}
|
||||
<ArrowRight className="ml-2 h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
<p className="text-center text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-center text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Remember your password?{' '}
|
||||
<Link to="/login" className="text-[var(--orange-light)] hover:underline font-medium">
|
||||
<Link to="/login" className="text-accent hover:underline font-medium">
|
||||
Login here
|
||||
</Link>
|
||||
</p>
|
||||
@@ -92,21 +92,21 @@ const ForgotPassword = () => {
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center">
|
||||
<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-[var(--green-success)]" />
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-[#E8F5E9] mb-6">
|
||||
<CheckCircle className="h-8 w-8 text-[#4CAF50]" />
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Check Your Email
|
||||
</h1>
|
||||
<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-[var(--purple-ink)]">{email}</span>,
|
||||
<p className="text-lg text-muted-foreground mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
If an account exists for <span className="font-medium text-primary">{email}</span>,
|
||||
you will receive a password reset link shortly.
|
||||
</p>
|
||||
<p className="text-sm text-brand-purple mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground 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.
|
||||
</p>
|
||||
<Link to="/login">
|
||||
<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">
|
||||
<Button className="bg-chart-6 text-primary hover:bg-background rounded-full px-8 py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform">
|
||||
Return to Login
|
||||
<ArrowRight className="ml-2 h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
@@ -23,13 +23,13 @@ const CardSection = ({ children, className = '', arrow = true }) => (
|
||||
);
|
||||
|
||||
const Title = ({ children }) => (
|
||||
<h2 className="text-3xl font-bold text-[var(--purple-deep)] mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
<h2 className="text-3xl font-bold text-foreground 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" }}>
|
||||
<p className="text-md text-foreground leading-relaxed mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
@@ -46,15 +46,15 @@ const History = () => {
|
||||
<div className="min-h-screen bg-background">
|
||||
<PublicNavbar />
|
||||
|
||||
<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">
|
||||
<main className="bg-gradient-to-br from-[#F9FAFB] to-[#DCD7EA] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-8 sm:py-10 md:py-12">
|
||||
{/* Hero Section */}
|
||||
<section className="py-12">
|
||||
<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-extrabold text-[var(--purple-deep)] "
|
||||
<h1 className="text-3xl sm:text-4xl md:text-5xl font-extrabold text-foreground "
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
History of LOAF
|
||||
</h1>
|
||||
<div className="flex items-center justify-center gap-6 text-[var(--purple-deep)]">
|
||||
<div className="flex items-center justify-center gap-6 text-foreground">
|
||||
<Pen className="size-7" />
|
||||
<p className="text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
By Arden Eversmeyer
|
||||
@@ -69,19 +69,19 @@ const History = () => {
|
||||
<div className="md:w-1/3 ">
|
||||
<img src={ardenCharlotteImg} alt="Arden Eversmeyer and Charlotte Avery"
|
||||
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
||||
<p className="text-sm text-[var(--purple-deep)] mt-2 text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-foreground mt-2 text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Arden Eversmeyer and Charlotte Avery
|
||||
</p>
|
||||
</div>
|
||||
<div className="md:w-2/3">
|
||||
<Title>Part 1</Title>
|
||||
<p className="text-md text-[var(--purple-deep)] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-md text-foreground 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”, 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>
|
||||
<p className="text-md mb-4 text-[var(--purple-deep)] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-md mb-4 text-foreground leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
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 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>
|
||||
<p className="text-md text-foreground 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>
|
||||
</CardSection>
|
||||
@@ -90,13 +90,13 @@ const History = () => {
|
||||
{/* Part 2 */}
|
||||
<CardSection >
|
||||
<Title>Part 2</Title>
|
||||
<p className="text-md text-[var(--purple-deep)] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-md text-foreground 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."
|
||||
</p>
|
||||
<p className="text-md text-[var(--purple-deep)] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-md text-foreground 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.
|
||||
</p>
|
||||
<ul className="list-disc ml-6 mt-4 space-y-2 text-md text-[var(--purple-deep)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<ul className="list-disc ml-6 mt-4 space-y-2 text-md text-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<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>NAME FOR THE GROUP - The acronym began as LOAFF, then we streamlined it to the now-familiar LOAF.</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>
|
||||
@@ -119,13 +119,13 @@ const History = () => {
|
||||
</div>
|
||||
<div className="">
|
||||
<Title>Part 3</Title>
|
||||
<p className="text-md text-[var(--purple-deep)] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-md text-foreground leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
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.
|
||||
</p>
|
||||
<p className="text-md text-[var(--purple-deep)] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-md text-foreground leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
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" }}>
|
||||
<p className="text-md text-foreground leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
In 1989 member Jo Stewart, social worker at Methodist Hospital, started urging LOAF to incorporate as a non-profit. The work began in 1990 with Moore & Hunt (Debbie Hunt) as our Corporate Attorneys. Jo died of cancer in 1990. The work for application of our 501(c)3 was done by Floi Ewing, Arden's sister, and our non-profit status was granted in January 1991. Loaf incorporated as a social networking and support group without a membership roll to protect the anonymity of the women in LOAF.
|
||||
</p>
|
||||
</div>
|
||||
@@ -137,7 +137,7 @@ const History = () => {
|
||||
<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" }}>
|
||||
<p className="text-md text-foreground leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Third Sunday meeting places have changed over the years. We moved from Womynspace to Autrey House near Rice University. We were there from November 1987 until May 1990 when the new Bishop dis-invited all GLBT groups because of homophobia. We spent a couple months at Montrose Counseling Center (on Lovett), and then moved to the Metropolitan Multi- Service Center on W. Gray. We met there from August 1990 until January 1993. We left because the city started closing the centers on Sunday, and we were not willing to change our meeting day. From February through June we met at Inklings Book Store . In July we started our long occupancy with Houston Mission Church, and met there until the church dissolved in April 2001. We then met at the Hollyfield Center for seven months. From there we went to the GLBT Community Center on Hawthorne where we stayed until July 2003. Attendance was dropping off, and some of the women were not comfortable in a gay identified place. So Third Sunday Meetings moved to Charlotte and Arden's home - and we met there from August 2003 until April 2011. Membership had grown until the meetings had reached critical mass and parking was a problem. So a team of board members started researching for a new home. And on the third Sunday of May 2011 LOAF started meeting at the Montrose Counseling Center. A new era had started.
|
||||
</p>
|
||||
</div>
|
||||
@@ -155,16 +155,16 @@ const History = () => {
|
||||
|
||||
<div className="w-full lg:w-1/2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Title>Part 5</Title>
|
||||
<p className="text-md text-[var(--purple-deep)] leading-relaxed mb-4" >
|
||||
<p className="text-md text-foreground leading-relaxed mb-4" >
|
||||
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.
|
||||
</p>
|
||||
<p className="text-md text-[var(--purple-deep)] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-md text-foreground leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
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" }}>
|
||||
<p className="text-md text-foreground 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" }}>
|
||||
<p className="text-md text-foreground leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
All of these decisions were made by the women at the Third Sunday meetings. There have never been rules instigated by the Board of Directors. Because many women don't want to attend meetings, we changed Third Sunday Meeting to Meet 'N Greet several years ago. And that is what we do - take care of any necessary business. But greet newcomers and socialize with our friends.
|
||||
</p>
|
||||
</div>
|
||||
@@ -176,23 +176,23 @@ const History = () => {
|
||||
{/* Part 6 */}
|
||||
<CardSection >
|
||||
|
||||
<h2 className="text-3xl font-bold text-[var(--purple-deep)] mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
<h2 className="text-3xl font-bold text-foreground mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
Part 6
|
||||
</h2>
|
||||
|
||||
<p className="text-md text-[var(--purple-deep)] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-md text-foreground 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 className="text-md text-[var(--purple-deep)] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-md text-foreground leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
The ice cream socials started in 1989. There are still women who have never cranked or eaten home made ice cream.The “picnic in the park” started in 2000. We have held picnics in a couple State Parks as well as Tom Bass Park in recent years.In 1988 we started attending the TUTS Broadway Musical at Miller Theater in July. We bring a snack supper and a chair and sit on the hill.In 2000 we started eating at Sudie's Catfish House in January. A breather from a busy party season, but a good way to connect.From 1987 to 1994 we had “Second Tuesday Dancing”. First at The Ranch, and then at Ms B's, it was our way to celebrate birthdays of the month. It was well attended.
|
||||
</p>
|
||||
|
||||
<p className="text-md text-[var(--purple-deep)] leading-relaxed mt-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-md text-foreground leading-relaxed mt-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
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" }}>
|
||||
<p className="text-md text-foreground 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>
|
||||
|
||||
@@ -208,13 +208,13 @@ const History = () => {
|
||||
|
||||
</div>
|
||||
<div className="md:w-2/3">
|
||||
<h2 className="text-3xl font-bold text-[var(--purple-deep)] mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
<h2 className="text-3xl font-bold text-foreground mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
Part 7
|
||||
</h2>
|
||||
<p className="text-md text-[var(--purple-deep)] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-md text-foreground leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
The LOAF Library has been an important part of the offering to the women. It started about 1987 when Arden discovered there were books - both fiction and non-fiction - about lesbians. We had one bookstore then -” Wilde 'N Stein” - that had a limited selection of lesbian books. Then Arden discovered Womencraft Books, a mail order book company. This began the collection now in the library. Over the years women have donated books. At one time we took duplicate titles to our book stores (Inkilngs and Book Woman) and traded them for titles we didn't have on the shelf. When the last book store closed we started donating duplicate copies to HATCH, and they are building their library. We have a collection that includes feminist, fantasy/sci-fi, poetry, non-fiction, as well as fiction books. We have a collection of out-of-print periodicals, women's music, and a video library. We have some beautiful “coffee table” books. We have copies of many of the “pulp” books.
|
||||
</p>
|
||||
<p className="text-md text-[var(--purple-deep)] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-md text-foreground leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
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>
|
||||
</div>
|
||||
@@ -225,16 +225,16 @@ const History = () => {
|
||||
{/* Part 8 */}
|
||||
<CardSection arrow={false} >
|
||||
|
||||
<h2 className="text-3xl font-bold text-[var(--purple-deep)] mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
<h2 className="text-3xl font-bold text-foreground mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
Part 8
|
||||
</h2>
|
||||
<p className="text-md text-[var(--purple-deep)] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-md text-foreground leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
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 className="text-md text-[var(--purple-deep)] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-md text-foreground leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
For 17 consecutive years, from 1987 to 2004, we had a Texas Lesbian Conference that rotated between Houston, San Antonio, Austin, and Dallas. LOAF presented workshops at five of these conferences. LOAF did a workshop at the National Lesbian Conference in Atlanta in 1991. We did a workshop at at the PFLAG “Healing the Hurt” conference in 1994. We did a program at the Silver Threads conference in St Petersburg, FL. We have done programs at three OLOC conferences. Charlotte and Arden participated in a live TV show about senior GLBT persons in Dallas. We participated in a documentary on GLBT seniors produced in Canada. And another documentary for Golden Threads at Cape Cod. We participated on a panel for the Women’s Studies Department at the University of Houston for their “Living archive” series. We have done several programs for the Women’s Group in Houston, and appeared on the After Hours radio show on KPFT several times.
|
||||
</p>
|
||||
<p className="text-md text-[var(--purple-deep)] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-md text-foreground leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
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>
|
||||
|
||||
@@ -243,32 +243,32 @@ const History = () => {
|
||||
</main>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-20 bg-[var(--purple-deep)] mx-0">
|
||||
<section className="py-20 bg-foreground mx-0">
|
||||
<div className="max-w-7xl mx-auto px-8">
|
||||
<div className="flex gap-8 md:flex-row flex-col">
|
||||
<Card className="p-8 text-center bg-background rounded-2xl shadow-lg hover:shadow-xl transition-shadow">
|
||||
<h3 className="text-2xl font-bold text-[var(--purple-deep)] mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
<h3 className="text-2xl font-bold text-foreground mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
A Life Remembered
|
||||
</h3>
|
||||
<p className="text-[var(--purple-deep)] mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-foreground 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.
|
||||
</p>
|
||||
<a href="https://www.oldlesbianhistory.org/arden-eversmeyer" target="_blank" rel="noopener noreferrer">
|
||||
<Button className="bg-[var(--purple-lavender)] hover:bg-[var(--purple-deep)] text-white rounded-full px-6 py-3">
|
||||
<Button className="bg-muted-foreground hover:bg-foreground text-white rounded-full px-6 py-3">
|
||||
View Arden's Tribute
|
||||
</Button>
|
||||
</a>
|
||||
</Card>
|
||||
|
||||
<Card className="p-8 text-center bg-background rounded-2xl shadow-lg hover:shadow-xl transition-shadow">
|
||||
<h3 className="text-2xl font-bold text-[var(--purple-deep)] mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
<h3 className="text-2xl font-bold text-foreground mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
The Old Lesbian Oral Herstory Project
|
||||
</h3>
|
||||
<p className="text-[var(--purple-deep)] mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-foreground 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.
|
||||
</p>
|
||||
<a href="https://www.olohp.org" target="_blank" rel="noopener noreferrer">
|
||||
<Button className="bg-[var(--purple-lavender)] hover:bg-[var(--purple-deep)] text-white rounded-full px-6 py-3">
|
||||
<Button className="bg-muted-foreground hover:bg-foreground text-white rounded-full px-6 py-3">
|
||||
Learn More About OLOHP
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
@@ -24,7 +24,7 @@ const Landing = () => {
|
||||
/>
|
||||
</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" }}>
|
||||
<h5 className="text-foreground text-[28px] leading-10 pb-10 font-semibold text-center" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
{infoTitle}
|
||||
</h5>
|
||||
{description}
|
||||
@@ -37,7 +37,7 @@ const Landing = () => {
|
||||
iconSrc: iconMeetGreet,
|
||||
infoTitle: 'Meet and Greet',
|
||||
description: (
|
||||
<p className="text-[var(--purple-deep)] text-lg text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-foreground 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{' '}
|
||||
@@ -49,7 +49,7 @@ const Landing = () => {
|
||||
iconSrc: iconSocials,
|
||||
infoTitle: 'Socials',
|
||||
description: (
|
||||
<p className="text-[var(--purple-deep)] text-lg text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-foreground 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
|
||||
@@ -61,7 +61,7 @@ const Landing = () => {
|
||||
iconSrc: iconActive,
|
||||
infoTitle: 'Active LOAFers',
|
||||
description: (
|
||||
<p className="text-[var(--purple-deep)] text-lg text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-foreground 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.
|
||||
@@ -75,7 +75,7 @@ const Landing = () => {
|
||||
<PublicNavbar />
|
||||
|
||||
{/* Hero Section */}
|
||||
<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">
|
||||
<section className="relative bg-gradient-to-b from-foreground to-muted-foreground 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">
|
||||
{/* 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" />
|
||||
@@ -89,7 +89,7 @@ const Landing = () => {
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 items-center justify-center w-full max-w-[339px]">
|
||||
<Link to="/become-a-member" className="w-full">
|
||||
<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">
|
||||
<Button style={{ fontFamily: "'Nunito sans', sans-serif" }} className="bg-chart-6 hover:bg-background text-primary rounded-full px-6 py-6 sm:py-[32px] text-base sm:text-lg font-medium w-full transition-colors">
|
||||
Become a Member
|
||||
</Button>
|
||||
</Link>
|
||||
@@ -105,9 +105,9 @@ const Landing = () => {
|
||||
</section>
|
||||
|
||||
{/* About Section */}
|
||||
<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">
|
||||
<section id="about" className="bg-gradient-to-b pb-10 lg:pb-44 from-white to-muted 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-4">
|
||||
<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" }}>
|
||||
<h3 className="text-foreground 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
|
||||
</h3>
|
||||
</div>
|
||||
@@ -118,17 +118,17 @@ const Landing = () => {
|
||||
</section>
|
||||
|
||||
{/* Feature Cards Section */}
|
||||
<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">
|
||||
<section className="bg-gradient-to-b pb-20 from-muted to-chart-6 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">
|
||||
{infoCardData.map((card) => (
|
||||
<InfoCard key={card.infoTitle} {...card} />
|
||||
))}
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<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">
|
||||
<section className="bg-gradient-to-b from-dusty-grape to-foreground 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-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 flex items-center justify-center">
|
||||
<Button className="bg-[var(--neutral-800)] hover:bg-background text-[var(--purple-ink)] rounded-full
|
||||
<Button className="bg-chart-6 hover:bg-background text-primary rounded-full
|
||||
py-8 text-xl font-normal px-12 sm:w-[392px] transition-colors ">
|
||||
Become a Member
|
||||
</Button>
|
||||
|
||||
@@ -60,18 +60,18 @@ const Login = () => {
|
||||
|
||||
<div className="max-w-md mx-auto px-6 py-12">
|
||||
<div className="mb-8">
|
||||
<Link to="/" className="inline-flex items-center text-brand-purple hover:text-[var(--orange-light)] transition-colors">
|
||||
<Link to="/" className="inline-flex items-center text-muted-foreground hover:text-accent transition-colors">
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back to Home
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Card className="p-8 md:p-12 bg-background rounded-2xl border border-[var(--neutral-800)] shadow-lg">
|
||||
<Card className="p-8 md:p-12 bg-background rounded-2xl border border-chart-6 shadow-lg">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Welcome Back
|
||||
</h1>
|
||||
<p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Login to access your member dashboard.
|
||||
</p>
|
||||
</div>
|
||||
@@ -87,7 +87,7 @@ const Login = () => {
|
||||
value={formData.email}
|
||||
onChange={handleInputChange}
|
||||
placeholder="your.email@example.com"
|
||||
className="h-14 rounded-xl border-2 focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
data-testid="login-email-input"
|
||||
/>
|
||||
</div>
|
||||
@@ -95,7 +95,7 @@ const Login = () => {
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Link to="/forgot-password" className="text-sm text-[var(--orange-light)] hover:underline">
|
||||
<Link to="/forgot-password" className="text-sm text-accent hover:underline">
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
@@ -106,7 +106,7 @@ const Login = () => {
|
||||
value={formData.password}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter your password"
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
data-testid="login-password-input"
|
||||
/>
|
||||
</div>
|
||||
@@ -114,16 +114,16 @@ const Login = () => {
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full py-6 text-lg font-medium shadow-lg hover:scale-105 disabled:opacity-50 btn-lavender"
|
||||
className="w-full bg-chart-6 text-primary hover:bg-background rounded-full py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform disabled:opacity-50"
|
||||
data-testid="login-submit-button"
|
||||
>
|
||||
{loading ? 'Logging in...' : 'Login'}
|
||||
<ArrowRight className="ml-2 h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
<p className="text-center text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-center text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Don't have an account?{' '}
|
||||
<Link to="/register" className="text-[var(--orange-light)] hover:underline font-medium">
|
||||
<Link to="/register" className="text-accent hover:underline font-medium">
|
||||
Register here
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
@@ -10,11 +10,11 @@ const MissionValues = () => {
|
||||
<div className="min-h-screen bg-background">
|
||||
<PublicNavbar />
|
||||
|
||||
<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">
|
||||
<main className="bg-gradient-to-b from-[#f9fafb] to-chart-6 px-4 sm:px-6 py-8 sm:py-12 md:py-20">
|
||||
<div className="max-w-[1400px] mx-auto">
|
||||
<div className="flex md:flex-row flex-col gap-10 items-stretch">
|
||||
{/* Left Card - Mission (Purple Gradient) */}
|
||||
<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 ">
|
||||
<Card className=" bg-gradient-to-br from-muted-foreground to-foreground 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"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
LOAF Mission
|
||||
@@ -30,11 +30,11 @@ const MissionValues = () => {
|
||||
|
||||
{/* Right Card - Values */}
|
||||
<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-[var(--purple-deep)] text-center mb-6"
|
||||
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold text-foreground text-center mb-6"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
LOAF Values
|
||||
</h2>
|
||||
<ol className="list-decimal list-inside space-y-8 text-lg text-[var(--purple-deep)]"
|
||||
<ol className="list-decimal list-inside space-y-8 text-lg text-foreground"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<li>Safe environments for lesbians to gather for a variety of social activities and interaction.</li>
|
||||
<li>Social support for lesbians.</li>
|
||||
|
||||
@@ -8,32 +8,32 @@ 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">
|
||||
<div className="min-h-screen bg-gradient-to-br from-bright-snow to-white flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-2xl p-12 bg-background rounded-2xl border border-chart-6 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"
|
||||
className="text-[180px] font-bold text-transparent bg-clip-text bg-gradient-to-br from-chart-6 to-bright-snow 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" />
|
||||
<Search className="h-24 w-24 text-muted-foreground opacity-30" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
<h2
|
||||
className="text-3xl font-semibold text-[var(--purple-ink)] mb-4"
|
||||
className="text-3xl font-semibold text-primary 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"
|
||||
className="text-lg text-muted-foreground 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.
|
||||
@@ -44,14 +44,14 @@ const NotFound = () => {
|
||||
<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"
|
||||
className="rounded-xl border-2 border-muted-foreground text-muted-foreground hover:bg-bright-snow 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"
|
||||
className="rounded-xl bg-gradient-to-r from-muted-foreground to-primary hover:from-primary hover:to-muted-foreground text-white px-6 py-6"
|
||||
>
|
||||
<Home className="h-5 w-5 mr-2" />
|
||||
Back to Home
|
||||
@@ -59,15 +59,15 @@ const NotFound = () => {
|
||||
</div>
|
||||
|
||||
{/* Help Text */}
|
||||
<div className="mt-8 pt-8 border-t border-[var(--neutral-800)]">
|
||||
<div className="mt-8 pt-8 border-t border-chart-6">
|
||||
<p
|
||||
className="text-sm text-brand-purple "
|
||||
className="text-sm text-muted-foreground"
|
||||
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"
|
||||
className="text-muted-foreground hover:text-primary font-semibold underline"
|
||||
>
|
||||
support@loaftx.org
|
||||
</a>
|
||||
|
||||
@@ -22,48 +22,48 @@ const PaymentCancel = () => {
|
||||
</div>
|
||||
|
||||
{/* Cancel Message */}
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Payment Cancelled
|
||||
</h1>
|
||||
<p className="text-lg text-brand-purple max-w-2xl mx-auto mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground 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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Info Card */}
|
||||
<Card className="p-8 bg-background rounded-2xl border border-[var(--neutral-800)] shadow-lg mb-8">
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6 text-center" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Card className="p-8 bg-background rounded-2xl border border-chart-6 shadow-lg mb-8">
|
||||
<h2 className="text-2xl font-semibold text-primary mb-6 text-center" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
What Happened?
|
||||
</h2>
|
||||
|
||||
<div className="space-y-6 mb-8">
|
||||
<p className="text-brand-purple text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground 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.
|
||||
</p>
|
||||
|
||||
<div className="bg-[var(--neutral-800)]/20 p-6 rounded-xl">
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<div className="bg-chart-6/20 p-6 rounded-xl">
|
||||
<h3 className="text-lg font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Ready to Complete Your Membership?
|
||||
</h3>
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-start gap-3">
|
||||
<CreditCard className="h-5 w-5 text-brand-purple flex-shrink-0 mt-0.5" />
|
||||
<span className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<CreditCard className="h-5 w-5 text-muted-foreground flex-shrink-0 mt-0.5" />
|
||||
<span className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Return to the plans page to complete your subscription
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<Mail className="h-5 w-5 text-brand-purple flex-shrink-0 mt-0.5" />
|
||||
<span className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Mail className="h-5 w-5 text-muted-foreground flex-shrink-0 mt-0.5" />
|
||||
<span className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Contact us if you experienced any issues during checkout
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-[var(--lavender-300)] p-6 rounded-xl">
|
||||
<p className="text-sm text-brand-purple text-center mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<span className="font-medium text-[var(--purple-ink)]">Note:</span>{' '}
|
||||
<div className="bg-muted p-6 rounded-xl">
|
||||
<p className="text-sm text-muted-foreground text-center mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<span className="font-medium text-primary">Note:</span>{' '}
|
||||
Your membership application is still validated. You can complete payment whenever you're ready.
|
||||
</p>
|
||||
</div>
|
||||
@@ -73,7 +73,7 @@ const PaymentCancel = () => {
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button
|
||||
onClick={() => navigate('/plans')}
|
||||
className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background rounded-full px-8 py-6 text-lg font-semibold"
|
||||
className="bg-chart-6 text-primary hover:bg-background rounded-full px-8 py-6 text-lg font-semibold"
|
||||
data-testid="try-again-button"
|
||||
>
|
||||
<CreditCard className="mr-2 h-5 w-5" />
|
||||
@@ -82,7 +82,7 @@ const PaymentCancel = () => {
|
||||
<Button
|
||||
onClick={() => navigate('/dashboard')}
|
||||
variant="outline"
|
||||
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"
|
||||
className="border-2 border-muted-foreground text-muted-foreground hover:bg-muted-foreground hover:text-white rounded-full px-8 py-6 text-lg font-semibold"
|
||||
data-testid="back-to-dashboard-button"
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-5 w-5" />
|
||||
@@ -92,17 +92,17 @@ const PaymentCancel = () => {
|
||||
</Card>
|
||||
|
||||
{/* Support Section */}
|
||||
<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-[var(--purple-ink)] mb-3 text-center" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Card className="p-6 bg-gradient-to-br from-chart-6/20 to-muted/20 rounded-2xl border border-chart-6">
|
||||
<h3 className="text-lg font-semibold text-primary mb-3 text-center" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Need Assistance?
|
||||
</h3>
|
||||
<p className="text-brand-purple text-center mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground 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.
|
||||
</p>
|
||||
<div className="text-center">
|
||||
<a
|
||||
href="mailto:support@loaf.org"
|
||||
className="text-[var(--orange-light)] hover:text-brand-purple font-medium text-lg"
|
||||
className="text-accent hover:text-muted-foreground font-medium text-lg"
|
||||
>
|
||||
support@loaf.org
|
||||
</a>
|
||||
|
||||
@@ -27,53 +27,53 @@ const PaymentSuccess = () => {
|
||||
<div className="text-center mb-12">
|
||||
{/* Success Icon */}
|
||||
<div className="mb-8">
|
||||
<div className="bg-[var(--green-light)] rounded-full w-24 h-24 mx-auto flex items-center justify-center">
|
||||
<div className="bg-success rounded-full w-24 h-24 mx-auto flex items-center justify-center">
|
||||
<CheckCircle className="h-12 w-12 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Success Message */}
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Payment Successful!
|
||||
</h1>
|
||||
<p className="text-lg text-brand-purple max-w-2xl mx-auto mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Thank you for your payment. Your LOAF membership is now active!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Confirmation Card */}
|
||||
<Card className="p-8 bg-background rounded-2xl border border-[var(--neutral-800)] shadow-lg mb-8">
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6 text-center" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Card className="p-8 bg-background rounded-2xl border border-chart-6 shadow-lg mb-8">
|
||||
<h2 className="text-2xl font-semibold text-primary mb-6 text-center" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Welcome to the LOAF Community!
|
||||
</h2>
|
||||
|
||||
<div className="space-y-6 mb-8">
|
||||
<div className="bg-[var(--lavender-300)] p-6 rounded-xl">
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<div className="bg-muted p-6 rounded-xl">
|
||||
<h3 className="text-lg font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
What's Next?
|
||||
</h3>
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle className="h-5 w-5 text-[var(--green-light)] flex-shrink-0 mt-0.5" />
|
||||
<span className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<CheckCircle className="h-5 w-5 text-success flex-shrink-0 mt-0.5" />
|
||||
<span className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Your membership is now active and you have full access to all member benefits
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle className="h-5 w-5 text-[var(--green-light)] flex-shrink-0 mt-0.5" />
|
||||
<span className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<CheckCircle className="h-5 w-5 text-success flex-shrink-0 mt-0.5" />
|
||||
<span className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
You can now RSVP and attend members-only events
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle className="h-5 w-5 text-[var(--green-light)] flex-shrink-0 mt-0.5" />
|
||||
<span className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<CheckCircle className="h-5 w-5 text-success flex-shrink-0 mt-0.5" />
|
||||
<span className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Access the community directory and connect with other members
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<CheckCircle className="h-5 w-5 text-[var(--green-light)] flex-shrink-0 mt-0.5" />
|
||||
<span className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<CheckCircle className="h-5 w-5 text-success flex-shrink-0 mt-0.5" />
|
||||
<span className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
You'll receive our newsletter with exclusive updates and announcements
|
||||
</span>
|
||||
</li>
|
||||
@@ -81,12 +81,12 @@ const PaymentSuccess = () => {
|
||||
</div>
|
||||
|
||||
{sessionId && (
|
||||
<div className="bg-[var(--neutral-800)]/20 p-4 rounded-xl">
|
||||
<p className="text-sm text-brand-purple text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<span className="font-medium text-[var(--purple-ink)]">Transaction ID:</span>{' '}
|
||||
<div className="bg-chart-6/20 p-4 rounded-xl">
|
||||
<p className="text-sm text-muted-foreground text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<span className="font-medium text-primary">Transaction ID:</span>{' '}
|
||||
<span className="font-mono text-xs">{sessionId}</span>
|
||||
</p>
|
||||
<p className="text-xs text-brand-purple text-center mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-xs text-muted-foreground text-center mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
A confirmation email has been sent to your registered email address.
|
||||
</p>
|
||||
</div>
|
||||
@@ -97,7 +97,7 @@ const PaymentSuccess = () => {
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button
|
||||
onClick={() => navigate('/dashboard')}
|
||||
className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background rounded-full px-8 py-6 text-lg font-semibold"
|
||||
className="bg-chart-6 text-primary hover:bg-background rounded-full px-8 py-6 text-lg font-semibold"
|
||||
data-testid="go-to-dashboard-button"
|
||||
>
|
||||
<User className="mr-2 h-5 w-5" />
|
||||
@@ -106,7 +106,7 @@ const PaymentSuccess = () => {
|
||||
<Button
|
||||
onClick={() => navigate('/events')}
|
||||
variant="outline"
|
||||
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"
|
||||
className="border-2 border-muted-foreground text-muted-foreground hover:bg-muted-foreground hover:text-white rounded-full px-8 py-6 text-lg font-semibold"
|
||||
data-testid="browse-events-button"
|
||||
>
|
||||
<Calendar className="mr-2 h-5 w-5" />
|
||||
@@ -117,11 +117,11 @@ const PaymentSuccess = () => {
|
||||
|
||||
{/* Additional Info */}
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Need help? Contact us at{' '}
|
||||
<a
|
||||
href="mailto:support@loaf.org"
|
||||
className="text-[var(--orange-light)] hover:text-brand-purple font-medium"
|
||||
className="text-accent hover:text-muted-foreground font-medium"
|
||||
>
|
||||
support@loaf.org
|
||||
</a>
|
||||
|
||||
@@ -214,30 +214,30 @@ const Plans = () => {
|
||||
<div className="max-w-7xl mx-auto px-6 py-12">
|
||||
{/* Header */}
|
||||
<div className="mb-12 text-center">
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Membership Plans
|
||||
</h1>
|
||||
<p className="text-lg text-brand-purple max-w-2xl mx-auto" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground 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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Status Banner */}
|
||||
{statusInfo && statusInfo.title && (
|
||||
<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 ">
|
||||
<Card className="max-w-3xl mx-auto mb-8 p-6 bg-gradient-to-r from-muted to-chart-6/30 border-2 border-muted-foreground">
|
||||
<div className="flex items-start gap-4">
|
||||
<AlertCircle className="h-6 w-6 text-brand-purple flex-shrink-0 mt-1" />
|
||||
<AlertCircle className="h-6 w-6 text-muted-foreground flex-shrink-0 mt-1" />
|
||||
<div className="flex-1">
|
||||
<h3 className="text-xl font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-xl font-semibold text-primary mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{statusInfo.title}
|
||||
</h3>
|
||||
<p className="text-brand-purple mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{statusInfo.message}
|
||||
</p>
|
||||
{statusInfo.action && statusInfo.actionLink && (
|
||||
<Button
|
||||
onClick={() => navigate(statusInfo.actionLink)}
|
||||
className="bg-brand-purple text-white hover:bg-[var(--purple-ink)] rounded-full"
|
||||
className="bg-muted-foreground text-white hover:bg-primary rounded-full"
|
||||
>
|
||||
{statusInfo.action}
|
||||
</Button>
|
||||
@@ -249,8 +249,8 @@ const Plans = () => {
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-20">
|
||||
<Loader2 className="h-12 w-12 text-brand-purple mx-auto mb-4 animate-spin" />
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading plans...</p>
|
||||
<Loader2 className="h-12 w-12 text-muted-foreground mx-auto mb-4 animate-spin" />
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading plans...</p>
|
||||
</div>
|
||||
) : plans.length > 0 ? (
|
||||
<div className={`grid gap-6 sm:gap-8 mx-auto ${plans.length === 1
|
||||
@@ -266,19 +266,19 @@ const Plans = () => {
|
||||
return (
|
||||
<Card
|
||||
key={plan.id}
|
||||
className="p-8 bg-background rounded-2xl border-2 border-[var(--neutral-800)] hover:border-brand-purple hover:shadow-xl transition-all"
|
||||
className="p-8 bg-background rounded-2xl border-2 border-chart-6 hover:border-muted-foreground hover:shadow-xl transition-all"
|
||||
data-testid={`plan-card-${plan.id}`}
|
||||
>
|
||||
{/* Plan Header */}
|
||||
<div className="text-center mb-6">
|
||||
<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-brand-purple " />
|
||||
<div className="bg-chart-6/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-muted-foreground" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h2 className="text-2xl font-semibold text-primary mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{plan.name}
|
||||
</h2>
|
||||
{plan.description && (
|
||||
<p className="text-sm text-brand-purple mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{plan.description}
|
||||
</p>
|
||||
)}
|
||||
@@ -286,22 +286,22 @@ const Plans = () => {
|
||||
|
||||
{/* Pricing */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="text-sm text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="text-sm text-muted-foreground mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Starting at
|
||||
</div>
|
||||
<div className="text-2xl sm:text-3xl md:text-4xl font-bold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<div className="text-2xl sm:text-3xl md:text-4xl font-bold text-primary mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{formatPrice(minimumPrice)}
|
||||
</div>
|
||||
{suggestedPrice > minimumPrice && (
|
||||
<div className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="text-sm text-muted-foreground mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Suggested: {formatPrice(suggestedPrice)}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{getBillingCycleLabel(plan.billing_cycle)}
|
||||
</p>
|
||||
{plan.allow_donation && (
|
||||
<div className="mt-2 flex items-center justify-center gap-1 text-xs text-[var(--orange-light)]">
|
||||
<div className="mt-2 flex items-center justify-center gap-1 text-xs text-accent">
|
||||
<Heart className="h-3 w-3" />
|
||||
<span>Donations welcome</span>
|
||||
</div>
|
||||
@@ -311,20 +311,20 @@ const Plans = () => {
|
||||
{/* Features */}
|
||||
<div className="space-y-3 mb-8">
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle className="h-5 w-5 text-[var(--green-light)] flex-shrink-0 mt-0.5" />
|
||||
<span className="text-sm text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Access to all member events</span>
|
||||
<CheckCircle className="h-5 w-5 text-success flex-shrink-0 mt-0.5" />
|
||||
<span className="text-sm text-primary" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Access to all member events</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle className="h-5 w-5 text-[var(--green-light)] flex-shrink-0 mt-0.5" />
|
||||
<span className="text-sm text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Community directory access</span>
|
||||
<CheckCircle className="h-5 w-5 text-success flex-shrink-0 mt-0.5" />
|
||||
<span className="text-sm text-primary" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Community directory access</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle className="h-5 w-5 text-[var(--green-light)] flex-shrink-0 mt-0.5" />
|
||||
<span className="text-sm text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Exclusive member benefits</span>
|
||||
<CheckCircle className="h-5 w-5 text-success flex-shrink-0 mt-0.5" />
|
||||
<span className="text-sm text-primary" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Exclusive member benefits</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle className="h-5 w-5 text-[var(--green-light)] flex-shrink-0 mt-0.5" />
|
||||
<span className="text-sm text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Newsletter subscription</span>
|
||||
<CheckCircle className="h-5 w-5 text-success flex-shrink-0 mt-0.5" />
|
||||
<span className="text-sm text-primary" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Newsletter subscription</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -332,7 +332,7 @@ const Plans = () => {
|
||||
<Button
|
||||
onClick={() => handleSelectPlan(plan)}
|
||||
disabled={processingPlanId === plan.id || (statusInfo && !statusInfo.canSubscribe)}
|
||||
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"
|
||||
className="w-full bg-chart-6 text-primary hover:bg-background rounded-full py-6 text-lg font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
data-testid={`subscribe-button-${plan.id}`}
|
||||
>
|
||||
{processingPlanId === plan.id ? (
|
||||
@@ -352,11 +352,11 @@ const Plans = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-20">
|
||||
<CreditCard className="h-20 w-20 text-[var(--neutral-800)] mx-auto mb-6" />
|
||||
<h3 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<CreditCard className="h-20 w-20 text-chart-6 mx-auto mb-6" />
|
||||
<h3 className="text-2xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
No Plans Available
|
||||
</h3>
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Membership plans are not currently available. Please check back later!
|
||||
</p>
|
||||
</div>
|
||||
@@ -364,17 +364,17 @@ const Plans = () => {
|
||||
|
||||
{/* Info Section */}
|
||||
<div className="mt-16 max-w-3xl mx-auto">
|
||||
<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-[var(--purple-ink)] mb-4 text-center" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Card className="p-8 bg-gradient-to-br from-chart-6/20 to-muted/20 rounded-2xl border border-chart-6">
|
||||
<h3 className="text-xl font-semibold text-primary mb-4 text-center" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Need Help Choosing?
|
||||
</h3>
|
||||
<p className="text-brand-purple text-center mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground 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.
|
||||
</p>
|
||||
<div className="text-center">
|
||||
<a
|
||||
href="mailto:support@loaf.org"
|
||||
className="text-[var(--orange-light)] hover:text-brand-purple font-medium"
|
||||
className="text-accent hover:text-muted-foreground font-medium"
|
||||
>
|
||||
support@loaf.org
|
||||
</a>
|
||||
@@ -387,10 +387,10 @@ const Plans = () => {
|
||||
<Dialog open={amountDialogOpen} onOpenChange={setAmountDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<DialogTitle className="text-2xl text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Choose Your Amount
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<DialogDescription className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{selectedPlan?.name} - {getBillingCycleLabel(selectedPlan?.billing_cycle)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
@@ -398,11 +398,11 @@ const Plans = () => {
|
||||
<div className="space-y-6">
|
||||
{/* Amount Input */}
|
||||
<div>
|
||||
<Label htmlFor="amount" className="text-[var(--purple-ink)]">
|
||||
<Label htmlFor="amount" className="text-primary">
|
||||
Amount (USD) *
|
||||
</Label>
|
||||
<div className="relative mt-2">
|
||||
<span className="absolute left-4 top-1/2 transform -translate-y-1/2 text-brand-purple text-lg font-semibold">
|
||||
<span className="absolute left-4 top-1/2 transform -translate-y-1/2 text-muted-foreground text-lg font-semibold">
|
||||
$
|
||||
</span>
|
||||
<Input
|
||||
@@ -412,25 +412,25 @@ const Plans = () => {
|
||||
min={selectedPlan ? (selectedPlan.minimum_price_cents / 100).toFixed(2) : "30.00"}
|
||||
value={amountInput}
|
||||
onChange={(e) => setAmountInput(e.target.value)}
|
||||
className="pl-8 h-14 text-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="pl-8 h-14 text-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="50.00"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-brand-purple mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Minimum: {selectedPlan ? formatPrice(selectedPlan.minimum_price_cents || 3000) : '$30.00'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Breakdown Display */}
|
||||
{breakdown && breakdown.total >= breakdown.base && (
|
||||
<Card className="p-4 bg-[var(--lavender-400)] border border-[var(--neutral-800)]">
|
||||
<Card className="p-4 bg-lavender-mist border border-chart-6">
|
||||
<div className="space-y-2 text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="flex justify-between text-[var(--purple-ink)]">
|
||||
<div className="flex justify-between text-primary">
|
||||
<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)]">
|
||||
<div className="flex justify-between text-accent">
|
||||
<span className="flex items-center gap-1">
|
||||
<Heart className="h-4 w-4" />
|
||||
Additional Donation:
|
||||
@@ -438,7 +438,7 @@ const Plans = () => {
|
||||
<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)]">
|
||||
<div className="flex justify-between text-primary font-bold text-base pt-2 border-t border-chart-6">
|
||||
<span>Total:</span>
|
||||
<span>{formatPrice(breakdown.total)}</span>
|
||||
</div>
|
||||
@@ -448,8 +448,8 @@ const Plans = () => {
|
||||
|
||||
{/* Donation Message */}
|
||||
{selectedPlan?.allow_donation && (
|
||||
<div className="bg-[var(--neutral-800)]/20 rounded-lg p-4">
|
||||
<p className="text-sm text-[var(--purple-ink)] text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="bg-chart-6/20 rounded-lg p-4">
|
||||
<p className="text-sm text-primary text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<strong>Thank you for supporting our community!</strong><br />
|
||||
Your donation helps us continue our mission and provide meaningful experiences for all members.
|
||||
</p>
|
||||
@@ -469,7 +469,7 @@ const Plans = () => {
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleCheckout}
|
||||
className="flex-1 bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background"
|
||||
className="flex-1 bg-chart-6 text-primary hover:bg-background"
|
||||
>
|
||||
Continue to Checkout
|
||||
</Button>
|
||||
|
||||
@@ -7,7 +7,7 @@ export default function PrivacyPolicy() {
|
||||
return (
|
||||
<>
|
||||
<PublicNavbar />
|
||||
<main className="bg-gradient-to-bl from-[var(--neutral-100:)] to-[var(--neutral-800)] text-[var(--purple-deep)]">
|
||||
<main className="bg-gradient-to-bl from-[#F9FAFB] to-chart-6 text-foreground">
|
||||
<div className="mx-auto w-full max-w-5xl px-4 sm:px-6 lg:px-8 py-10">
|
||||
<header className="border-b pb-6">
|
||||
<h1 className="text-3xl sm:text-4xl font-bold tracking-tight" style={{ fontFamily: 'Poppins' }}>
|
||||
@@ -15,7 +15,7 @@ export default function PrivacyPolicy() {
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<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="prose text-foreground max-w-none prose-h2:mt-10 prose-h2:scroll-mt-24 prose-h3:mt-6">
|
||||
<section className="mt-8">
|
||||
<p>
|
||||
This Privacy Policy ("Policy") applies to Membership Applications, and LOAFers, Inc. ("Company") and
|
||||
@@ -31,7 +31,7 @@ export default function PrivacyPolicy() {
|
||||
</section>
|
||||
|
||||
<section id="user-data" className="scroll-mt-24">
|
||||
<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-foreground">What User Data We Collect</h2>
|
||||
<p>When you visit the Site, we may collect the following data:</p>
|
||||
<ul className="list-disc pl-6 space-y-1">
|
||||
<li>Your IP address.</li>
|
||||
@@ -63,7 +63,7 @@ export default function PrivacyPolicy() {
|
||||
</section>
|
||||
|
||||
<section id="why-collect" className="scroll-mt-24">
|
||||
<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-foreground">Why We Collect Your Data</h2>
|
||||
<ul className="list-disc pl-6 space-y-1">
|
||||
<li>
|
||||
To send you announcement emails containing the information about our events and information we think you
|
||||
@@ -75,7 +75,7 @@ export default function PrivacyPolicy() {
|
||||
</section>
|
||||
|
||||
<section id="third-parties" className="scroll-mt-24">
|
||||
<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-foreground">Sharing Information with Third Parties</h2>
|
||||
<p>The Company does not sell, rent, or lease personal data to third parties.</p>
|
||||
<p>
|
||||
The Company may share data with trusted partners to help perform statistical analysis, provide customer
|
||||
@@ -89,7 +89,7 @@ export default function PrivacyPolicy() {
|
||||
</section>
|
||||
|
||||
<section id="safeguarding" className="scroll-mt-24">
|
||||
<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-foreground">Safeguarding and Securing the Data</h2>
|
||||
<p>
|
||||
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
|
||||
@@ -98,7 +98,7 @@ export default function PrivacyPolicy() {
|
||||
</section>
|
||||
|
||||
<section id="cookies" className="scroll-mt-24">
|
||||
<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-foreground">Our Cookie Policy</h2>
|
||||
<p>
|
||||
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).
|
||||
@@ -123,7 +123,7 @@ export default function PrivacyPolicy() {
|
||||
</section>
|
||||
|
||||
<section id="other-sites" className="scroll-mt-24">
|
||||
<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-foreground">Links to Other Websites</h2>
|
||||
<p>
|
||||
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
|
||||
@@ -133,7 +133,7 @@ export default function PrivacyPolicy() {
|
||||
</section>
|
||||
|
||||
<section id="restricting" className="scroll-mt-24">
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-foreground">
|
||||
Restricting the Collection of your Personal Data
|
||||
</h2>
|
||||
<p>
|
||||
@@ -150,12 +150,12 @@ export default function PrivacyPolicy() {
|
||||
</section>
|
||||
|
||||
<section id="children" className="scroll-mt-24">
|
||||
<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-foreground">Children Under Thirteen</h2>
|
||||
<p>The Company does not knowingly collect information from children under the age of 13.</p>
|
||||
</section>
|
||||
|
||||
<section id="changes" className="scroll-mt-24">
|
||||
<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-foreground">Changes to this Statement</h2>
|
||||
<p>
|
||||
The Company may make changes to this Policy. When this occurs the effective date of this policy will be
|
||||
updated.
|
||||
@@ -163,7 +163,7 @@ export default function PrivacyPolicy() {
|
||||
</section>
|
||||
|
||||
<section id="contact" className="scroll-mt-24">
|
||||
<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-foreground">Contact Information</h2>
|
||||
<p>If you have any question, please contact LOAFers, Inc. at:</p>
|
||||
<div className="not-prose mt-4">
|
||||
<p className="font-semibold mb-2">LOAFers, Inc.</p>
|
||||
@@ -184,7 +184,7 @@ export default function PrivacyPolicy() {
|
||||
<div className="mt-8 text-center">
|
||||
<Link
|
||||
to="/"
|
||||
className="text-[var(--purple-lavender)] hover:text-[var(--purple-ink)] font-semibold transition-colors inline-flex items-center gap-2"
|
||||
className="text-muted-foreground hover:text-primary font-semibold transition-colors inline-flex items-center gap-2"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
>
|
||||
<span>←</span> Back to Home
|
||||
|
||||
@@ -12,7 +12,6 @@ import MemberFooter from '../components/MemberFooter';
|
||||
import { User, Save, Lock, Heart, Users, Mail, BookUser, Camera, Upload, Trash2 } from 'lucide-react';
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '../components/ui/avatar';
|
||||
import ChangePasswordDialog from '../components/ChangePasswordDialog';
|
||||
import TransactionHistory from '../components/TransactionHistory';
|
||||
|
||||
const Profile = () => {
|
||||
const { user } = useAuth();
|
||||
@@ -25,8 +24,6 @@ const Profile = () => {
|
||||
const fileInputRef = useRef(null);
|
||||
const [maxFileSizeMB, setMaxFileSizeMB] = useState(50); // Default 50MB
|
||||
const [maxFileSizeBytes, setMaxFileSizeBytes] = useState(52428800); // Default 50MB in bytes
|
||||
const [transactions, setTransactions] = useState({ subscriptions: [], donations: [] });
|
||||
const [transactionsLoading, setTransactionsLoading] = useState(true);
|
||||
const [formData, setFormData] = useState({
|
||||
// Personal Information
|
||||
first_name: '',
|
||||
@@ -61,7 +58,6 @@ const Profile = () => {
|
||||
useEffect(() => {
|
||||
fetchConfig();
|
||||
fetchProfile();
|
||||
fetchTransactions();
|
||||
}, []);
|
||||
|
||||
const fetchConfig = async () => {
|
||||
@@ -116,19 +112,6 @@ const Profile = () => {
|
||||
}
|
||||
};
|
||||
|
||||
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 handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
@@ -230,52 +213,52 @@ const Profile = () => {
|
||||
|
||||
if (!profileData) {
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
<div className="min-h-screen bg-background">
|
||||
<Navbar />
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading profile...</p>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading profile...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
<div className="min-h-screen bg-background">
|
||||
<Navbar />
|
||||
|
||||
<div className="max-w-4xl mx-auto px-6 py-12">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
My Profile
|
||||
</h1>
|
||||
<p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Update your personal information below.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card className="p-8 bg-white rounded-2xl border border-[var(--neutral-800)] shadow-lg">
|
||||
<Card className="p-8 bg-background rounded-2xl border border-chart-6 shadow-lg">
|
||||
{/* Read-only Information */}
|
||||
<div className="mb-8 pb-8 border-b border-[var(--neutral-800)]">
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<User className="h-6 w-6 text-brand-purple " />
|
||||
<div className="mb-8 pb-8 border-b border-chart-6">
|
||||
<h2 className="text-2xl font-semibold text-primary mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<User className="h-6 w-6 text-muted-foreground" />
|
||||
Account Information
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Email</p>
|
||||
<p className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{profileData.email}</p>
|
||||
<p className="text-sm text-muted-foreground mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Email</p>
|
||||
<p className="text-primary font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{profileData.email}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Status</p>
|
||||
<p className="text-[var(--purple-ink)] font-medium capitalize" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{profileData.status.replace('_', ' ')}</p>
|
||||
<p className="text-sm text-muted-foreground mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Status</p>
|
||||
<p className="text-primary font-medium capitalize" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{profileData.status.replace('_', ' ')}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Role</p>
|
||||
<p className="text-[var(--purple-ink)] font-medium capitalize" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{profileData.role}</p>
|
||||
<p className="text-sm text-muted-foreground mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Role</p>
|
||||
<p className="text-primary font-medium capitalize" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{profileData.role}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Date of Birth</p>
|
||||
<p className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Date of Birth</p>
|
||||
<p className="text-primary font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{new Date(profileData.date_of_birth).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
@@ -286,7 +269,7 @@ const Profile = () => {
|
||||
type="button"
|
||||
onClick={() => setPasswordDialogOpen(true)}
|
||||
variant="outline"
|
||||
className="border-2 border-brand-purple text-brand-purple hover:bg-[var(--lavender-300)] rounded-full px-6 py-3"
|
||||
className="border-2 border-muted-foreground text-muted-foreground hover:bg-muted rounded-full px-6 py-3"
|
||||
>
|
||||
<Lock className="h-4 w-4 mr-2" />
|
||||
Change Password
|
||||
@@ -295,15 +278,15 @@ const Profile = () => {
|
||||
</div>
|
||||
|
||||
{/* Profile Photo Section */}
|
||||
<div className="pb-8 mb-8 border-b border-[var(--neutral-800)]">
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Camera className="h-6 w-6 text-brand-purple " />
|
||||
<div className="pb-8 mb-8 border-b border-chart-6">
|
||||
<h2 className="text-2xl font-semibold text-primary mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Camera className="h-6 w-6 text-muted-foreground" />
|
||||
Profile Photo
|
||||
</h2>
|
||||
<div className="flex flex-col md:flex-row items-center gap-6">
|
||||
<Avatar className="h-32 w-32 border-4 border-[var(--neutral-800)]">
|
||||
<Avatar className="h-32 w-32 border-4 border-chart-6">
|
||||
<AvatarImage src={previewImage} alt="Profile" />
|
||||
<AvatarFallback className="bg-[var(--lavender-300)] text-brand-purple text-3xl">
|
||||
<AvatarFallback className="bg-muted text-muted-foreground text-3xl">
|
||||
{profileData?.first_name?.charAt(0)}{profileData?.last_name?.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
@@ -321,7 +304,7 @@ const Profile = () => {
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={uploadingPhoto}
|
||||
className="bg-brand-purple text-white hover:bg-[var(--purple-ink)] rounded-full px-6 py-3"
|
||||
className="bg-muted-foreground text-white hover:bg-primary rounded-full px-6 py-3"
|
||||
>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
{uploadingPhoto ? 'Uploading...' : 'Upload Photo'}
|
||||
@@ -340,7 +323,7 @@ const Profile = () => {
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Upload a profile photo (Max {maxFileSizeMB}MB)
|
||||
</p>
|
||||
</div>
|
||||
@@ -349,7 +332,7 @@ const Profile = () => {
|
||||
|
||||
{/* Editable Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-6" data-testid="profile-form">
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h2 className="text-2xl font-semibold text-primary mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Personal Information
|
||||
</h2>
|
||||
|
||||
@@ -361,7 +344,7 @@ const Profile = () => {
|
||||
name="first_name"
|
||||
value={formData.first_name}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
data-testid="first-name-input"
|
||||
/>
|
||||
</div>
|
||||
@@ -372,7 +355,7 @@ const Profile = () => {
|
||||
name="last_name"
|
||||
value={formData.last_name}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
data-testid="last-name-input"
|
||||
/>
|
||||
</div>
|
||||
@@ -386,7 +369,7 @@ const Profile = () => {
|
||||
type="tel"
|
||||
value={formData.phone}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
data-testid="phone-input"
|
||||
/>
|
||||
</div>
|
||||
@@ -398,7 +381,7 @@ const Profile = () => {
|
||||
name="address"
|
||||
value={formData.address}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
data-testid="address-input"
|
||||
/>
|
||||
</div>
|
||||
@@ -411,7 +394,7 @@ const Profile = () => {
|
||||
name="city"
|
||||
value={formData.city}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
data-testid="city-input"
|
||||
/>
|
||||
</div>
|
||||
@@ -422,7 +405,7 @@ const Profile = () => {
|
||||
name="state"
|
||||
value={formData.state}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
data-testid="state-input"
|
||||
/>
|
||||
</div>
|
||||
@@ -433,16 +416,16 @@ const Profile = () => {
|
||||
name="zipcode"
|
||||
value={formData.zipcode}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
data-testid="zipcode-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 2: Partner Information */}
|
||||
<div className="pt-8 mt-8 border-t border-[var(--neutral-800)]">
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Heart className="h-6 w-6 text-[var(--orange-light)]" />
|
||||
<div className="pt-8 mt-8 border-t border-chart-6">
|
||||
<h2 className="text-2xl font-semibold text-primary mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Heart className="h-6 w-6 text-accent" />
|
||||
Partner Information
|
||||
</h2>
|
||||
<div className="space-y-6">
|
||||
@@ -454,7 +437,7 @@ const Profile = () => {
|
||||
name="partner_first_name"
|
||||
value={formData.partner_first_name}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="Optional"
|
||||
/>
|
||||
</div>
|
||||
@@ -465,7 +448,7 @@ const Profile = () => {
|
||||
name="partner_last_name"
|
||||
value={formData.partner_last_name}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="Optional"
|
||||
/>
|
||||
</div>
|
||||
@@ -477,11 +460,10 @@ const Profile = () => {
|
||||
id="partner_is_member"
|
||||
name="partner_is_member"
|
||||
checked={formData.partner_is_member}
|
||||
accent-color="var(--brand-white)"
|
||||
onChange={handleCheckboxChange}
|
||||
className="ui-checkbox "
|
||||
className="w-5 h-5 text-muted-foreground border-2 border-chart-6 rounded focus:ring-muted-foreground"
|
||||
/>
|
||||
<Label htmlFor="partner_is_member" className="cursor-pointer text-[var(--purple-ink)] ">
|
||||
<Label htmlFor="partner_is_member" className="cursor-pointer text-primary">
|
||||
My partner is a current member
|
||||
</Label>
|
||||
</div>
|
||||
@@ -492,9 +474,9 @@ const Profile = () => {
|
||||
name="partner_plan_to_become_member"
|
||||
checked={formData.partner_plan_to_become_member}
|
||||
onChange={handleCheckboxChange}
|
||||
className="ui-checkbox "
|
||||
className="w-5 h-5 text-muted-foreground border-2 border-chart-6 rounded focus:ring-muted-foreground"
|
||||
/>
|
||||
<Label htmlFor="partner_plan_to_become_member" className="cursor-pointer text-[var(--purple-ink)]">
|
||||
<Label htmlFor="partner_plan_to_become_member" className="cursor-pointer text-primary">
|
||||
My partner plans to become a member
|
||||
</Label>
|
||||
</div>
|
||||
@@ -503,12 +485,12 @@ const Profile = () => {
|
||||
</div>
|
||||
|
||||
{/* Section 3: Newsletter Preferences */}
|
||||
<div className="pt-8 mt-8 border-t border-[var(--neutral-800)]">
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Mail className="h-6 w-6 text-[var(--green-light)]" />
|
||||
<div className="pt-8 mt-8 border-t border-chart-6">
|
||||
<h2 className="text-2xl font-semibold text-primary mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Mail className="h-6 w-6 text-success" />
|
||||
Newsletter Preferences
|
||||
</h2>
|
||||
<p className="text-brand-purple mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground 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">
|
||||
@@ -519,9 +501,9 @@ const Profile = () => {
|
||||
name="newsletter_publish_name"
|
||||
checked={formData.newsletter_publish_name}
|
||||
onChange={handleCheckboxChange}
|
||||
className="ui-checkbox "
|
||||
className="w-5 h-5 text-muted-foreground border-2 border-chart-6 rounded focus:ring-muted-foreground"
|
||||
/>
|
||||
<Label htmlFor="newsletter_publish_name" className="cursor-pointer text-[var(--purple-ink)]">
|
||||
<Label htmlFor="newsletter_publish_name" className="cursor-pointer text-primary">
|
||||
Publish my name
|
||||
</Label>
|
||||
</div>
|
||||
@@ -532,9 +514,9 @@ const Profile = () => {
|
||||
name="newsletter_publish_photo"
|
||||
checked={formData.newsletter_publish_photo}
|
||||
onChange={handleCheckboxChange}
|
||||
className="ui-checkbox"
|
||||
className="w-5 h-5 text-muted-foreground border-2 border-chart-6 rounded focus:ring-muted-foreground"
|
||||
/>
|
||||
<Label htmlFor="newsletter_publish_photo" className="cursor-pointer text-[var(--purple-ink)]">
|
||||
<Label htmlFor="newsletter_publish_photo" className="cursor-pointer text-primary">
|
||||
Publish my photo
|
||||
</Label>
|
||||
</div>
|
||||
@@ -545,9 +527,9 @@ const Profile = () => {
|
||||
name="newsletter_publish_birthday"
|
||||
checked={formData.newsletter_publish_birthday}
|
||||
onChange={handleCheckboxChange}
|
||||
className="ui-checkbox"
|
||||
className="w-5 h-5 text-muted-foreground border-2 border-chart-6 rounded focus:ring-muted-foreground"
|
||||
/>
|
||||
<Label htmlFor="newsletter_publish_birthday" className="cursor-pointer text-[var(--purple-ink)]">
|
||||
<Label htmlFor="newsletter_publish_birthday" className="cursor-pointer text-primary">
|
||||
Publish my birthday
|
||||
</Label>
|
||||
</div>
|
||||
@@ -558,9 +540,9 @@ const Profile = () => {
|
||||
name="newsletter_publish_none"
|
||||
checked={formData.newsletter_publish_none}
|
||||
onChange={handleCheckboxChange}
|
||||
className="ui-checkbox"
|
||||
className="w-5 h-5 text-muted-foreground border-2 border-chart-6 rounded focus:ring-muted-foreground"
|
||||
/>
|
||||
<Label htmlFor="newsletter_publish_none" className="cursor-pointer text-[var(--purple-ink)]">
|
||||
<Label htmlFor="newsletter_publish_none" className="cursor-pointer text-primary">
|
||||
Do not publish any information
|
||||
</Label>
|
||||
</div>
|
||||
@@ -568,12 +550,12 @@ const Profile = () => {
|
||||
</div>
|
||||
|
||||
{/* Section 4: Volunteer Interests */}
|
||||
<div className="pt-8 mt-8 border-t border-[var(--neutral-800)]">
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Users className="h-6 w-6 text-brand-purple " />
|
||||
<div className="pt-8 mt-8 border-t border-chart-6">
|
||||
<h2 className="text-2xl font-semibold text-primary mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Users className="h-6 w-6 text-muted-foreground" />
|
||||
Volunteer Interests
|
||||
</h2>
|
||||
<p className="text-brand-purple mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground 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">
|
||||
@@ -584,11 +566,11 @@ const Profile = () => {
|
||||
id={`volunteer_${option.replace(/\s+/g, '_').toLowerCase()}`}
|
||||
checked={formData.volunteer_interests.includes(option)}
|
||||
onChange={() => handleVolunteerToggle(option)}
|
||||
className="ui-checkbox "
|
||||
className="w-5 h-5 text-muted-foreground border-2 border-chart-6 rounded focus:ring-muted-foreground"
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`volunteer_${option.replace(/\s+/g, '_').toLowerCase()}`}
|
||||
className="cursor-pointer text-[var(--purple-ink)]"
|
||||
className="cursor-pointer text-primary"
|
||||
>
|
||||
{option}
|
||||
</Label>
|
||||
@@ -598,32 +580,32 @@ const Profile = () => {
|
||||
</div>
|
||||
|
||||
{/* Section 5: Member Directory Settings */}
|
||||
<div className="pt-8 mt-8 border-t border-[var(--neutral-800)]">
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<BookUser className="h-6 w-6 text-[var(--orange-light)]" />
|
||||
<div className="pt-8 mt-8 border-t border-chart-6">
|
||||
<h2 className="text-2xl font-semibold text-primary mb-6 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<BookUser className="h-6 w-6 text-accent" />
|
||||
Member Directory Settings
|
||||
</h2>
|
||||
<p className="text-brand-purple mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Control your visibility and information in the member directory.
|
||||
</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-lavender-mist rounded-lg">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="show_in_directory"
|
||||
name="show_in_directory"
|
||||
checked={formData.show_in_directory}
|
||||
onChange={handleCheckboxChange}
|
||||
className="ui-checkbox"
|
||||
className="w-5 h-5 text-muted-foreground border-2 border-chart-6 rounded focus:ring-muted-foreground"
|
||||
/>
|
||||
<Label htmlFor="show_in_directory" className="cursor-pointer text-[var(--purple-ink)] font-medium">
|
||||
<Label htmlFor="show_in_directory" className="cursor-pointer text-primary font-medium">
|
||||
Include me in the member directory
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{formData.show_in_directory && (
|
||||
<div className="space-y-6 pl-4 border-l-4 border-[var(--neutral-800)]">
|
||||
<div className="space-y-6 pl-4 border-l-4 border-chart-6">
|
||||
<div>
|
||||
<Label htmlFor="directory_email">Directory Email</Label>
|
||||
<Input
|
||||
@@ -632,7 +614,7 @@ const Profile = () => {
|
||||
type="email"
|
||||
value={formData.directory_email}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="Optional - email to show in directory"
|
||||
/>
|
||||
</div>
|
||||
@@ -644,7 +626,7 @@ const Profile = () => {
|
||||
name="directory_bio"
|
||||
value={formData.directory_bio}
|
||||
onChange={handleInputChange}
|
||||
className="rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple min-h-[100px]"
|
||||
className="rounded-xl border-2 border-chart-6 focus:border-muted-foreground min-h-[100px]"
|
||||
placeholder="Tell other members about yourself..."
|
||||
/>
|
||||
</div>
|
||||
@@ -656,7 +638,7 @@ const Profile = () => {
|
||||
name="directory_address"
|
||||
value={formData.directory_address}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="Optional - address to show in directory"
|
||||
/>
|
||||
</div>
|
||||
@@ -669,7 +651,7 @@ const Profile = () => {
|
||||
type="tel"
|
||||
value={formData.directory_phone}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="Optional - phone to show in directory"
|
||||
/>
|
||||
</div>
|
||||
@@ -682,7 +664,7 @@ const Profile = () => {
|
||||
type="date"
|
||||
value={formData.directory_dob}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -693,7 +675,7 @@ const Profile = () => {
|
||||
name="directory_partner_name"
|
||||
value={formData.directory_partner_name}
|
||||
onChange={handleInputChange}
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
placeholder="Optional - partner name to show in directory"
|
||||
/>
|
||||
</div>
|
||||
@@ -702,11 +684,11 @@ const Profile = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-8 mt-8 border-t border-[var(--neutral-800)]">
|
||||
<div className="pt-8 mt-8 border-t border-chart-6">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="bg-brand-purple text-white hover:bg-brand-dark-lavender rounded-full px-8 py-6 text-lg font-medium shadow-lg disabled:opacity-50"
|
||||
className="bg-chart-6 text-primary hover:bg-background rounded-full px-8 py-6 text-lg font-medium shadow-lg disabled:opacity-50"
|
||||
data-testid="save-profile-button"
|
||||
>
|
||||
<Save className="h-5 w-5 mr-2" />
|
||||
@@ -720,18 +702,6 @@ const Profile = () => {
|
||||
open={passwordDialogOpen}
|
||||
onOpenChange={setPasswordDialogOpen}
|
||||
/>
|
||||
|
||||
{/* 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>
|
||||
<MemberFooter />
|
||||
</div>
|
||||
|
||||
@@ -188,18 +188,18 @@ const Register = () => {
|
||||
|
||||
<div className="max-w-4xl mx-auto px-6 py-12">
|
||||
<div className="mb-8">
|
||||
<Link to="/" className="inline-flex items-center text-brand-purple hover:text-[var(--orange-light)] transition-colors">
|
||||
<Link to="/" className="inline-flex items-center text-muted-foreground hover:text-accent transition-colors">
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back to Home
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Card className="p-8 md:p-12 bg-background rounded-2xl border border-[var(--neutral-800)] shadow-lg">
|
||||
<Card className="p-8 md:p-12 bg-background rounded-2xl border border-chart-6 shadow-lg">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Join Our Community
|
||||
</h1>
|
||||
<p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Fill out the form below to start your membership journey.
|
||||
</p>
|
||||
</div>
|
||||
@@ -245,7 +245,7 @@ const Register = () => {
|
||||
type="button"
|
||||
onClick={handleBack}
|
||||
variant="outline"
|
||||
className="rounded-full px-6 py-6 text-lg border-2 border-[var(--neutral-800)] hover:border-brand-purple text-[var(--purple-ink)]"
|
||||
className="rounded-full px-6 py-6 text-lg border-2 border-chart-6 hover:border-muted-foreground text-primary"
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-5 w-5" />
|
||||
Back
|
||||
@@ -258,7 +258,7 @@ const Register = () => {
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleNext}
|
||||
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"
|
||||
className="bg-chart-6 text-primary hover:bg-background rounded-full px-6 py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform"
|
||||
>
|
||||
Next
|
||||
<ArrowRight className="ml-2 h-5 w-5" />
|
||||
@@ -267,7 +267,7 @@ const Register = () => {
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
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"
|
||||
className="bg-chart-6 text-primary hover:bg-background rounded-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"
|
||||
>
|
||||
{loading ? 'Creating Account...' : 'Create Account'}
|
||||
@@ -276,9 +276,9 @@ const Register = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-center text-brand-purple mt-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-center text-muted-foreground mt-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Already have an account?{' '}
|
||||
<Link to="/login" className="text-[var(--orange-light)] hover:underline font-medium">
|
||||
<Link to="/login" className="text-accent hover:underline font-medium">
|
||||
Login here
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
@@ -68,15 +68,15 @@ const ResetPassword = () => {
|
||||
<PublicNavbar />
|
||||
|
||||
<div className="max-w-md mx-auto px-6 py-12">
|
||||
<Card className="p-8 md:p-12 bg-background rounded-2xl border border-[var(--neutral-800)] shadow-lg">
|
||||
<Card className="p-8 md:p-12 bg-background rounded-2xl border border-chart-6 shadow-lg">
|
||||
<div className="mb-8 text-center">
|
||||
<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-brand-purple " />
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-muted mb-4">
|
||||
<Lock className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Reset Password
|
||||
</h1>
|
||||
<p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Enter your new password below.
|
||||
</p>
|
||||
</div>
|
||||
@@ -92,7 +92,7 @@ const ResetPassword = () => {
|
||||
value={formData.newPassword}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter new password (min. 6 characters)"
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -106,15 +106,15 @@ const ResetPassword = () => {
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Re-enter new password"
|
||||
className="h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-[var(--lavender-300)] border-l-4 border-brand-purple p-4 rounded-lg">
|
||||
<div className="bg-muted border-l-4 border-muted-foreground p-4 rounded-lg">
|
||||
<div className="flex items-start">
|
||||
<AlertCircle className="h-5 w-5 text-brand-purple mr-2 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="font-medium text-[var(--purple-ink)] mb-1">Password Requirements:</p>
|
||||
<AlertCircle className="h-5 w-5 text-muted-foreground mr-2 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="font-medium text-primary mb-1">Password Requirements:</p>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
<li>At least 6 characters long</li>
|
||||
<li>Both passwords must match</li>
|
||||
@@ -126,15 +126,15 @@ const ResetPassword = () => {
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
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"
|
||||
className="w-full bg-chart-6 text-primary 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'}
|
||||
<ArrowRight className="ml-2 h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
<p className="text-center text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-center text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Remember your password?{' '}
|
||||
<Link to="/login" className="text-[var(--orange-light)] hover:underline font-medium">
|
||||
<Link to="/login" className="text-accent hover:underline font-medium">
|
||||
Login here
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
@@ -100,10 +100,10 @@ const Resources = () => {
|
||||
<div className="min-h-screen bg-background">
|
||||
<PublicNavbar />
|
||||
|
||||
<main className="bg-gradient-to-b from-white via-[var(--lavender-300)] to-[var(--lavender-100)] px-6 py-16">
|
||||
<main className="bg-gradient-to-b from-white via-muted to-[#e8e0f5] px-6 py-16">
|
||||
{/* Header Section */}
|
||||
<section className="max-w-7xl mx-auto mb-12">
|
||||
<h1 className="text-[28px] font-bold text-[var(--purple-deep)] text-center mb-12" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-[28px] font-bold text-foreground text-center mb-12" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Tap or click on each purple tab below to open and read its contents
|
||||
</h1>
|
||||
|
||||
@@ -115,8 +115,8 @@ const Resources = () => {
|
||||
{categories.map((category, categoryIndex) => (
|
||||
<div key={categoryIndex} className="space-y-6">
|
||||
{/* Category Title */}
|
||||
<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" }}>
|
||||
<div className="flex justify-center text-4xl text-[#664ea2]">{category.icon}</div>
|
||||
<h2 className="text-[32px] leading-6 font-bold text-foreground text-center mb-8" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
{category.title}
|
||||
</h2>
|
||||
|
||||
@@ -130,7 +130,7 @@ const Resources = () => {
|
||||
{/* Accordion Button */}
|
||||
<button
|
||||
onClick={() => toggleAccordion(categoryIndex, resourceIndex)}
|
||||
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' : ''}`
|
||||
className={`w-full bg-gradient-to-tr from-foreground to-muted-foreground hover:bg-[#5a4290] text-white px-6 py-4 rounded-3xl flex items-center justify-between transition-all ${isExpanded ? 'rounded-b-none rounded-t-3xl' : ''}`
|
||||
|
||||
}
|
||||
>
|
||||
@@ -150,7 +150,7 @@ const Resources = () => {
|
||||
>
|
||||
<Card className="p-6 bg-background rounded-b-2xl rounded-t-none border-none ">
|
||||
{/* Description */}
|
||||
<p className="text-[var(--purple-deep)] mb-4 leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-foreground mb-4 leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{resource.description}
|
||||
</p>
|
||||
|
||||
@@ -158,7 +158,7 @@ const Resources = () => {
|
||||
<div className="space-y-3">
|
||||
{/* Location */}
|
||||
{resource.location && (
|
||||
<div className="flex items-start gap-2 text-[var(--purple-lavender)]">
|
||||
<div className="flex items-start gap-2 text-muted-foreground">
|
||||
<MapPin className="h-5 w-5 flex-shrink-0 mt-0.5" />
|
||||
<span className="text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{resource.location}
|
||||
@@ -168,7 +168,7 @@ const Resources = () => {
|
||||
|
||||
{/* Contact */}
|
||||
{resource.contact && (
|
||||
<div className="text-[var(--purple-lavender)]">
|
||||
<div className="text-muted-foreground">
|
||||
<p className="text-sm font-medium mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Contact: {resource.contact}
|
||||
</p>
|
||||
@@ -178,7 +178,7 @@ const Resources = () => {
|
||||
<Phone className="size-4" />
|
||||
<a
|
||||
href={`tel:${resource.phone.replace(/[^0-9]/g, '')}`}
|
||||
className="text-sm hover:text-[var(--purple-deep)] transition-colors"
|
||||
className="text-sm hover:text-foreground transition-colors"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
>
|
||||
{resource.phone}
|
||||
@@ -190,7 +190,7 @@ const Resources = () => {
|
||||
<Mail className="h-4 w-4" />
|
||||
<a
|
||||
href={`mailto:${resource.email}`}
|
||||
className="text-sm hover:text-[var(--purple-deep)] transition-colors"
|
||||
className="text-sm hover:text-foreground transition-colors"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
>
|
||||
{resource.email}
|
||||
@@ -207,7 +207,7 @@ const Resources = () => {
|
||||
href={resource.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 text-[var(--orange-light)] hover:text-[var(--orange-peach)] font-medium transition-colors mt-2"
|
||||
className="inline-flex items-center gap-2 text-accent hover:text-[#e88a63] font-medium transition-colors mt-2"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
Visit Website
|
||||
@@ -228,7 +228,7 @@ const Resources = () => {
|
||||
|
||||
{/* Additional Help Section */}
|
||||
<section className="max-w-4xl mx-auto mt-16">
|
||||
<Card className="p-8 bg-gradient-to-r from-[var(--purple-lavender)] to-[var(--purple-deep)] rounded-2xl shadow-xl text-center">
|
||||
<Card className="p-8 bg-gradient-to-r from-muted-foreground to-foreground rounded-2xl shadow-xl text-center">
|
||||
<h3 className="text-2xl font-bold text-white mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Need Additional Support?
|
||||
</h3>
|
||||
@@ -237,7 +237,7 @@ const Resources = () => {
|
||||
</p>
|
||||
<a
|
||||
href="mailto:support@loaf.org"
|
||||
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"
|
||||
className="inline-block bg-background text-foreground px-8 py-3 rounded-full font-semibold hover:bg-muted transition-colors shadow-lg"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
Contact Us
|
||||
|
||||
@@ -8,7 +8,7 @@ export default function TermsOfService() {
|
||||
return (
|
||||
<>
|
||||
<PublicNavbar />
|
||||
<main className="bg-gradient-to-bl from-[var(--neutral-100:)] to-[var(--neutral-800)] text-[var(--purple-deep)]">
|
||||
<main className="bg-gradient-to-bl from-[#F9FAFB] to-chart-6 text-foreground">
|
||||
<div className="mx-auto w-full max-w-5xl px-4 sm:px-6 lg:px-8 py-10">
|
||||
{/* Title */}
|
||||
<header className="border-b pb-6">
|
||||
@@ -21,10 +21,10 @@ export default function TermsOfService() {
|
||||
</header>
|
||||
|
||||
{/* Body */}
|
||||
<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="prose text-foreground max-w-none prose-h2:mt-10 prose-h2:scroll-mt-24 prose-h3:mt-6">
|
||||
{/* AGREEMENT */}
|
||||
<section aria-labelledby="agreement" className="mt-8">
|
||||
<h2 id="agreement" className="text-xl sm:text-2xl text-[var(--purple-deep)] font-bold ">
|
||||
<h2 id="agreement" className="text-xl sm:text-2xl text-foreground font-bold ">
|
||||
AGREEMENT TO OUR LEGAL TERMS
|
||||
</h2>
|
||||
|
||||
@@ -68,37 +68,37 @@ export default function TermsOfService() {
|
||||
</section>
|
||||
|
||||
{/* TABLE OF CONTENTS */}
|
||||
<section aria-labelledby="toc" className="text-[var(--purple-deep)]">
|
||||
<h2 id="toc" className="text-lg sm:text-xl font-bold text-[var(--purple-deep)] m-0">
|
||||
<section aria-labelledby="toc" className="text-foreground">
|
||||
<h2 id="toc" className="text-lg sm:text-xl font-bold text-foreground m-0">
|
||||
TABLE OF CONTENTS
|
||||
</h2>
|
||||
|
||||
<ol className="mt-4 list-decimal no-prose text-[var(--purple-deep)] pl-5 space-y-1">
|
||||
<li><a className="text-[var(--purple-deep)]" href="#our-services">OUR SERVICES</a></li>
|
||||
<li><a className="text-[var(--purple-deep)]" href="#ipr">INTELLECTUAL PROPERTY RIGHTS</a></li>
|
||||
<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 className="mt-4 list-decimal no-prose text-foreground pl-5 space-y-1">
|
||||
<li><a className="text-foreground" href="#our-services">OUR SERVICES</a></li>
|
||||
<li><a className="text-foreground" href="#ipr">INTELLECTUAL PROPERTY RIGHTS</a></li>
|
||||
<li><a className="text-foreground" href="#user-representations">USER REPRESENTATIONS</a></li>
|
||||
<li><a className="text-foreground" href="#prohibited-activities">PROHIBITED ACTIVITIES</a></li>
|
||||
<li><a className="text-foreground" href="#ugc">USER GENERATED CONTRIBUTIONS</a></li>
|
||||
<li><a className="text-foreground" href="#contribution-license">CONTRIBUTION LICENSE</a></li>
|
||||
<li><a className="text-foreground" href="#services-management">SERVICES MANAGEMENT</a></li>
|
||||
<li><a className="text-foreground" href="#term-termination">TERM AND TERMINATION</a></li>
|
||||
<li><a className="text-foreground" href="#modifications">MODIFICATIONS AND INTERRUPTIONS</a></li>
|
||||
<li><a className="text-foreground" href="#governing-law">GOVERNING LAW</a></li>
|
||||
<li><a className="text-foreground" href="#dispute-resolution">DISPUTE RESOLUTION</a></li>
|
||||
<li><a className="text-foreground" href="#corrections">CORRECTIONS</a></li>
|
||||
<li><a className="text-foreground" href="#disclaimer">DISCLAIMER</a></li>
|
||||
<li><a className="text-foreground" href="#limitations-liability">LIMITATIONS OF LIABILITY</a></li>
|
||||
<li><a className="text-foreground" href="#indemnification">INDEMNIFICATION</a></li>
|
||||
<li><a className="text-foreground" href="#user-data">USER DATA</a></li>
|
||||
<li><a className="text-foreground" href="#electronic-comms">ELECTRONIC COMMUNICATIONS, TRANSACTIONS, AND SIGNATURES</a></li>
|
||||
<li><a className="text-foreground" href="#miscellaneous">MISCELLANEOUS</a></li>
|
||||
<li><a className="text-foreground" href="#contact-us">CONTACT US</a></li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
{/* 1. OUR SERVICES */}
|
||||
<section id="our-services" className="scroll-mt-24">
|
||||
<h2 className="text-xl text-[var(--purple-deep)] sm:text-2xl font-bold ">1. OUR SERVICES</h2>
|
||||
<h2 className="text-xl text-foreground sm:text-2xl font-bold ">1. OUR SERVICES</h2>
|
||||
<p>
|
||||
The information provided when using the Services is not intended for distribution to or use by any person
|
||||
or entity in any jurisdiction or country where such distribution or use would be contrary to law or
|
||||
@@ -111,7 +111,7 @@ export default function TermsOfService() {
|
||||
|
||||
{/* 2. INTELLECTUAL PROPERTY RIGHTS */}
|
||||
<section id="ipr" className="scroll-mt-24">
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)] ">2. INTELLECTUAL PROPERTY RIGHTS</h2>
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-foreground ">2. INTELLECTUAL PROPERTY RIGHTS</h2>
|
||||
|
||||
<h3 className="text-lg font-semibold ">Our intellectual property</h3>
|
||||
<p>
|
||||
@@ -124,7 +124,7 @@ export default function TermsOfService() {
|
||||
internal business purpose only.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-deep)]">Your use of our Services</h3>
|
||||
<h3 className="text-lg font-semibold text-foreground">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:
|
||||
@@ -160,7 +160,7 @@ export default function TermsOfService() {
|
||||
right to use our Services will terminate immediately.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-deep)]">Your submissions</h3>
|
||||
<h3 className="text-lg font-semibold text-foreground">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
|
||||
@@ -225,7 +225,7 @@ export default function TermsOfService() {
|
||||
|
||||
{/* 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>
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-foreground">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
|
||||
@@ -261,7 +261,7 @@ export default function TermsOfService() {
|
||||
|
||||
{/* 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>
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-foreground">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
|
||||
@@ -274,7 +274,7 @@ export default function TermsOfService() {
|
||||
|
||||
{/* 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>
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-foreground">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).
|
||||
@@ -295,7 +295,7 @@ export default function TermsOfService() {
|
||||
|
||||
{/* 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>
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-foreground">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
|
||||
@@ -311,7 +311,7 @@ export default function TermsOfService() {
|
||||
|
||||
{/* 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>
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-foreground">8. TERM AND TERMINATION</h2>
|
||||
<p>
|
||||
These Legal Terms shall remain in full force and effect while you use the Services.{" "}
|
||||
<strong>
|
||||
@@ -334,7 +334,7 @@ export default function TermsOfService() {
|
||||
|
||||
{/* 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>
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-foreground">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
|
||||
@@ -357,7 +357,7 @@ export default function TermsOfService() {
|
||||
|
||||
{/* 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>
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-foreground">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
|
||||
@@ -367,7 +367,7 @@ export default function TermsOfService() {
|
||||
|
||||
{/* 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>
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-foreground">11. DISPUTE RESOLUTION</h2>
|
||||
|
||||
<h3 className="text-lg font-semibold ">Informal Negotiations</h3>
|
||||
<p>
|
||||
@@ -378,7 +378,7 @@ export default function TermsOfService() {
|
||||
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>
|
||||
<h3 className="text-lg font-semibold text-foreground">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
|
||||
@@ -393,7 +393,7 @@ export default function TermsOfService() {
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-deep)]">Restrictions</h3>
|
||||
<h3 className="text-lg font-semibold text-foreground">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)
|
||||
@@ -402,7 +402,7 @@ export default function TermsOfService() {
|
||||
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>
|
||||
<h3 className="text-lg font-semibold text-foreground">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
|
||||
@@ -420,7 +420,7 @@ export default function TermsOfService() {
|
||||
|
||||
{/* 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>
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-foreground">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
|
||||
@@ -431,7 +431,7 @@ export default function TermsOfService() {
|
||||
|
||||
{/* 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>
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-foreground">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
|
||||
@@ -462,7 +462,7 @@ export default function TermsOfService() {
|
||||
|
||||
{/* 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>
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-foreground">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
|
||||
@@ -483,7 +483,7 @@ export default function TermsOfService() {
|
||||
|
||||
{/* 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>
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-foreground">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,
|
||||
@@ -503,7 +503,7 @@ export default function TermsOfService() {
|
||||
|
||||
{/* 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>
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-foreground">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
|
||||
@@ -516,7 +516,7 @@ export default function TermsOfService() {
|
||||
|
||||
{/* 17. ELECTRONIC COMMUNICATIONS */}
|
||||
<section id="electronic-comms" className="scroll-mt-24">
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-[var(--purple-deep)]">
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-foreground">
|
||||
17. ELECTRONIC COMMUNICATIONS, TRANSACTIONS, AND SIGNATURES
|
||||
</h2>
|
||||
<p>
|
||||
@@ -539,7 +539,7 @@ export default function TermsOfService() {
|
||||
|
||||
{/* 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>
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-foreground">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
|
||||
@@ -567,7 +567,7 @@ export default function TermsOfService() {
|
||||
|
||||
{/* 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>
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-foreground">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:
|
||||
@@ -589,7 +589,7 @@ export default function TermsOfService() {
|
||||
</div>
|
||||
{/* Back to Home Link */}
|
||||
<div className="mt-8 text-center">
|
||||
<Link to="/" className="text-[var(--purple-lavender)] hover:text-[var(--purple-ink)] font-semibold transition-colors inline-flex items-center gap-2"
|
||||
<Link to="/" className="text-muted-foreground hover:text-primary font-semibold transition-colors inline-flex items-center gap-2"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<span>←</span> Back to Home
|
||||
</Link>
|
||||
|
||||
@@ -49,14 +49,14 @@ const VerifyEmail = () => {
|
||||
<PublicNavbar />
|
||||
|
||||
<div className="max-w-2xl mx-auto px-6 py-20">
|
||||
<Card className="p-6 sm:p-8 md:p-12 bg-background rounded-2xl border border-[var(--neutral-800)] shadow-lg text-center">
|
||||
<Card className="p-6 sm:p-8 md:p-12 bg-background rounded-2xl border border-chart-6 shadow-lg text-center">
|
||||
{status === 'loading' && (
|
||||
<>
|
||||
<Loader2 className="h-20 w-20 text-brand-purple mx-auto mb-6 animate-spin" />
|
||||
<h1 className="text-3xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Loader2 className="h-20 w-20 text-muted-foreground mx-auto mb-6 animate-spin" />
|
||||
<h1 className="text-3xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Verifying Your Email...
|
||||
</h1>
|
||||
<p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Please wait while we verify your email address.
|
||||
</p>
|
||||
</>
|
||||
@@ -64,19 +64,19 @@ const VerifyEmail = () => {
|
||||
|
||||
{status === 'success' && (
|
||||
<>
|
||||
<CheckCircle className="h-20 w-20 text-[var(--green-light)] mx-auto mb-6" />
|
||||
<h1 className="text-3xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<CheckCircle className="h-20 w-20 text-success mx-auto mb-6" />
|
||||
<h1 className="text-3xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Email Verified Successfully!
|
||||
</h1>
|
||||
<p className="text-lg text-brand-purple mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{message}
|
||||
</p>
|
||||
<p className="text-base text-brand-purple mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-base text-muted-foreground 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.
|
||||
</p>
|
||||
<Link to="/login">
|
||||
<Button
|
||||
className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background rounded-full px-12 py-6 text-lg font-medium shadow-lg"
|
||||
className="bg-chart-6 text-primary hover:bg-background rounded-full px-12 py-6 text-lg font-medium shadow-lg"
|
||||
data-testid="login-redirect-button"
|
||||
>
|
||||
Go to Login
|
||||
@@ -88,15 +88,15 @@ const VerifyEmail = () => {
|
||||
{status === 'error' && (
|
||||
<>
|
||||
<XCircle className="h-20 w-20 text-red-500 mx-auto mb-6" />
|
||||
<h1 className="text-3xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-3xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Verification Failed
|
||||
</h1>
|
||||
<p className="text-lg text-brand-purple mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{message}
|
||||
</p>
|
||||
<Link to="/">
|
||||
<Button
|
||||
className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background rounded-full px-12 py-6 text-lg font-medium shadow-lg"
|
||||
className="bg-chart-6 text-primary hover:bg-background rounded-full px-12 py-6 text-lg font-medium shadow-lg"
|
||||
data-testid="home-redirect-button"
|
||||
>
|
||||
Go to Home
|
||||
|
||||
@@ -169,7 +169,7 @@ const AdminBylaws = () => {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<p className="text-brand-purple ">Loading bylaws...</p>
|
||||
<p className="text-muted-foreground">Loading bylaws...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -179,17 +179,17 @@ const AdminBylaws = () => {
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Bylaws Management
|
||||
</h1>
|
||||
<p className="text-brand-purple mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Manage LOAF governing bylaws and version history
|
||||
</p>
|
||||
</div>
|
||||
{hasPermission('bylaws.create') && (
|
||||
<Button
|
||||
onClick={handleCreate}
|
||||
className="btn-lavender flex items-center gap-2"
|
||||
className="bg-muted-foreground text-white hover:bg-[#533a82] rounded-full flex items-center gap-2"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Add Version
|
||||
@@ -199,22 +199,22 @@ const AdminBylaws = () => {
|
||||
|
||||
{/* Current Bylaws */}
|
||||
{currentBylaws ? (
|
||||
<Card className="p-6 border-2 border-brand-purple ">
|
||||
<Card className="p-6 border-2 border-muted-foreground">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-light-lavender p-3 rounded-xl">
|
||||
<Scale className="h-6 w-6 " />
|
||||
<div className="bg-gradient-to-br from-muted-foreground to-primary p-3 rounded-xl">
|
||||
<Scale className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-[var(--purple-ink)]">
|
||||
<h3 className="text-xl font-semibold text-primary">
|
||||
{currentBylaws.title}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge variant={'green'} className="">
|
||||
<Badge className="bg-success text-white">
|
||||
<Check className="h-3 w-3 mr-1" />
|
||||
Current Version
|
||||
</Badge>
|
||||
<span className="text-brand-purple text-sm">
|
||||
<span className="text-muted-foreground text-sm">
|
||||
Version {currentBylaws.version}
|
||||
</span>
|
||||
</div>
|
||||
@@ -222,10 +222,10 @@ const AdminBylaws = () => {
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => window.open(currentBylaws.document_url, '_blank')}
|
||||
className="border-brand-purple text-brand-purple "
|
||||
className="border-muted-foreground text-muted-foreground"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4 mr-1" />
|
||||
View
|
||||
@@ -235,24 +235,24 @@ const AdminBylaws = () => {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(currentBylaws)}
|
||||
className="border-brand-purple text-brand-purple "
|
||||
className="border-muted-foreground text-muted-foreground"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
{hasPermission('bylaws.delete') && (
|
||||
<Button
|
||||
variant="outline-destructive"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(currentBylaws)}
|
||||
className="border-red-500 text-red-500"
|
||||
className="border-red-500 text-red-500 hover:bg-red-50"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-brand-purple ">
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span>Effective Date: <strong>{formatDate(currentBylaws.effective_date)}</strong></span>
|
||||
<span>•</span>
|
||||
<span>Document Type: <strong>{currentBylaws.document_type === 'upload' ? 'PDF Upload' : 'Link'}</strong></span>
|
||||
@@ -260,10 +260,10 @@ const AdminBylaws = () => {
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="p-12 text-center">
|
||||
<Scale className="h-16 w-16 text-[var(--neutral-800)] mx-auto mb-4" />
|
||||
<p className="text-brand-purple text-lg mb-4">No current bylaws set</p>
|
||||
<Scale className="h-16 w-16 text-chart-6 mx-auto mb-4" />
|
||||
<p className="text-muted-foreground text-lg mb-4">No current bylaws set</p>
|
||||
{hasPermission('bylaws.create') && (
|
||||
<Button onClick={handleCreate} className="bg-brand-purple text-white">
|
||||
<Button onClick={handleCreate} className="bg-muted-foreground text-white">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Create Bylaws
|
||||
</Button>
|
||||
@@ -274,7 +274,7 @@ const AdminBylaws = () => {
|
||||
{/* Historical Versions */}
|
||||
{historicalBylaws.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-[var(--purple-ink)] mb-4">
|
||||
<h2 className="text-xl font-semibold text-primary mb-4">
|
||||
Version History ({historicalBylaws.length})
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
@@ -282,10 +282,10 @@ const AdminBylaws = () => {
|
||||
<Card key={bylawsDoc.id} className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-1">
|
||||
<h3 className="text-lg font-semibold text-primary mb-1">
|
||||
{bylawsDoc.title}
|
||||
</h3>
|
||||
<div className="flex items-center gap-3 text-sm text-brand-purple ">
|
||||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
||||
<span>Version {bylawsDoc.version}</span>
|
||||
<span>•</span>
|
||||
<span>Effective {formatDate(bylawsDoc.effective_date)}</span>
|
||||
@@ -296,7 +296,7 @@ const AdminBylaws = () => {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => window.open(bylawsDoc.document_url, '_blank')}
|
||||
className="border-brand-purple text-brand-purple "
|
||||
className="border-muted-foreground text-muted-foreground"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -305,7 +305,7 @@ const AdminBylaws = () => {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(bylawsDoc)}
|
||||
className="border-brand-purple text-brand-purple "
|
||||
className="border-muted-foreground text-muted-foreground"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -315,7 +315,7 @@ const AdminBylaws = () => {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(bylawsDoc)}
|
||||
className="btn-outline-destructive"
|
||||
className="border-red-500 text-red-500 hover:bg-red-50"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -404,12 +404,12 @@ const AdminBylaws = () => {
|
||||
required={!selectedBylaws}
|
||||
/>
|
||||
{uploadedFile && (
|
||||
<p className="text-sm text-brand-purple mt-1">
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Selected: {uploadedFile.name}
|
||||
</p>
|
||||
)}
|
||||
{selectedBylaws && !uploadedFile && (
|
||||
<p className="text-sm text-brand-purple mt-1">
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Current file will be kept if no new file is selected
|
||||
</p>
|
||||
)}
|
||||
@@ -424,7 +424,7 @@ const AdminBylaws = () => {
|
||||
placeholder="https://docs.google.com/... or https://example.com/file.pdf"
|
||||
required
|
||||
/>
|
||||
<p className="text-sm text-brand-purple mt-1">
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Paste the shareable link to your document (Google Drive, Dropbox, PDF URL, etc.)
|
||||
</p>
|
||||
</div>
|
||||
@@ -455,7 +455,7 @@ const AdminBylaws = () => {
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-brand-purple text-white"
|
||||
className="bg-muted-foreground text-white"
|
||||
disabled={submitting}
|
||||
>
|
||||
{submitting ? 'Saving...' : selectedBylaws ? 'Update' : 'Create'}
|
||||
@@ -482,9 +482,9 @@ const AdminBylaws = () => {
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="destructive"
|
||||
onClick={confirmDelete}
|
||||
className="btn-outline-destructive"
|
||||
className="bg-red-500 hover:bg-red-600"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
@@ -4,16 +4,13 @@ import api from '../../utils/api';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import { Button } from '../../components/ui/button';
|
||||
import { Badge } from '../../components/ui/badge';
|
||||
import { Users, Calendar, Clock, CheckCircle, Mail, AlertCircle, Globe, CircleMinus } from 'lucide-react';
|
||||
import { StatCard } from '../../components/StatCard';
|
||||
|
||||
import { Users, Calendar, Clock, CheckCircle, Mail, AlertCircle, Globe } from 'lucide-react';
|
||||
|
||||
const AdminDashboard = () => {
|
||||
const [stats, setStats] = useState({
|
||||
totalMembers: 0,
|
||||
pendingValidations: 0,
|
||||
activeMembers: 0,
|
||||
inactiveMembers: 0
|
||||
activeMembers: 0
|
||||
});
|
||||
const [usersNeedingAttention, setUsersNeedingAttention] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -32,8 +29,7 @@ const AdminDashboard = () => {
|
||||
pendingValidations: users.filter(u =>
|
||||
['pending_email', 'pending_validation', 'pre_validated', 'payment_pending'].includes(u.status)
|
||||
).length,
|
||||
activeMembers: users.filter(u => u.status === 'active' && u.role === 'member').length,
|
||||
inactiveMembers: users.filter(u => u.status === 'inactive' && u.role === 'member').length
|
||||
activeMembers: users.filter(u => u.status === 'active' && u.role === 'member').length
|
||||
});
|
||||
|
||||
// Find users who have received 3+ reminders (may need personal outreach)
|
||||
@@ -62,16 +58,16 @@ const AdminDashboard = () => {
|
||||
<>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Admin Dashboard
|
||||
</h1>
|
||||
<p className="text-lg text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Manage users, events, and membership applications.
|
||||
</p>
|
||||
</div>
|
||||
<Link to={'/'}>
|
||||
<Button
|
||||
className="btn-lavender"
|
||||
className="bg-muted-foreground text-background hover:text-white hover:bg-[#533a82] rounded-full flex items-center gap-2"
|
||||
>
|
||||
<Globe />
|
||||
View Public Site
|
||||
@@ -80,57 +76,57 @@ const AdminDashboard = () => {
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
|
||||
<div className='rounded-3xl bg-brand-lavender/10 p-8 mb-8'>
|
||||
<div className=' text-2xl text-[var(--purple-ink)] pb-8 font-semibold'>
|
||||
Quick Overview
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6" data-testid="stat-total-users">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="bg-chart-6/20 p-3 rounded-lg">
|
||||
<Users className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8 ">
|
||||
<StatCard
|
||||
title="Total Members"
|
||||
value={loading ? '-' : stats.totalMembers}
|
||||
icon={Users}
|
||||
iconBgClass="text-brand-purple"
|
||||
dataTestId="stat-total-users"
|
||||
/>
|
||||
<StatCard
|
||||
title="Pending Validations"
|
||||
value={loading ? '-' : stats.pendingValidations}
|
||||
icon={Clock}
|
||||
iconBgClass="text-brand-light-orange"
|
||||
dataTestId="stat-total-users"
|
||||
/>
|
||||
<StatCard
|
||||
title="Active Members"
|
||||
value={loading ? '-' : stats.activeMembers}
|
||||
icon={CheckCircle}
|
||||
iconBgClass="text-[var(--green-light)]"
|
||||
dataTestId="stat-total-users"
|
||||
/>
|
||||
<StatCard
|
||||
title="Inactive Members"
|
||||
value={loading ? '-' : stats.inactiveMembers}
|
||||
icon={CircleMinus}
|
||||
iconBgClass="text-brand-pink"
|
||||
dataTestId="stat-total-users"
|
||||
/>
|
||||
|
||||
</div>
|
||||
<p className="text-3xl font-semibold text-primary mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{loading ? '-' : stats.totalMembers}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total Members</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6" data-testid="stat-pending-validations">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="bg-accent/10 p-3 rounded-lg">
|
||||
<Clock className="h-6 w-6 text-accent" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-3xl font-semibold text-primary mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{loading ? '-' : stats.pendingValidations}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Pending Validations</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6" data-testid="stat-active-members">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="bg-success/20 p-3 rounded-lg">
|
||||
<CheckCircle className="h-6 w-6 text-success" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-3xl font-semibold text-primary mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{loading ? '-' : stats.activeMembers}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Active Members</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
<Link to="/admin/members">
|
||||
<Card className="p-8 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-lg hover:-translate-y-1 transition-all cursor-pointer" data-testid="quick-action-users">
|
||||
<Users className="h-12 w-12 text-brand-purple mb-4" />
|
||||
<h3 className="text-xl font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Card className="p-8 bg-background rounded-2xl border border-chart-6 hover:shadow-lg hover:-translate-y-1 transition-all cursor-pointer" data-testid="quick-action-users">
|
||||
<Users className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-xl font-semibold text-primary mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Manage Members
|
||||
</h3>
|
||||
<p className="text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
View and manage paying members and their subscription status.
|
||||
</p>
|
||||
<Button
|
||||
className="btn-lavender mt-4"
|
||||
className="mt-4 bg-chart-6 text-primary hover:bg-background rounded-full"
|
||||
data-testid="manage-users-button"
|
||||
>
|
||||
Go to Members
|
||||
@@ -139,16 +135,16 @@ const AdminDashboard = () => {
|
||||
</Link>
|
||||
|
||||
<Link to="/admin/validations">
|
||||
<Card className="p-8 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-lg hover:-translate-y-1 transition-all cursor-pointer" data-testid="quick-action-validations">
|
||||
<Card className="p-8 bg-background rounded-2xl border border-chart-6 hover:shadow-lg hover:-translate-y-1 transition-all cursor-pointer" data-testid="quick-action-validations">
|
||||
<Clock className="h-12 w-12 text-orange-600 mb-4" />
|
||||
<h3 className="text-xl font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-xl font-semibold text-primary mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Validation Queue
|
||||
</h3>
|
||||
<p className="text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Review and validate pending membership applications.
|
||||
</p>
|
||||
<Button
|
||||
className="mt-4 btn-lavender"
|
||||
className="mt-4 bg-chart-6 text-primary hover:bg-background rounded-full"
|
||||
data-testid="manage-validations-button"
|
||||
>
|
||||
View Validations
|
||||
@@ -160,16 +156,16 @@ const AdminDashboard = () => {
|
||||
{/* Users Needing Attention Widget */}
|
||||
{usersNeedingAttention.length > 0 && (
|
||||
<div className="mt-12">
|
||||
<Card className="p-8 bg-background rounded-2xl border-2 border-[var(--orange-light)] shadow-lg">
|
||||
<Card className="p-8 bg-background rounded-2xl border-2 border-accent shadow-lg">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="bg-[var(--orange-light)]/20 p-3 rounded-lg">
|
||||
<AlertCircle className="h-6 w-6 text-[var(--orange-light)]" />
|
||||
<div className="bg-accent/20 p-3 rounded-lg">
|
||||
<AlertCircle className="h-6 w-6 text-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-2xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Members Needing Personal Outreach
|
||||
</h3>
|
||||
<p className="text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
These members have received multiple reminder emails. Consider calling them directly.
|
||||
</p>
|
||||
</div>
|
||||
@@ -178,18 +174,18 @@ const AdminDashboard = () => {
|
||||
<div className="space-y-4">
|
||||
{usersNeedingAttention.map(user => (
|
||||
<Link key={user.id} to={`/admin/users/${user.id}`}>
|
||||
<div className="p-4 bg-[var(--lavender-500)] rounded-xl border border-[var(--neutral-800)] hover:border-[var(--orange-light)] hover:shadow-md transition-all cursor-pointer">
|
||||
<div className="p-4 bg-chart-7 rounded-xl border border-chart-6 hover:border-accent hover:shadow-md transition-all cursor-pointer">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h4 className="font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h4 className="font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{user.first_name} {user.last_name}
|
||||
</h4>
|
||||
<Badge className="bg-[var(--orange-light)] text-white px-3 py-1 rounded-full text-xs">
|
||||
<Badge className="bg-accent text-white px-3 py-1 rounded-full text-xs">
|
||||
{user.totalReminders} reminder{user.totalReminders !== 1 ? 's' : ''}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p>Email: {user.email}</p>
|
||||
<p>Phone: {user.phone || 'N/A'}</p>
|
||||
<p className="capitalize">Status: {user.status.replace('_', ' ')}</p>
|
||||
@@ -214,7 +210,7 @@ const AdminDashboard = () => {
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background rounded-full text-sm"
|
||||
className="bg-chart-6 text-primary hover:bg-background rounded-full text-sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.location.href = `tel:${user.phone}`;
|
||||
@@ -228,8 +224,8 @@ const AdminDashboard = () => {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-[var(--neutral-800)]/20 rounded-lg border border-[var(--neutral-800)]">
|
||||
<p className="text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="mt-6 p-4 bg-chart-6/20 rounded-lg border border-chart-6">
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<strong>💡 Tip for helping older members:</strong> Many of our members are older ladies who may struggle with email.
|
||||
A friendly phone call can help them complete the registration process and feel more welcomed to the community.
|
||||
</p>
|
||||
|
||||
@@ -28,13 +28,7 @@ import {
|
||||
Loader2,
|
||||
Download,
|
||||
FileDown,
|
||||
Calendar,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
ExternalLink,
|
||||
Copy,
|
||||
CreditCard,
|
||||
Info
|
||||
Calendar
|
||||
} from 'lucide-react';
|
||||
|
||||
const AdminDonations = () => {
|
||||
@@ -49,7 +43,6 @@ const AdminDonations = () => {
|
||||
const [statusFilter, setStatusFilter] = useState('all');
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [endDate, setEndDate] = useState('');
|
||||
const [expandedRows, setExpandedRows] = useState(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
@@ -164,27 +157,6 @@ const AdminDonations = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const toggleRowExpansion = (donationId) => {
|
||||
setExpandedRows((prev) => {
|
||||
const newExpanded = new Set(prev);
|
||||
if (newExpanded.has(donationId)) {
|
||||
newExpanded.delete(donationId);
|
||||
} else {
|
||||
newExpanded.add(donationId);
|
||||
}
|
||||
return newExpanded;
|
||||
});
|
||||
};
|
||||
|
||||
const copyToClipboard = async (text, label) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
toast.success(`${label} copied to clipboard`);
|
||||
} catch (error) {
|
||||
toast.error('Failed to copy to clipboard');
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadgeVariant = (status) => {
|
||||
const variants = {
|
||||
completed: 'default',
|
||||
@@ -195,13 +167,13 @@ const AdminDonations = () => {
|
||||
};
|
||||
|
||||
const getTypeBadgeColor = (type) => {
|
||||
return type === 'member' ? 'bg-[var(--green-light)]' : 'bg-brand-purple ';
|
||||
return type === 'member' ? 'bg-success' : 'bg-muted-foreground';
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<Loader2 className="h-12 w-12 animate-spin text-brand-purple " />
|
||||
<Loader2 className="h-12 w-12 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -210,93 +182,93 @@ const AdminDonations = () => {
|
||||
<div className="space-y-8">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Donation Management
|
||||
</h1>
|
||||
<p className="text-brand-purple mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Track and manage all donations from members and the public
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid md:grid-cols-4 gap-6">
|
||||
<Card className="p-6 bg-background rounded-2xl border-2 border-[var(--neutral-800)]">
|
||||
<Card className="p-6 bg-background rounded-2xl border-2 border-chart-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Total Donations
|
||||
</p>
|
||||
<p className="text-3xl font-bold text-[var(--purple-ink)] mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="text-3xl font-bold text-primary mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{stats.total_donations || 0}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-[var(--neutral-800)]/20 rounded-full">
|
||||
<Heart className="h-6 w-6 text-brand-purple " />
|
||||
<div className="p-3 bg-chart-6/20 rounded-full">
|
||||
<Heart className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 bg-background rounded-2xl border-2 border-[var(--neutral-800)]">
|
||||
<Card className="p-6 bg-background rounded-2xl border-2 border-chart-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Member Donations
|
||||
</p>
|
||||
<p className="text-3xl font-bold text-[var(--green-light)] mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="text-3xl font-bold text-success mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{stats.member_donations || 0}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-[var(--green-light)]/10 rounded-full">
|
||||
<Users className="h-6 w-6 text-[var(--green-light)]" />
|
||||
<div className="p-3 bg-success/10 rounded-full">
|
||||
<Users className="h-6 w-6 text-success" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 bg-background rounded-2xl border-2 border-[var(--neutral-800)]">
|
||||
<Card className="p-6 bg-background rounded-2xl border-2 border-chart-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Public Donations
|
||||
</p>
|
||||
<p className="text-3xl font-bold text-brand-purple mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="text-3xl font-bold text-muted-foreground mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{stats.public_donations || 0}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-[var(--neutral-800)]/20 rounded-full">
|
||||
<Globe className="h-6 w-6 text-brand-purple " />
|
||||
<div className="p-3 bg-chart-6/20 rounded-full">
|
||||
<Globe className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 bg-background rounded-2xl border-2 border-[var(--neutral-800)]">
|
||||
<Card className="p-6 bg-background rounded-2xl border-2 border-chart-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Total Amount
|
||||
</p>
|
||||
<p className="text-3xl font-bold text-[var(--purple-ink)] mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="text-3xl font-bold text-primary mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{stats.total_amount || '$0.00'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-[var(--neutral-800)]/20 rounded-full">
|
||||
<DollarSign className="h-6 w-6 text-brand-purple " />
|
||||
<div className="p-3 bg-chart-6/20 rounded-full">
|
||||
<DollarSign className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filters and Actions */}
|
||||
<Card className="p-6 bg-background rounded-2xl border-2 border-[var(--neutral-800)]">
|
||||
<Card className="p-6 bg-background rounded-2xl border-2 border-chart-6">
|
||||
<div className="space-y-4">
|
||||
{/* Search and Export Row */}
|
||||
<div className="flex flex-col md:flex-row gap-4 justify-between">
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-brand-purple " />
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search by donor name or email..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 rounded-full border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="pl-10 rounded-full border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
{hasPermission('donations.export') && (
|
||||
@@ -304,26 +276,26 @@ const AdminDonations = () => {
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
disabled={exporting}
|
||||
className="bg-[var(--green-light)] text-white hover:bg-[var(--green-soft)] rounded-full px-6 py-3 flex items-center gap-2"
|
||||
className="bg-success text-white hover:bg-muted-teal-dark rounded-full px-6 py-3 flex items-center gap-2"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
{exporting ? 'Exporting...' : 'Export'}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56 bg-background rounded-xl border-2 border-[var(--neutral-800)] shadow-lg">
|
||||
<DropdownMenuContent align="end" className="w-56 bg-background rounded-xl border-2 border-chart-6 shadow-lg">
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleExport('all')}
|
||||
className="cursor-pointer hover:bg-[var(--lavender-300)] rounded-lg p-3"
|
||||
className="cursor-pointer hover:bg-muted rounded-lg p-3"
|
||||
>
|
||||
<FileDown className="h-4 w-4 mr-2 text-brand-purple " />
|
||||
<span className="text-[var(--purple-ink)]">Export All Donations</span>
|
||||
<FileDown className="h-4 w-4 mr-2 text-muted-foreground" />
|
||||
<span className="text-primary">Export All Donations</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleExport('current')}
|
||||
className="cursor-pointer hover:bg-[var(--lavender-300)] rounded-lg p-3"
|
||||
className="cursor-pointer hover:bg-muted rounded-lg p-3"
|
||||
>
|
||||
<FileDown className="h-4 w-4 mr-2 text-brand-purple " />
|
||||
<span className="text-[var(--purple-ink)]">Export Current View</span>
|
||||
<FileDown className="h-4 w-4 mr-2 text-muted-foreground" />
|
||||
<span className="text-primary">Export Current View</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -334,7 +306,7 @@ const AdminDonations = () => {
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<Select value={typeFilter} onValueChange={setTypeFilter}>
|
||||
<SelectTrigger className="rounded-full border-2 border-[var(--neutral-800)]">
|
||||
<SelectTrigger className="rounded-full border-2 border-chart-6">
|
||||
<SelectValue placeholder="All Types" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -347,7 +319,7 @@ const AdminDonations = () => {
|
||||
|
||||
<div>
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="rounded-full border-2 border-[var(--neutral-800)]">
|
||||
<SelectTrigger className="rounded-full border-2 border-chart-6">
|
||||
<SelectValue placeholder="All Statuses" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -364,7 +336,7 @@ const AdminDonations = () => {
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
className="rounded-full border-2 border-[var(--neutral-800)]"
|
||||
className="rounded-full border-2 border-chart-6"
|
||||
placeholder="Start Date"
|
||||
/>
|
||||
</div>
|
||||
@@ -374,7 +346,7 @@ const AdminDonations = () => {
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
className="rounded-full border-2 border-[var(--neutral-800)]"
|
||||
className="rounded-full border-2 border-chart-6"
|
||||
placeholder="End Date"
|
||||
/>
|
||||
</div>
|
||||
@@ -382,7 +354,7 @@ const AdminDonations = () => {
|
||||
|
||||
{/* Active Filters Summary */}
|
||||
{(searchQuery || typeFilter !== 'all' || statusFilter !== 'all' || startDate || endDate) && (
|
||||
<div className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Showing {filteredDonations.length} of {donations.length} donations
|
||||
</div>
|
||||
)}
|
||||
@@ -390,58 +362,52 @@ const AdminDonations = () => {
|
||||
</Card>
|
||||
|
||||
{/* Donations Table */}
|
||||
<Card className="bg-background rounded-2xl border-2 border-[var(--neutral-800)] overflow-hidden">
|
||||
<Card className="bg-background rounded-2xl border-2 border-chart-6 overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-[var(--lavender-300)] border-b-2 border-[var(--neutral-800)]">
|
||||
<thead className="bg-muted border-b-2 border-chart-6">
|
||||
<tr>
|
||||
<th className="px-6 py-4 text-left text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<th className="px-6 py-4 text-left text-sm font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Donor
|
||||
</th>
|
||||
<th className="px-6 py-4 text-left text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<th className="px-6 py-4 text-left text-sm font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Type
|
||||
</th>
|
||||
<th className="px-6 py-4 text-left text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<th className="px-6 py-4 text-left text-sm font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Amount
|
||||
</th>
|
||||
<th className="px-6 py-4 text-left text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<th className="px-6 py-4 text-left text-sm font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Status
|
||||
</th>
|
||||
<th className="px-6 py-4 text-left text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<th className="px-6 py-4 text-left text-sm font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Date
|
||||
</th>
|
||||
<th className="px-6 py-4 text-left text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<th className="px-6 py-4 text-left text-sm font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Payment Method
|
||||
</th>
|
||||
<th className="px-6 py-4 text-center text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Details
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-[var(--neutral-800)]">
|
||||
<tbody className="divide-y divide-chart-6">
|
||||
{filteredDonations.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan="7" className="px-6 py-12 text-center">
|
||||
<td colSpan="6" className="px-6 py-12 text-center">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Heart className="h-12 w-12 text-[var(--neutral-800)]" />
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Heart className="h-12 w-12 text-chart-6" />
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{donations.length === 0 ? 'No donations yet' : 'No donations match your filters'}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredDonations.map((donation) => {
|
||||
const isExpanded = expandedRows.has(donation.id);
|
||||
return (
|
||||
<React.Fragment key={donation.id}>
|
||||
<tr className="hover:bg-[var(--lavender-400)] transition-colors">
|
||||
filteredDonations.map((donation) => (
|
||||
<tr key={donation.id} className="hover:bg-lavender-mist transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<div>
|
||||
<p className="font-medium text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="font-medium text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{donation.donor_name || 'Anonymous'}
|
||||
</p>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{donation.donor_email || 'No email'}
|
||||
</p>
|
||||
</div>
|
||||
@@ -455,7 +421,7 @@ const AdminDonations = () => {
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<p className="font-semibold text-[var(--purple-ink)] text-lg" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="font-semibold text-primary text-lg" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{donation.amount}
|
||||
</p>
|
||||
</td>
|
||||
@@ -465,7 +431,7 @@ const AdminDonations = () => {
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-2 text-brand-purple ">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{formatDate(donation.created_at)}
|
||||
@@ -473,140 +439,12 @@ const AdminDonations = () => {
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{donation.payment_method || 'N/A'}
|
||||
</p>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => toggleRowExpansion(donation.id)}
|
||||
className="text-brand-purple hover:bg-[var(--neutral-800)]"
|
||||
>
|
||||
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
{/* Expandable Details Row */}
|
||||
{isExpanded && (
|
||||
<tr className="bg-[var(--lavender-400)]/30">
|
||||
<td colSpan="7" className="px-6 py-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">
|
||||
{/* Payment Information */}
|
||||
<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">
|
||||
{donation.payment_completed_at && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-brand-purple ">Payment Date:</span>
|
||||
<span className="text-[var(--purple-ink)] font-medium">{formatDate(donation.payment_completed_at)}</span>
|
||||
</div>
|
||||
)}
|
||||
{donation.payment_method && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-brand-purple ">Payment Method:</span>
|
||||
<span className="text-[var(--purple-ink)] font-medium capitalize">{donation.payment_method}</span>
|
||||
</div>
|
||||
)}
|
||||
{donation.card_brand && donation.card_last4 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-brand-purple ">Card:</span>
|
||||
<span className="text-[var(--purple-ink)] font-medium">{donation.card_brand} ****{donation.card_last4}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stripe Transaction IDs */}
|
||||
<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">
|
||||
{donation.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)]">
|
||||
{donation.stripe_payment_intent_id.substring(0, 20)}...
|
||||
</code>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => copyToClipboard(donation.stripe_payment_intent_id, 'Payment Intent ID')}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{donation.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)]">
|
||||
{donation.stripe_charge_id.substring(0, 20)}...
|
||||
</code>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => copyToClipboard(donation.stripe_charge_id, 'Charge ID')}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{donation.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)]">
|
||||
{donation.stripe_customer_id.substring(0, 20)}...
|
||||
</code>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => copyToClipboard(donation.stripe_customer_id, 'Customer ID')}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{donation.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(donation.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>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -615,18 +453,18 @@ const AdminDonations = () => {
|
||||
|
||||
{/* This Month Summary */}
|
||||
{stats.this_month_count > 0 && (
|
||||
<Card className="p-6 bg-gradient-to-r from-[var(--lavender-400)] to-[var(--lavender-300)] rounded-2xl border-2 border-[var(--neutral-800)]">
|
||||
<Card className="p-6 bg-gradient-to-r from-lavender-mist to-muted rounded-2xl border-2 border-chart-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-brand-purple font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
This Month's Donations
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-[var(--purple-ink)] mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="text-2xl font-bold text-primary mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{stats.this_month_count} donations • {stats.this_month_amount}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 bg-background rounded-full shadow-sm">
|
||||
<Heart className="h-8 w-8 text-[var(--orange-light)]" />
|
||||
<Heart className="h-8 w-8 text-accent" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -213,7 +213,7 @@ const AdminEventAttendance = () => {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-brand-purple ">Loading event data...</div>
|
||||
<div className="text-muted-foreground">Loading event data...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -221,8 +221,8 @@ const AdminEventAttendance = () => {
|
||||
if (!event) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-brand-purple mb-4">Event not found</p>
|
||||
<Button onClick={() => navigate('/admin/events')} className="bg-brand-purple hover:bg-[var(--purple-ink)] text-white rounded-xl">
|
||||
<p className="text-muted-foreground mb-4">Event not found</p>
|
||||
<Button onClick={() => navigate('/admin/events')} className="bg-muted-foreground hover:bg-primary text-white rounded-xl">
|
||||
Back to Events
|
||||
</Button>
|
||||
</div>
|
||||
@@ -237,24 +237,24 @@ const AdminEventAttendance = () => {
|
||||
<Button
|
||||
onClick={() => navigate('/admin/events')}
|
||||
variant="outline"
|
||||
className="border-[var(--neutral-800)] text-brand-purple rounded-xl"
|
||||
className="border-chart-6 text-muted-foreground rounded-xl"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back to Events
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Event Attendance
|
||||
</h1>
|
||||
<p className="text-brand-purple mt-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mt-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Manage RSVPs and track attendance for this event
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={exportToCSV}
|
||||
className="bg-[var(--green-light)] hover:bg-[var(--green-eucalyptus)] dark:bg-[var(--green-forest)] text-white rounded-xl"
|
||||
className="bg-success hover:bg-[#6a9a83] text-white rounded-xl"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
@@ -263,13 +263,13 @@ const AdminEventAttendance = () => {
|
||||
</div>
|
||||
|
||||
{/* Event Details Card */}
|
||||
<Card className="p-6 bg-background border-[var(--neutral-800)] rounded-xl">
|
||||
<Card className="p-6 bg-background border-chart-6 rounded-xl">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h2 className="text-2xl font-semibold text-primary mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{event.title}
|
||||
</h2>
|
||||
<div className="flex flex-wrap gap-4 text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="flex flex-wrap gap-4 text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span>{moment(event.start_at).format('MMMM D, YYYY [at] h:mm A')}</span>
|
||||
@@ -282,7 +282,7 @@ const AdminEventAttendance = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Badge className={`${event.published ? 'bg-[var(--green-light)] dark:bg-[var(--green-forest)]' : 'bg-[var(--neutral-800)]'} text-white px-3 py-1`}>
|
||||
<Badge className={`${event.published ? 'bg-success' : 'bg-chart-6'} text-white px-3 py-1`}>
|
||||
{event.published ? 'Published' : 'Draft'}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -290,59 +290,59 @@ const AdminEventAttendance = () => {
|
||||
|
||||
{/* Statistics Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||
<Card className="p-4 bg-background border-[var(--neutral-800)] rounded-xl">
|
||||
<Card className="p-4 bg-background border-chart-6 rounded-xl">
|
||||
<div className="flex items-center gap-3">
|
||||
<Users className="h-8 w-8 text-brand-purple " />
|
||||
<Users className="h-8 w-8 text-muted-foreground" />
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total RSVPs</p>
|
||||
<p className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>{stats.total}</p>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total RSVPs</p>
|
||||
<p className="text-2xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>{stats.total}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4 bg-background border-[var(--neutral-800)] rounded-xl">
|
||||
<Card className="p-4 bg-background border-chart-6 rounded-xl">
|
||||
<div className="flex items-center gap-3">
|
||||
<UserCheck className="h-8 w-8 text-[var(--green-light)] dark:text-[var(--green-forest)]" />
|
||||
<UserCheck className="h-8 w-8 text-success" />
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Yes</p>
|
||||
<p className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>{stats.yesCount}</p>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Yes</p>
|
||||
<p className="text-2xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>{stats.yesCount}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4 bg-background border-[var(--neutral-800)] rounded-xl">
|
||||
<Card className="p-4 bg-background border-chart-6 rounded-xl">
|
||||
<div className="flex items-center gap-3">
|
||||
<UserX className="h-8 w-8 text-[var(--orange-soft)]" />
|
||||
<UserX className="h-8 w-8 text-burnt-peach" />
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>No</p>
|
||||
<p className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>{stats.noCount}</p>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>No</p>
|
||||
<p className="text-2xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>{stats.noCount}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4 bg-background border-[var(--neutral-800)] rounded-xl">
|
||||
<Card className="p-4 bg-background border-chart-6 rounded-xl">
|
||||
<div className="flex items-center gap-3">
|
||||
<HelpCircle className="h-8 w-8 text-[var(--gold-warm)]" />
|
||||
<HelpCircle className="h-8 w-8 text-apricot-cream" />
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Maybe</p>
|
||||
<p className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>{stats.maybeCount}</p>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Maybe</p>
|
||||
<p className="text-2xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>{stats.maybeCount}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4 bg-background border-[var(--neutral-800)] rounded-xl">
|
||||
<Card className="p-4 bg-background border-chart-6 rounded-xl">
|
||||
<div className="flex items-center gap-3">
|
||||
<Check className="h-8 w-8 text-brand-purple " />
|
||||
<Check className="h-8 w-8 text-muted-foreground" />
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Attended</p>
|
||||
<p className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>{stats.attendedCount}</p>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Attended</p>
|
||||
<p className="text-2xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>{stats.attendedCount}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filters and Actions */}
|
||||
<Card className="p-6 bg-background border-[var(--neutral-800)] rounded-xl">
|
||||
<Card className="p-6 bg-background border-chart-6 rounded-xl">
|
||||
<div className="space-y-4">
|
||||
{/* Tab Filters */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -350,8 +350,8 @@ const AdminEventAttendance = () => {
|
||||
onClick={() => setActiveTab('all')}
|
||||
variant={activeTab === 'all' ? 'default' : 'outline'}
|
||||
className={`rounded-xl ${activeTab === 'all'
|
||||
? 'bg-brand-purple hover:bg-[var(--purple-ink)] dark:bg-brand-dark-lavender text-white'
|
||||
: 'border-[var(--neutral-800)] text-brand-purple hover:bg-[var(--lavender-500)]'
|
||||
? 'bg-muted-foreground hover:bg-primary text-white'
|
||||
: 'border-chart-6 text-muted-foreground hover:bg-chart-7'
|
||||
}`}
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
@@ -361,8 +361,8 @@ const AdminEventAttendance = () => {
|
||||
onClick={() => setActiveTab('yes')}
|
||||
variant={activeTab === 'yes' ? 'default' : 'outline'}
|
||||
className={`rounded-xl ${activeTab === 'yes'
|
||||
? 'bg-[var(--green-light)] hover:bg-[var(--green-eucalyptus)] dark:bg-[var(--green-forest)] text-white'
|
||||
: 'border-[var(--neutral-800)] text-brand-purple hover:bg-[var(--lavender-500)]'
|
||||
? 'bg-success hover:bg-[#6a9a83] text-white'
|
||||
: 'border-chart-6 text-muted-foreground hover:bg-chart-7'
|
||||
}`}
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
@@ -372,8 +372,8 @@ const AdminEventAttendance = () => {
|
||||
onClick={() => setActiveTab('no')}
|
||||
variant={activeTab === 'no' ? 'default' : 'outline'}
|
||||
className={`rounded-xl ${activeTab === 'no'
|
||||
? 'bg-[var(--orange-soft)] hover:bg-[var(--orange-rust)] text-white'
|
||||
: 'border-[var(--neutral-800)] text-brand-purple hover:bg-[var(--lavender-500)]'
|
||||
? 'bg-burnt-peach hover:bg-[#d16b54] text-white'
|
||||
: 'border-chart-6 text-muted-foreground hover:bg-chart-7'
|
||||
}`}
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
@@ -383,8 +383,8 @@ const AdminEventAttendance = () => {
|
||||
onClick={() => setActiveTab('maybe')}
|
||||
variant={activeTab === 'maybe' ? 'default' : 'outline'}
|
||||
className={`rounded-xl ${activeTab === 'maybe'
|
||||
? 'bg-[var(--gold-warm)] dark:bg-orange-400 hover:bg-[var(--gold-soft)] text-white'
|
||||
: 'border-[var(--neutral-800)] text-brand-purple hover:bg-[var(--lavender-500)]'
|
||||
? 'bg-apricot-cream hover:bg-[#e8bf7a] text-primary'
|
||||
: 'border-chart-6 text-muted-foreground hover:bg-chart-7'
|
||||
}`}
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
@@ -395,26 +395,26 @@ const AdminEventAttendance = () => {
|
||||
{/* Search and Bulk Actions */}
|
||||
<div className="flex flex-wrap gap-3 items-center justify-between">
|
||||
<div className="flex-1 min-w-[200px] max-w-md relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-brand-purple " />
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search by name or email..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 border-[var(--neutral-800)] rounded-xl"
|
||||
className="pl-10 border-chart-6 rounded-xl"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedRsvps.size > 0 && (
|
||||
<div className="flex gap-2">
|
||||
<Badge className="bg-brand-purple text-white px-3 py-1">
|
||||
<Badge className="bg-muted-foreground text-white px-3 py-1">
|
||||
{selectedRsvps.size} selected
|
||||
</Badge>
|
||||
<Button
|
||||
onClick={() => handleBulkAttendance(true)}
|
||||
disabled={saving}
|
||||
className="bg-[var(--green-light)] hover:bg-[var(--green-eucalyptus)] text-white rounded-xl"
|
||||
className="bg-success hover:bg-[#6a9a83] text-white rounded-xl"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
<Check className="h-4 w-4 mr-1" />
|
||||
@@ -423,7 +423,7 @@ const AdminEventAttendance = () => {
|
||||
<Button
|
||||
onClick={() => handleBulkAttendance(false)}
|
||||
disabled={saving}
|
||||
className="bg-[var(--orange-soft)] hover:bg-[var(--orange-rust)] text-white rounded-xl"
|
||||
className="bg-burnt-peach hover:bg-[#d16b54] text-white rounded-xl"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
<X className="h-4 w-4 mr-1" />
|
||||
@@ -436,10 +436,10 @@ const AdminEventAttendance = () => {
|
||||
</Card>
|
||||
|
||||
{/* RSVP Table */}
|
||||
<Card className="bg-background border-[var(--neutral-800)] rounded-xl overflow-hidden">
|
||||
<Card className="bg-background border-chart-6 rounded-xl overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-[var(--lavender-500)] border-b border-[var(--neutral-800)]">
|
||||
<thead className="bg-chart-7 border-b border-chart-6">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left">
|
||||
<Checkbox
|
||||
@@ -447,19 +447,19 @@ const AdminEventAttendance = () => {
|
||||
onCheckedChange={handleSelectAll}
|
||||
/>
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Name
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Email
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
RSVP Status
|
||||
</th>
|
||||
<th className="px-4 py-3 text-center text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<th className="px-4 py-3 text-center text-sm font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Attendance
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Attended At
|
||||
</th>
|
||||
</tr>
|
||||
@@ -467,26 +467,26 @@ const AdminEventAttendance = () => {
|
||||
<tbody>
|
||||
{filteredRsvps.length > 0 ? (
|
||||
filteredRsvps.map((rsvp) => (
|
||||
<tr key={rsvp.user_id} className="border-b border-[var(--neutral-800)] hover:bg-[var(--lavender-500)] transition-colors">
|
||||
<tr key={rsvp.user_id} className="border-b border-chart-6 hover:bg-chart-7 transition-colors">
|
||||
<td className="px-4 py-3">
|
||||
<Checkbox
|
||||
checked={selectedRsvps.has(rsvp.user_id)}
|
||||
onCheckedChange={() => handleSelectRsvp(rsvp.user_id)}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<td className="px-4 py-3 text-sm text-primary" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{rsvp.user_name}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<td className="px-4 py-3 text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{rsvp.user_email}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<Badge
|
||||
className={`${rsvp.rsvp_status === 'yes'
|
||||
? 'bg-[var(--green-light)] dark:bg-[var(--green-forest)]'
|
||||
? 'bg-success'
|
||||
: rsvp.rsvp_status === 'no'
|
||||
? 'bg-[var(--orange-soft)]'
|
||||
: 'bg-[var(--gold-warm)] text-[var(--purple-ink)]'
|
||||
? 'bg-burnt-peach'
|
||||
: 'bg-apricot-cream text-primary'
|
||||
} text-white text-xs px-2 py-1`}
|
||||
>
|
||||
{rsvp.rsvp_status.toUpperCase()}
|
||||
@@ -498,7 +498,7 @@ const AdminEventAttendance = () => {
|
||||
onClick={() => handleIndividualAttendance(rsvp.user_id, false)}
|
||||
disabled={saving}
|
||||
size="sm"
|
||||
className="bg-[var(--green-light)] dark:bg-[var(--green-forest)] hover:bg-[var(--green-eucalyptus)] text-white rounded-lg min-w-[120px]"
|
||||
className="bg-success hover:bg-[#6a9a83] text-white rounded-lg min-w-[120px]"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
<Check className="h-3 w-3 mr-1" />
|
||||
@@ -510,7 +510,7 @@ const AdminEventAttendance = () => {
|
||||
disabled={saving}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="border-[var(--neutral-800)] text-brand-purple hover:bg-[var(--green-light)] dark:bg-[var(--green-forest)] hover:text-white hover:border-[var(--green-light)] dark:bg-[var(--green-forest)] rounded-lg min-w-[120px]"
|
||||
className="border-chart-6 text-muted-foreground hover:bg-success hover:text-white hover:border-success rounded-lg min-w-[120px]"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
<X className="h-3 w-3 mr-1" />
|
||||
@@ -518,7 +518,7 @@ const AdminEventAttendance = () => {
|
||||
</Button>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<td className="px-4 py-3 text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{rsvp.attended_at ? moment(rsvp.attended_at).format('MMM D, YYYY h:mm A') : '-'}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -526,7 +526,7 @@ const AdminEventAttendance = () => {
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="6" className="px-4 py-12 text-center">
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{searchQuery ? 'No RSVPs match your search' : 'No RSVPs for this filter'}
|
||||
</p>
|
||||
</td>
|
||||
|
||||
@@ -134,10 +134,10 @@ const AdminEvents = () => {
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex flex-col sm:flex-row gap-4 justify-between items-start sm:items-center">
|
||||
<div>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Event Management
|
||||
</h1>
|
||||
<p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Create and manage community events.
|
||||
</p>
|
||||
</div>
|
||||
@@ -150,7 +150,7 @@ const AdminEvents = () => {
|
||||
resetForm();
|
||||
setEditingEvent(null);
|
||||
}}
|
||||
className="btn-lavender "
|
||||
className="bg-chart-6 text-primary hover:bg-background rounded-full px-6"
|
||||
data-testid="create-event-button"
|
||||
>
|
||||
<Plus className="mr-2 h-5 w-5" />
|
||||
@@ -158,41 +158,41 @@ const AdminEvents = () => {
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="max-w-[calc(100vw-2rem)] sm:max-w-2xl max-h-[90vh] overflow-y-auto scrollbar-dashboard">
|
||||
<DialogContent className="max-w-[calc(100vw-2rem)] sm:max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<DialogTitle className="text-2xl text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{editingEvent ? 'Edit Event' : 'Create New Event'}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4 mt-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<label className="block text-sm font-medium text-primary mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Title *
|
||||
</label>
|
||||
<Input
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||
required
|
||||
className="border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<label className="block text-sm font-medium text-primary mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
rows={4}
|
||||
className="w-full border-2 border-[var(--neutral-800)] bg-background focus:border-brand-purple rounded-lg p-3"
|
||||
className="w-full border-2 border-chart-6 focus:border-muted-foreground rounded-lg p-3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<label className="block text-sm font-medium text-primary mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Start Date & Time *
|
||||
</label>
|
||||
<Input
|
||||
@@ -200,12 +200,12 @@ const AdminEvents = () => {
|
||||
value={formData.start_at}
|
||||
onChange={(e) => setFormData({ ...formData, start_at: e.target.value })}
|
||||
required
|
||||
className="border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<label className="block text-sm font-medium text-primary mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
End Date & Time *
|
||||
</label>
|
||||
<Input
|
||||
@@ -213,25 +213,25 @@ const AdminEvents = () => {
|
||||
value={formData.end_at}
|
||||
onChange={(e) => setFormData({ ...formData, end_at: e.target.value })}
|
||||
required
|
||||
className="border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<label className="block text-sm font-medium text-primary mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Location *
|
||||
</label>
|
||||
<Input
|
||||
value={formData.location}
|
||||
onChange={(e) => setFormData({ ...formData, location: e.target.value })}
|
||||
required
|
||||
className="border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<label className="block text-sm font-medium text-primary mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Capacity (optional)
|
||||
</label>
|
||||
<Input
|
||||
@@ -239,7 +239,7 @@ const AdminEvents = () => {
|
||||
value={formData.capacity}
|
||||
onChange={(e) => setFormData({ ...formData, capacity: e.target.value })}
|
||||
placeholder="Leave empty for unlimited"
|
||||
className="border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -249,9 +249,9 @@ const AdminEvents = () => {
|
||||
id="published"
|
||||
checked={formData.published}
|
||||
onChange={(e) => setFormData({ ...formData, published: e.target.checked })}
|
||||
className="w-4 h-4 ui-checkbox text-brand-purple border-[var(--neutral-800)] rounded focus:ring-brand-purple "
|
||||
className="w-4 h-4 text-muted-foreground border-chart-6 rounded focus:ring-muted-foreground"
|
||||
/>
|
||||
<label htmlFor="published" className="text-sm font-medium text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<label htmlFor="published" className="text-sm font-medium text-primary" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Publish event (make visible to members)
|
||||
</label>
|
||||
</div>
|
||||
@@ -267,7 +267,7 @@ const AdminEvents = () => {
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="btn-lavender flex-1"
|
||||
className="flex-1 bg-chart-6 text-primary hover:bg-background rounded-full"
|
||||
>
|
||||
{editingEvent ? 'Update Event' : 'Create Event'}
|
||||
</Button>
|
||||
@@ -281,25 +281,24 @@ const AdminEvents = () => {
|
||||
{/* Events List */}
|
||||
{loading ? (
|
||||
<div className="text-center py-20">
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading events...</p>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading events...</p>
|
||||
</div>
|
||||
) : events.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{events.map((event) => (
|
||||
<Card
|
||||
key={event.id}
|
||||
className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-lg transition-all"
|
||||
className="p-6 bg-background rounded-2xl border border-chart-6 hover:shadow-lg transition-all"
|
||||
data-testid={`event-card-${event.id}`}
|
||||
>
|
||||
{/* Event Header */}
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="bg-[var(--neutral-800)]/20 p-3 rounded-lg">
|
||||
<Calendar className="h-6 w-6 text-brand-purple " />
|
||||
<div className="bg-chart-6/20 p-3 rounded-lg">
|
||||
<Calendar className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
<Badge
|
||||
|
||||
className={`${event.published
|
||||
? 'border-transparent bg-[var(--green-light)] text-white hover:bg-[var(--green-forest)]'
|
||||
? 'bg-success text-white'
|
||||
: 'bg-gray-400 text-white'
|
||||
} px-3 py-1 rounded-full`}
|
||||
>
|
||||
@@ -308,18 +307,18 @@ const AdminEvents = () => {
|
||||
</div>
|
||||
|
||||
{/* Event Details */}
|
||||
<h3 className="text-xl font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-xl font-semibold text-primary mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{event.title}
|
||||
</h3>
|
||||
|
||||
{event.description && (
|
||||
<p className="text-brand-purple mb-4 line-clamp-2 text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mb-4 line-clamp-2 text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{event.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="flex items-center gap-2 text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span>
|
||||
{new Date(event.start_at).toLocaleDateString()} at{' '}
|
||||
@@ -329,24 +328,24 @@ const AdminEvents = () => {
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<MapPin className="h-4 w-4" />
|
||||
<span className="truncate">{event.location}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Users className="h-4 w-4" />
|
||||
<span>{event.rsvp_count || 0} attending</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="space-y-2 pt-4 border-t border-[var(--neutral-800)]">
|
||||
<div className="space-y-2 pt-4 border-t border-chart-6">
|
||||
{/* Manage Attendance Button */}
|
||||
<Button
|
||||
onClick={() => navigate(`/admin/events/${event.id}/attendance`)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full border-[var(--green-light)] text-[var(--green-light)] hover:bg-[var(--green-light)] hover:text-white dark:hover:text-background"
|
||||
className="w-full border-success text-success hover:bg-success hover:text-white"
|
||||
data-testid={`mark-attendance-${event.id}`}
|
||||
>
|
||||
<Users className="h-4 w-4 mr-2" />
|
||||
@@ -359,7 +358,7 @@ const AdminEvents = () => {
|
||||
onClick={() => togglePublish(event)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 border-brand-purple text-brand-purple hover:bg-brand-purple hover:text-white dark:hover:bg-brand-lavender dark:hover:text-background"
|
||||
className="flex-1 border-muted-foreground text-muted-foreground hover:bg-muted-foreground hover:text-white"
|
||||
data-testid={`toggle-publish-${event.id}`}
|
||||
>
|
||||
{event.published ? (
|
||||
@@ -378,7 +377,7 @@ const AdminEvents = () => {
|
||||
onClick={() => handleEdit(event)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-gray-400 text-gray-600 dark:text-gray-400 hover:bg-gray-400 dark:hover:text-background hover:text-white"
|
||||
className="border-gray-400 text-gray-600 hover:bg-gray-400 hover:text-white"
|
||||
data-testid={`edit-event-${event.id}`}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
@@ -399,16 +398,16 @@ const AdminEvents = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-20">
|
||||
<Calendar className="h-20 w-20 text-[var(--neutral-800)] mx-auto mb-6" />
|
||||
<h3 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Calendar className="h-20 w-20 text-chart-6 mx-auto mb-6" />
|
||||
<h3 className="text-2xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
No Events Yet
|
||||
</h3>
|
||||
<p className="text-brand-purple mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Create your first event to get started!
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => setIsCreateDialogOpen(true)}
|
||||
className="btn-lavender px-8"
|
||||
className="bg-chart-6 text-primary hover:bg-background rounded-full px-8"
|
||||
>
|
||||
<Plus className="mr-2 h-5 w-5" />
|
||||
Create Event
|
||||
|
||||
@@ -147,7 +147,7 @@ const AdminFinancials = () => {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<p className="text-brand-purple ">Loading financial reports...</p>
|
||||
<p className="text-muted-foreground">Loading financial reports...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -157,17 +157,17 @@ const AdminFinancials = () => {
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Financial Reports Management
|
||||
</h1>
|
||||
<p className="text-brand-purple mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Manage annual financial reports
|
||||
</p>
|
||||
</div>
|
||||
{hasPermission('financials.create') && (
|
||||
<Button
|
||||
onClick={handleCreate}
|
||||
className="btn-lavender flex items-center gap-2"
|
||||
className="bg-muted-foreground text-white hover:bg-[#533a82] rounded-full flex items-center gap-2"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Add Report
|
||||
@@ -178,10 +178,10 @@ const AdminFinancials = () => {
|
||||
{/* Reports List */}
|
||||
{reports.length === 0 ? (
|
||||
<Card className="p-12 text-center">
|
||||
<TrendingUp className="h-16 w-16 text-[var(--neutral-800)] mx-auto mb-4" />
|
||||
<p className="text-brand-purple text-lg mb-4">No financial reports yet</p>
|
||||
<TrendingUp className="h-16 w-16 text-chart-6 mx-auto mb-4" />
|
||||
<p className="text-muted-foreground text-lg mb-4">No financial reports yet</p>
|
||||
{hasPermission('financials.create') && (
|
||||
<Button onClick={handleCreate} className="bg-brand-purple text-white">
|
||||
<Button onClick={handleCreate} className="bg-muted-foreground text-white">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Create First Report
|
||||
</Button>
|
||||
@@ -192,23 +192,23 @@ const AdminFinancials = () => {
|
||||
{reports.map(report => (
|
||||
<Card key={report.id} className="p-6">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="bg-light-lavender p-4 rounded-xl min-w-[100px] text-center">
|
||||
<div className="bg-gradient-to-br from-muted-foreground to-primary p-4 rounded-xl text-white min-w-[100px] text-center">
|
||||
<DollarSign className="h-6 w-6 mx-auto mb-1" />
|
||||
<div className="text-2xl font-bold">{report.year}</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2">
|
||||
<h3 className="text-lg font-semibold text-primary mb-2">
|
||||
{report.title}
|
||||
</h3>
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant="outline" className="border-brand-purple text-brand-purple ">
|
||||
<Badge variant="outline" className="border-muted-foreground text-muted-foreground">
|
||||
{report.document_type === 'google_drive' ? 'Google Drive' : report.document_type.toUpperCase()}
|
||||
</Badge>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => window.open(report.document_url, '_blank')}
|
||||
className="text-brand-purple hover:text-[var(--purple-muted)]"
|
||||
className="text-muted-foreground hover:text-[#533a82]"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4 mr-1" />
|
||||
View
|
||||
@@ -222,17 +222,17 @@ const AdminFinancials = () => {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(report)}
|
||||
className="border-brand-purple text-brand-purple "
|
||||
className="border-muted-foreground text-muted-foreground"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
{hasPermission('financials.delete') && (
|
||||
<Button
|
||||
variant="outline-destructive"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(report)}
|
||||
className=""
|
||||
className="border-red-500 text-red-500 hover:bg-red-50"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -313,12 +313,12 @@ const AdminFinancials = () => {
|
||||
required={!selectedReport}
|
||||
/>
|
||||
{uploadedFile && (
|
||||
<p className="text-sm text-brand-purple mt-1">
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Selected: {uploadedFile.name}
|
||||
</p>
|
||||
)}
|
||||
{selectedReport && !uploadedFile && (
|
||||
<p className="text-sm text-brand-purple mt-1">
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Current file will be kept if no new file is selected
|
||||
</p>
|
||||
)}
|
||||
@@ -333,7 +333,7 @@ const AdminFinancials = () => {
|
||||
placeholder="https://docs.google.com/... or https://example.com/file.pdf"
|
||||
required
|
||||
/>
|
||||
<p className="text-sm text-brand-purple mt-1">
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Paste the shareable link to your document (Google Drive, Dropbox, PDF URL, etc.)
|
||||
</p>
|
||||
</div>
|
||||
@@ -350,7 +350,7 @@ const AdminFinancials = () => {
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-brand-purple text-white"
|
||||
className="bg-muted-foreground text-white"
|
||||
disabled={submitting}
|
||||
>
|
||||
{submitting ? 'Saving...' : selectedReport ? 'Update' : 'Create'}
|
||||
|
||||
@@ -153,23 +153,23 @@ const AdminGallery = () => {
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Event Gallery Management
|
||||
</h1>
|
||||
<p className="text-brand-purple mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Upload and manage photos for event galleries
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Event Selection */}
|
||||
<Card className="p-6 bg-background border-[var(--neutral-800)] rounded-xl">
|
||||
<Card className="p-6 bg-background border-chart-6 rounded-xl">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label className="text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Label className="text-primary mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Select Event
|
||||
</Label>
|
||||
<Select value={selectedEvent || ''} onValueChange={setSelectedEvent}>
|
||||
<SelectTrigger className="border-[var(--neutral-800)] rounded-xl">
|
||||
<SelectTrigger className="border-chart-6 rounded-xl">
|
||||
<SelectValue placeholder="Choose an event..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -184,19 +184,19 @@ const AdminGallery = () => {
|
||||
|
||||
{/* Empty State Message */}
|
||||
{events.length === 0 && (
|
||||
<div className="mt-4 p-4 bg-[var(--lavender-300)] dark:bg-brand-lavender/10 dark:border-transparent border-2 border-[var(--neutral-800)] rounded-xl">
|
||||
<div className="mt-4 p-4 bg-muted border-2 border-chart-6 rounded-xl">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle className="h-5 w-5 text-brand-purple flex-shrink-0 mt-0.5" />
|
||||
<AlertCircle className="h-5 w-5 text-muted-foreground flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h4 className="text-sm font-semibold text-[var(--purple-ink)] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h4 className="text-sm font-semibold text-primary mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
No Events Available
|
||||
</h4>
|
||||
<p className="text-sm text-brand-purple mb-3" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground mb-3" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
You need to create an event before uploading gallery images. Events help organize photos by occasion.
|
||||
</p>
|
||||
<Link to="/admin/events">
|
||||
<Button
|
||||
className="btn-lavender text-sm"
|
||||
className="bg-muted-foreground hover:bg-primary text-white rounded-xl text-sm"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
<Calendar className="h-4 w-4 mr-2" />
|
||||
@@ -221,7 +221,7 @@ const AdminGallery = () => {
|
||||
<Button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={uploading}
|
||||
className="btn-lavender "
|
||||
className="bg-muted-foreground hover:bg-primary text-white rounded-xl"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
{uploading ? (
|
||||
@@ -236,7 +236,7 @@ const AdminGallery = () => {
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<p className="text-sm text-brand-purple mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
You can select multiple images. Max {formatFileSize(maxFileSize)} per image.
|
||||
</p>
|
||||
</div>
|
||||
@@ -246,12 +246,12 @@ const AdminGallery = () => {
|
||||
|
||||
{/* Gallery Grid */}
|
||||
{selectedEvent && (
|
||||
<Card className="p-6 bg-background border-[var(--neutral-800)] rounded-xl">
|
||||
<Card className="p-6 bg-background border-chart-6 rounded-xl">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h2 className="text-xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Gallery Images
|
||||
</h2>
|
||||
<Badge variant="purple" className=" px-3 py-1">
|
||||
<Badge className="bg-muted-foreground text-white px-3 py-1">
|
||||
{galleryImages.length} {galleryImages.length === 1 ? 'image' : 'images'}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -260,7 +260,7 @@ const AdminGallery = () => {
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{galleryImages.map((image) => (
|
||||
<div key={image.id} className="relative group">
|
||||
<div className="aspect-square rounded-xl overflow-hidden bg-[var(--lavender-500)]">
|
||||
<div className="aspect-square rounded-xl overflow-hidden bg-chart-7">
|
||||
<img
|
||||
src={image.image_url}
|
||||
alt={image.caption || 'Gallery image'}
|
||||
@@ -275,7 +275,7 @@ const AdminGallery = () => {
|
||||
<Button
|
||||
onClick={() => openEditCaption(image)}
|
||||
size="sm"
|
||||
className="bg-background/90 hover:bg-background text-[var(--purple-ink)] dark:text-[#ddd8eb] rounded-lg"
|
||||
className="bg-background/90 hover:bg-background text-primary rounded-lg"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-1" />
|
||||
@@ -299,7 +299,7 @@ const AdminGallery = () => {
|
||||
{/* Caption Preview */}
|
||||
{image.caption && (
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-brand-purple line-clamp-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground line-clamp-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{image.caption}
|
||||
</p>
|
||||
</div>
|
||||
@@ -307,7 +307,7 @@ const AdminGallery = () => {
|
||||
|
||||
{/* File Size */}
|
||||
<div className="mt-1">
|
||||
<p className="text-xs text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-xs text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{formatFileSize(image.file_size_bytes)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -316,11 +316,11 @@ const AdminGallery = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-16">
|
||||
<ImageIcon className="h-16 w-16 text-[var(--neutral-800)] mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<ImageIcon className="h-16 w-16 text-chart-6 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-primary mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
No Images Yet
|
||||
</h3>
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Upload images to create a gallery for this event.
|
||||
</p>
|
||||
</div>
|
||||
@@ -332,14 +332,14 @@ const AdminGallery = () => {
|
||||
<Dialog open={!!editingCaption} onOpenChange={() => setEditingCaption(null)}>
|
||||
<DialogContent className="bg-background sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<DialogTitle className="text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Edit Image Caption
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{editingCaption && (
|
||||
<div className="space-y-4">
|
||||
<div className="aspect-video rounded-xl overflow-hidden bg-[var(--lavender-500)]">
|
||||
<div className="aspect-video rounded-xl overflow-hidden bg-chart-7">
|
||||
<img
|
||||
src={editingCaption.image_url}
|
||||
alt="Preview"
|
||||
@@ -348,7 +348,7 @@ const AdminGallery = () => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Label className="text-primary" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Caption
|
||||
</Label>
|
||||
<Textarea
|
||||
@@ -356,7 +356,7 @@ const AdminGallery = () => {
|
||||
onChange={(e) => setNewCaption(e.target.value)}
|
||||
placeholder="Add a caption for this image..."
|
||||
rows={3}
|
||||
className="border-[var(--neutral-800)] rounded-xl mt-2"
|
||||
className="border-chart-6 rounded-xl mt-2"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
/>
|
||||
</div>
|
||||
@@ -367,14 +367,14 @@ const AdminGallery = () => {
|
||||
<Button
|
||||
onClick={() => setEditingCaption(null)}
|
||||
variant="outline"
|
||||
className="border-[var(--neutral-800)] text-brand-purple rounded-xl"
|
||||
className="border-chart-6 text-muted-foreground rounded-xl"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleUpdateCaption}
|
||||
className="bg-brand-purple hover:bg-[var(--purple-ink)] text-white rounded-xl"
|
||||
className="bg-muted-foreground hover:bg-primary text-white rounded-xl"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
Save Caption
|
||||
|
||||
@@ -14,13 +14,12 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '../../components/ui/dropdown-menu';
|
||||
import { toast } from 'sonner';
|
||||
import { Users, Search, User, CreditCard, Eye, CheckCircle, Calendar, AlertCircle, Clock, Mail, UserPlus, Upload, Download, FileDown, ChevronDown, CircleMinus } from 'lucide-react';
|
||||
import { Users, Search, User, CreditCard, Eye, CheckCircle, Calendar, AlertCircle, Clock, Mail, UserPlus, Upload, Download, FileDown, ChevronDown } from 'lucide-react';
|
||||
import PaymentActivationDialog from '../../components/PaymentActivationDialog';
|
||||
import ConfirmationDialog from '../../components/ConfirmationDialog';
|
||||
import CreateMemberDialog from '../../components/CreateMemberDialog';
|
||||
import InviteStaffDialog from '../../components/InviteStaffDialog';
|
||||
import WordPressImportWizard from '../../components/WordPressImportWizard';
|
||||
import { StatCard } from '@/components/StatCard';
|
||||
|
||||
const AdminMembers = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -202,20 +201,20 @@ const AdminMembers = () => {
|
||||
|
||||
const getStatusBadge = (status) => {
|
||||
const config = {
|
||||
pending_email: { label: 'Pending Email', variant: 'orange2' },
|
||||
pending_validation: { label: 'Pending Validation', variant: 'gray' },
|
||||
pre_validated: { label: 'Pre-Validated', variant: 'green' },
|
||||
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' }
|
||||
pending_email: { label: 'Pending Email', className: 'bg-orange-100 text-orange-700' },
|
||||
pending_validation: { label: 'Pending Validation', className: 'bg-gray-200 text-gray-700' },
|
||||
pre_validated: { label: 'Pre-Validated', className: 'bg-success text-white' },
|
||||
payment_pending: { label: 'Payment Pending', className: 'bg-orange-500 text-white' },
|
||||
active: { label: 'Active', className: 'bg-success text-white' },
|
||||
inactive: { label: 'Inactive', className: 'bg-gray-400 text-white' },
|
||||
canceled: { label: 'Canceled', className: 'bg-red-100 text-red-700' },
|
||||
expired: { label: 'Expired', className: 'bg-red-500 text-white' },
|
||||
abandoned: { label: 'Abandoned', className: 'bg-gray-300 text-gray-600' }
|
||||
};
|
||||
|
||||
const statusConfig = config[status] || config.inactive;
|
||||
return (
|
||||
<Badge variant={statusConfig.variant} className={` px-3 py-1 rounded-full text-sm`}>
|
||||
<Badge className={`${statusConfig.className} px-3 py-1 rounded-full text-sm`}>
|
||||
{statusConfig.label}
|
||||
</Badge>
|
||||
);
|
||||
@@ -246,10 +245,10 @@ const AdminMembers = () => {
|
||||
<div className="mb-8">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Members Management
|
||||
</h1>
|
||||
<p className="text-lg text-brand-purple dark:text-brand-lavender" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Manage paying members and their subscriptions.
|
||||
</p>
|
||||
</div>
|
||||
@@ -258,7 +257,7 @@ const AdminMembers = () => {
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className="btn-util-purple "
|
||||
className="bg-muted-foreground hover:bg-primary text-white rounded-xl h-12 px-6"
|
||||
disabled={exporting}
|
||||
>
|
||||
{exporting ? (
|
||||
@@ -289,7 +288,7 @@ const AdminMembers = () => {
|
||||
{hasPermission('users.import') && (
|
||||
<Button
|
||||
onClick={() => setImportDialogOpen(true)}
|
||||
className="btn-util-green "
|
||||
className="bg-success hover:bg-muted-teal-balanced text-white rounded-xl h-12 px-6"
|
||||
>
|
||||
<Upload className="h-5 w-5 mr-2" />
|
||||
Import
|
||||
@@ -299,7 +298,7 @@ const AdminMembers = () => {
|
||||
{hasPermission('users.invite') && (
|
||||
<Button
|
||||
onClick={() => setInviteDialogOpen(true)}
|
||||
className="btn-util-purple "
|
||||
className="bg-muted-foreground hover:bg-primary text-white rounded-xl h-12 px-6"
|
||||
>
|
||||
<Mail className="h-5 w-5 mr-2" />
|
||||
Invite Member
|
||||
@@ -309,7 +308,7 @@ const AdminMembers = () => {
|
||||
{hasPermission('users.create') && (
|
||||
<Button
|
||||
onClick={() => setCreateDialogOpen(true)}
|
||||
className="btn-util-green "
|
||||
className="bg-success hover:bg-muted-teal-balanced text-white rounded-xl h-12 px-6"
|
||||
>
|
||||
<UserPlus className="h-5 w-5 mr-2" />
|
||||
Create Member
|
||||
@@ -320,62 +319,48 @@ const AdminMembers = () => {
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className='rounded-3xl bg-brand-lavender/10 p-8 mb-8'>
|
||||
<div className=' text-2xl text-[var(--purple-ink)] pb-8 font-semibold'>
|
||||
Quick Overview
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
|
||||
|
||||
|
||||
<StatCard
|
||||
title="Total Members"
|
||||
value={users.length}
|
||||
icon={Users}
|
||||
iconBgClass="bg-[var(--blue-light)] text-[var(--blue-dark)]"
|
||||
dataTestId="stat-total-members"
|
||||
/>
|
||||
<StatCard
|
||||
title="Active"
|
||||
value={users.filter(u => u.status === 'active').length}
|
||||
icon={CheckCircle}
|
||||
iconBgClass="text-[var(--green-light)]"
|
||||
dataTestId="stat-active-members"
|
||||
/>
|
||||
<StatCard
|
||||
title="Payment Pending"
|
||||
value={users.filter(u => u.status === 'payment_pending').length}
|
||||
icon={CreditCard}
|
||||
iconBgClass="text-brand-light-orange"
|
||||
dataTestId="stat-payment-pending-members"
|
||||
/>
|
||||
<StatCard
|
||||
title="Inactive"
|
||||
value={users.filter(u => u.status === 'inactive').length}
|
||||
icon={CircleMinus}
|
||||
iconBgClass=" text-brand-pink"
|
||||
dataTestId="stat-inactive-members"
|
||||
/>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6">
|
||||
<p className="text-sm text-muted-foreground mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total Members</p>
|
||||
<p className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{users.length}
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6">
|
||||
<p className="text-sm text-muted-foreground mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Active</p>
|
||||
<p className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{users.filter(u => u.status === 'active').length}
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6">
|
||||
<p className="text-sm text-muted-foreground mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Payment Pending</p>
|
||||
<p className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{users.filter(u => u.status === 'payment_pending').length}
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6">
|
||||
<p className="text-sm text-muted-foreground mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Inactive</p>
|
||||
<p className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{users.filter(u => u.status === 'inactive').length}
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] mb-8">
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6 mb-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-brand-purple " />
|
||||
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search by name or email..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-12 h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="pl-12 h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
data-testid="search-members-input"
|
||||
/>
|
||||
</div>
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="h-14 rounded-xl border-2 border-[var(--neutral-800)]" data-testid="status-filter-select">
|
||||
<SelectTrigger className="h-14 rounded-xl border-2 border-chart-6" data-testid="status-filter-select">
|
||||
<SelectValue placeholder="Filter by status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -396,37 +381,35 @@ const AdminMembers = () => {
|
||||
{/* Members List */}
|
||||
{loading ? (
|
||||
<div className="text-center py-20">
|
||||
<p className="text-brand-purple dark:text-brand-lavender " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading members...</p>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading members...</p>
|
||||
</div>
|
||||
) : filteredUsers.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{filteredUsers.map((user) => {
|
||||
const joinedDate = user.member_since || user.created_at;
|
||||
return (
|
||||
{filteredUsers.map((user) => (
|
||||
<Card
|
||||
key={user.id}
|
||||
className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-md transition-shadow"
|
||||
className="p-6 bg-background rounded-2xl border border-chart-6 hover:shadow-md transition-shadow"
|
||||
data-testid={`member-card-${user.id}`}
|
||||
>
|
||||
<div className="flex justify-between items-start flex-wrap gap-4">
|
||||
<div className="flex items-start gap-4 flex-1">
|
||||
{/* Avatar */}
|
||||
<div className="h-14 w-14 rounded-full bg-[var(--neutral-800)] flex items-center justify-center text-[var(--purple-ink)] font-semibold text-lg flex-shrink-0">
|
||||
<div className="h-14 w-14 rounded-full bg-chart-6 flex items-center justify-center text-primary font-semibold text-lg flex-shrink-0">
|
||||
{user.first_name?.[0]}{user.last_name?.[0]}
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 mb-2 flex-wrap">
|
||||
<h3 className="text-xl font-semibold text-[var(--purple-ink)] " style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{user.first_name} {user.last_name}
|
||||
</h3>
|
||||
{getStatusBadge(user.status)}
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 gap-2 text-sm text-brand-purple dark:text-brand-lavender " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="grid md:grid-cols-2 gap-2 text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p>Email: {user.email}</p>
|
||||
<p>Phone: {user.phone}</p>
|
||||
<p>Joined: {joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}</p>
|
||||
<p>Joined: {new Date(user.created_at).toLocaleDateString()}</p>
|
||||
{user.referred_by_member_name && (
|
||||
<p>Referred by: {user.referred_by_member_name}</p>
|
||||
)}
|
||||
@@ -437,19 +420,19 @@ const AdminMembers = () => {
|
||||
const reminderInfo = getReminderInfo(user);
|
||||
if (reminderInfo.totalReminders > 0) {
|
||||
return (
|
||||
<div className="mt-4 p-3 bg-[var(--lavender-500)] rounded-lg border border-[var(--neutral-800)]">
|
||||
<div className="mt-4 p-3 bg-chart-7 rounded-lg border border-chart-6">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<AlertCircle className="h-4 w-4 text-[var(--orange-light)]" />
|
||||
<span className="text-sm font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<AlertCircle className="h-4 w-4 text-accent" />
|
||||
<span className="text-sm font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{reminderInfo.totalReminders} reminder{reminderInfo.totalReminders !== 1 ? 's' : ''} sent
|
||||
{reminderInfo.totalReminders >= 3 && (
|
||||
<Badge className="ml-2 bg-[var(--orange-light)] text-white px-2 py-0.5 rounded-full text-xs">
|
||||
<Badge className="ml-2 bg-accent text-white px-2 py-0.5 rounded-full text-xs">
|
||||
Needs attention
|
||||
</Badge>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-xs text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-xs text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{reminderInfo.emailReminders > 0 && (
|
||||
<p>
|
||||
<Mail className="inline h-3 w-3 mr-1" />
|
||||
@@ -476,7 +459,7 @@ const AdminMembers = () => {
|
||||
)}
|
||||
</div>
|
||||
{reminderInfo.lastReminderAt && (
|
||||
<p className="mt-2 text-xs text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="mt-2 text-xs text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Last reminder: {new Date(reminderInfo.lastReminderAt).toLocaleDateString()} at {new Date(reminderInfo.lastReminderAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</p>
|
||||
)}
|
||||
@@ -495,7 +478,7 @@ const AdminMembers = () => {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className=""
|
||||
className="border-muted-foreground text-muted-foreground hover:bg-muted-foreground hover:text-white"
|
||||
>
|
||||
<Eye className="h-4 w-4 mr-1" />
|
||||
View Profile
|
||||
@@ -507,7 +490,7 @@ const AdminMembers = () => {
|
||||
<Button
|
||||
onClick={() => handleActivatePayment(user)}
|
||||
size="sm"
|
||||
className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background"
|
||||
className="bg-chart-6 text-primary hover:bg-background"
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-1" />
|
||||
Activate Payment
|
||||
@@ -517,7 +500,7 @@ const AdminMembers = () => {
|
||||
|
||||
{/* Status Management */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-brand-purple dark:text-brand-lavender whitespace-nowrap" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<span className="text-sm text-muted-foreground whitespace-nowrap" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Change Status:
|
||||
</span>
|
||||
<Select
|
||||
@@ -525,7 +508,7 @@ const AdminMembers = () => {
|
||||
onValueChange={(newStatus) => handleStatusChangeRequest(user.id, user.status, newStatus, user)}
|
||||
disabled={statusChanging === user.id}
|
||||
>
|
||||
<SelectTrigger className="w-[180px] h-9 border-[var(--neutral-800)]">
|
||||
<SelectTrigger className="w-[180px] h-9 border-chart-6">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -540,16 +523,15 @@ const AdminMembers = () => {
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-20">
|
||||
<Users className="h-20 w-20 text-[var(--neutral-800)] mx-auto mb-6" />
|
||||
<h3 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Users className="h-20 w-20 text-chart-6 mx-auto mb-6" />
|
||||
<h3 className="text-2xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
No Members Found
|
||||
</h3>
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{searchQuery || statusFilter !== 'all'
|
||||
? 'Try adjusting your filters'
|
||||
: 'No members yet'}
|
||||
|
||||
@@ -175,7 +175,7 @@ const AdminNewsletters = () => {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<p className="text-brand-purple ">Loading newsletters...</p>
|
||||
<p className="text-muted-foreground">Loading newsletters...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -185,17 +185,17 @@ const AdminNewsletters = () => {
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Newsletter Management
|
||||
</h1>
|
||||
<p className="text-brand-purple mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Create and manage newsletter archive
|
||||
</p>
|
||||
</div>
|
||||
{hasPermission('newsletters.create') && (
|
||||
<Button
|
||||
onClick={handleCreate}
|
||||
className="btn-light-lavender flex items-center gap-2"
|
||||
className="bg-muted-foreground text-white hover:bg-[#533a82] rounded-full flex items-center gap-2"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Add Newsletter
|
||||
@@ -206,10 +206,10 @@ const AdminNewsletters = () => {
|
||||
{/* Newsletters List */}
|
||||
{newsletters.length === 0 ? (
|
||||
<Card className="p-12 text-center">
|
||||
<FileText className="h-16 w-16 text-[var(--neutral-800)] mx-auto mb-4" />
|
||||
<p className="text-brand-purple text-lg mb-4">No newsletters yet</p>
|
||||
<FileText className="h-16 w-16 text-chart-6 mx-auto mb-4" />
|
||||
<p className="text-muted-foreground text-lg mb-4">No newsletters yet</p>
|
||||
{hasPermission('newsletters.create') && (
|
||||
<Button onClick={handleCreate} className="bg-brand-purple text-white">
|
||||
<Button onClick={handleCreate} className="bg-muted-foreground text-white">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Create First Newsletter
|
||||
</Button>
|
||||
@@ -219,7 +219,7 @@ const AdminNewsletters = () => {
|
||||
<div className="space-y-6">
|
||||
{sortedYears.map(year => (
|
||||
<div key={year}>
|
||||
<h2 className="text-xl font-semibold text-[var(--purple-ink)] mb-4 flex items-center gap-2">
|
||||
<h2 className="text-xl font-semibold text-primary mb-4 flex items-center gap-2">
|
||||
<Calendar className="h-5 w-5" />
|
||||
{year}
|
||||
</h2>
|
||||
@@ -228,24 +228,24 @@ const AdminNewsletters = () => {
|
||||
<Card key={newsletter.id} className="p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2">
|
||||
<h3 className="text-lg font-semibold text-primary mb-2">
|
||||
{newsletter.title}
|
||||
</h3>
|
||||
{newsletter.description && (
|
||||
<p className="text-brand-purple mb-3">{newsletter.description}</p>
|
||||
<p className="text-muted-foreground mb-3">{newsletter.description}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge className="bg-[var(--neutral-800)] text-[var(--purple-ink)]">
|
||||
<Badge className="bg-chart-6 text-primary">
|
||||
{formatDate(newsletter.published_date)}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="border-brand-purple text-brand-purple ">
|
||||
<Badge variant="outline" className="border-muted-foreground text-muted-foreground">
|
||||
{newsletter.document_type === 'upload' ? 'PDF Upload' : 'Link'}
|
||||
</Badge>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => window.open(newsletter.document_url, '_blank')}
|
||||
className="text-brand-purple hover:text-[var(--purple-muted)]"
|
||||
className="text-muted-foreground hover:text-[#533a82]"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4 mr-1" />
|
||||
View
|
||||
@@ -259,17 +259,17 @@ const AdminNewsletters = () => {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(newsletter)}
|
||||
className="border-brand-purple text-brand-purple "
|
||||
className="border-muted-foreground text-muted-foreground"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
{hasPermission('newsletters.delete') && (
|
||||
<Button
|
||||
variant="outline-destructive"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(newsletter)}
|
||||
className=""
|
||||
className="border-red-500 text-red-500 hover:bg-red-50"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -361,12 +361,12 @@ const AdminNewsletters = () => {
|
||||
required={!selectedNewsletter}
|
||||
/>
|
||||
{uploadedFile && (
|
||||
<p className="text-sm text-brand-purple mt-1">
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Selected: {uploadedFile.name}
|
||||
</p>
|
||||
)}
|
||||
{selectedNewsletter && !uploadedFile && (
|
||||
<p className="text-sm text-brand-purple mt-1">
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Current file will be kept if no new file is selected
|
||||
</p>
|
||||
)}
|
||||
@@ -381,7 +381,7 @@ const AdminNewsletters = () => {
|
||||
placeholder="https://docs.google.com/document/d/... or https://example.com/file.pdf"
|
||||
required
|
||||
/>
|
||||
<p className="text-sm text-brand-purple mt-1">
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Paste the shareable link to your document (Google Docs, Dropbox, PDF URL, etc.)
|
||||
</p>
|
||||
</div>
|
||||
@@ -398,7 +398,7 @@ const AdminNewsletters = () => {
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-brand-purple text-white"
|
||||
className="bg-muted-foreground text-white"
|
||||
disabled={submitting}
|
||||
>
|
||||
{submitting ? 'Saving...' : selectedNewsletter ? 'Update' : 'Create'}
|
||||
|
||||
@@ -187,8 +187,8 @@ const AdminPermissions = () => {
|
||||
|
||||
const getRoleBadge = (role) => {
|
||||
const config = {
|
||||
admin: { label: 'Admin', color: 'bg-[var(--green-light)]', icon: Shield },
|
||||
member: { label: 'Member', color: 'bg-brand-purple ', icon: Shield },
|
||||
admin: { label: 'Admin', color: 'bg-success', icon: Shield },
|
||||
member: { label: 'Member', color: 'bg-muted-foreground', icon: Shield },
|
||||
guest: { label: 'Guest', color: 'bg-gray-400', icon: Shield }
|
||||
};
|
||||
|
||||
@@ -206,7 +206,7 @@ const AdminPermissions = () => {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-center py-20">
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Loading permissions...
|
||||
</p>
|
||||
</div>
|
||||
@@ -217,10 +217,10 @@ const AdminPermissions = () => {
|
||||
return (
|
||||
<div className="text-center py-20">
|
||||
<Lock className="h-20 w-20 text-red-500 mx-auto mb-6" />
|
||||
<h2 className="text-3xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h2 className="text-3xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Access Denied
|
||||
</h2>
|
||||
<p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
You don't have permission to manage role permissions.
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 mt-2">
|
||||
@@ -233,10 +233,10 @@ const AdminPermissions = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Permission Management
|
||||
</h1>
|
||||
<p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Configure granular permissions for each role. Superadmin always has all permissions.
|
||||
</p>
|
||||
</div>
|
||||
@@ -259,27 +259,27 @@ const AdminPermissions = () => {
|
||||
<TabsContent key={role} value={role}>
|
||||
{/* Stats */}
|
||||
<div className="grid md:grid-cols-3 gap-4 mb-8">
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<p className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6">
|
||||
<p className="text-sm text-muted-foreground mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Total Permissions
|
||||
</p>
|
||||
<p className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{permissions.length}
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<p className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6">
|
||||
<p className="text-sm text-muted-foreground mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Assigned
|
||||
</p>
|
||||
<p className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{selectedPermissions[role].length}
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<p className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6">
|
||||
<p className="text-sm text-muted-foreground mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Modules
|
||||
</p>
|
||||
<p className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{Object.keys(groupedPermissions).length}
|
||||
</p>
|
||||
</Card>
|
||||
@@ -288,10 +288,10 @@ const AdminPermissions = () => {
|
||||
{/* Permissions by Module */}
|
||||
<div className="space-y-4">
|
||||
{Object.entries(groupedPermissions).map(([module, perms]) => (
|
||||
<Card key={module} className="bg-background rounded-2xl border border-[var(--neutral-800)] overflow-hidden">
|
||||
<Card key={module} className="bg-background rounded-2xl border border-chart-6 overflow-hidden">
|
||||
{/* Module Header */}
|
||||
<div
|
||||
className="p-6 bg-gradient-to-r from-[var(--neutral-800)] to-white cursor-pointer hover:from-[var(--neutral-600)] transition-colors"
|
||||
className="p-6 bg-gradient-to-r from-chart-6 to-white cursor-pointer hover:from-[#C5BFD9] transition-colors"
|
||||
onClick={() => toggleModuleExpansion(module)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -300,21 +300,21 @@ const AdminPermissions = () => {
|
||||
checked={isModuleFullySelected(role, module)}
|
||||
onCheckedChange={() => toggleModule(role, module)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="h-6 w-6 border-2 border-brand-purple data-[state=checked]:bg-brand-purple "
|
||||
className="h-6 w-6 border-2 border-muted-foreground data-[state=checked]:bg-muted-foreground"
|
||||
/>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-[var(--purple-ink)] capitalize" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-xl font-semibold text-primary capitalize" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{module}
|
||||
</h3>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{getModuleProgress(role, module)} permissions
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{expandedModules[module] ? (
|
||||
<ChevronUp className="h-6 w-6 text-brand-purple " />
|
||||
<ChevronUp className="h-6 w-6 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronDown className="h-6 w-6 text-brand-purple " />
|
||||
<ChevronDown className="h-6 w-6 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -326,18 +326,18 @@ const AdminPermissions = () => {
|
||||
{perms.map(perm => (
|
||||
<div
|
||||
key={perm.code}
|
||||
className="flex items-start gap-4 p-4 rounded-xl hover:bg-[var(--lavender-700)] transition-colors border border-transparent hover:border-[var(--neutral-800)]"
|
||||
className="flex items-start gap-4 p-4 rounded-xl hover:bg-bright-snow transition-colors border border-transparent hover:border-chart-6"
|
||||
>
|
||||
<Checkbox
|
||||
checked={selectedPermissions[role].includes(perm.code)}
|
||||
onCheckedChange={() => togglePermission(role, perm.code)}
|
||||
className="mt-1 h-5 w-5 border-2 border-brand-purple data-[state=checked]:bg-brand-purple "
|
||||
className="mt-1 h-5 w-5 border-2 border-muted-foreground data-[state=checked]:bg-muted-foreground"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-[var(--purple-ink)] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="font-semibold text-primary mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{perm.name}
|
||||
</p>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{perm.description}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-1 font-mono">
|
||||
@@ -357,7 +357,7 @@ const AdminPermissions = () => {
|
||||
</Tabs>
|
||||
|
||||
{/* Superadmin Note */}
|
||||
<Card className="p-6 bg-gradient-to-r from-brand-purple to-[var(--purple-ink)] rounded-2xl border-none mb-8">
|
||||
<Card className="p-6 bg-gradient-to-r from-muted-foreground to-primary rounded-2xl border-none mb-8">
|
||||
<div className="flex items-start gap-4">
|
||||
<Lock className="h-6 w-6 text-white flex-shrink-0 mt-1" />
|
||||
<div className="text-white">
|
||||
@@ -377,7 +377,7 @@ const AdminPermissions = () => {
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="h-14 px-8 bg-[var(--green-light)] hover:bg-[var(--green-fern)] text-white rounded-full shadow-lg text-lg font-semibold"
|
||||
className="h-14 px-8 bg-success hover:bg-muted-teal-balanced text-white rounded-full shadow-lg text-lg font-semibold"
|
||||
>
|
||||
<Save className="h-5 w-5 mr-2" />
|
||||
{saving ? 'Saving...' : 'Save Changes'}
|
||||
@@ -389,10 +389,10 @@ const AdminPermissions = () => {
|
||||
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
||||
<AlertDialogContent className="rounded-2xl">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="text-2xl text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<AlertDialogTitle className="text-2xl text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Confirm Permission Changes
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<AlertDialogDescription className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Are you sure you want to update permissions for <span className="font-semibold capitalize">{selectedRole}</span>?
|
||||
This will immediately affect all users with this role.
|
||||
</AlertDialogDescription>
|
||||
@@ -401,7 +401,7 @@ const AdminPermissions = () => {
|
||||
<AlertDialogCancel className="rounded-xl">Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmSave}
|
||||
className="rounded-xl bg-[var(--green-light)] hover:bg-[var(--green-fern)] text-white"
|
||||
className="rounded-xl bg-success hover:bg-muted-teal-balanced text-white"
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
|
||||
@@ -131,17 +131,17 @@ const AdminPlans = () => {
|
||||
<div className="mb-8">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Subscription Plans
|
||||
</h1>
|
||||
<p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Manage membership plans and pricing.
|
||||
</p>
|
||||
</div>
|
||||
{hasPermission('subscriptions.plans') && (
|
||||
<Button
|
||||
onClick={handleCreatePlan}
|
||||
className="btn-lavender "
|
||||
className="bg-chart-6 text-primary hover:bg-background rounded-full px-6"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Create Plan
|
||||
@@ -152,27 +152,27 @@ const AdminPlans = () => {
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<p className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total Plans</p>
|
||||
<p className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6">
|
||||
<p className="text-sm text-muted-foreground mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total Plans</p>
|
||||
<p className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{plans.length}
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<p className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Active Plans</p>
|
||||
<p className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6">
|
||||
<p className="text-sm text-muted-foreground mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Active Plans</p>
|
||||
<p className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{plans.filter(p => p.active).length}
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<p className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total Subscribers</p>
|
||||
<p className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6">
|
||||
<p className="text-sm text-muted-foreground mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total Subscribers</p>
|
||||
<p className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{plans.reduce((sum, p) => sum + (p.subscriber_count || 0), 0)}
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<p className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Revenue (Annual Est.)</p>
|
||||
<p className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6">
|
||||
<p className="text-sm text-muted-foreground mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Revenue (Annual Est.)</p>
|
||||
<p className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{formatPrice(
|
||||
plans.reduce((sum, p) => {
|
||||
const annualPrice = p.billing_cycle === 'yearly'
|
||||
@@ -186,19 +186,19 @@ const AdminPlans = () => {
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] mb-8">
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6 mb-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-brand-purple " />
|
||||
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search plans..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-12 h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="pl-12 h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
<Select value={activeFilter} onValueChange={setActiveFilter}>
|
||||
<SelectTrigger className="h-14 rounded-xl border-2 border-[var(--neutral-800)]">
|
||||
<SelectTrigger className="h-14 rounded-xl border-2 border-chart-6">
|
||||
<SelectValue placeholder="Filter by status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -213,7 +213,7 @@ const AdminPlans = () => {
|
||||
{/* Plans Grid */}
|
||||
{loading ? (
|
||||
<div className="text-center py-20">
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading plans...</p>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading plans...</p>
|
||||
</div>
|
||||
) : filteredPlans.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
@@ -221,7 +221,7 @@ const AdminPlans = () => {
|
||||
<Card
|
||||
key={plan.id}
|
||||
className={`p-6 bg-background rounded-2xl border-2 transition-all hover:shadow-lg ${plan.active
|
||||
? 'border-[var(--neutral-800)] hover:border-brand-purple '
|
||||
? 'border-chart-6 hover:border-muted-foreground'
|
||||
: 'border-gray-400 opacity-60'
|
||||
}`}
|
||||
>
|
||||
@@ -229,38 +229,38 @@ const AdminPlans = () => {
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
<Badge
|
||||
className={`${plan.active
|
||||
? 'bg-[var(--green-light)] text-white'
|
||||
? 'bg-success text-white'
|
||||
: 'bg-gray-400 text-white'
|
||||
}`}
|
||||
>
|
||||
{plan.active ? 'Active' : 'Inactive'}
|
||||
</Badge>
|
||||
{plan.subscriber_count > 0 && (
|
||||
<Badge className="bg-[var(--neutral-800)] text-[var(--purple-ink)]">
|
||||
<Badge className="bg-chart-6 text-primary">
|
||||
<Users className="h-3 w-3 mr-1" />
|
||||
{plan.subscriber_count}
|
||||
</Badge>
|
||||
)}
|
||||
{plan.custom_cycle_enabled && (
|
||||
<Badge className="bg-brand-purple text-white">
|
||||
<Badge className="bg-muted-foreground text-white">
|
||||
Custom Dates
|
||||
</Badge>
|
||||
)}
|
||||
{plan.allow_donation && (
|
||||
<Badge className="bg-[var(--orange-light)] text-white">
|
||||
<Badge className="bg-accent text-white">
|
||||
Donations Enabled
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Plan Name */}
|
||||
<h3 className="text-2xl font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-2xl font-semibold text-primary mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{plan.name}
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
{plan.description && (
|
||||
<p className="text-sm text-brand-purple mb-4 line-clamp-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground mb-4 line-clamp-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{plan.description}
|
||||
</p>
|
||||
)}
|
||||
@@ -268,20 +268,20 @@ const AdminPlans = () => {
|
||||
{/* Price */}
|
||||
<div className="mb-4">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<div className="text-3xl font-bold text-[var(--orange-light)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<div className="text-3xl font-bold text-accent" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{formatPrice(plan.minimum_price_cents || plan.price_cents)}
|
||||
</div>
|
||||
{plan.suggested_price_cents && plan.suggested_price_cents > plan.minimum_price_cents && (
|
||||
<div className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="text-lg text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
(Suggested: {formatPrice(plan.suggested_price_cents)})
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{getBillingCycleLabel(plan.billing_cycle)}
|
||||
</p>
|
||||
{plan.custom_cycle_enabled && (
|
||||
<p className="text-xs text-brand-purple font-mono mt-1">
|
||||
<p className="text-xs text-muted-foreground font-mono mt-1">
|
||||
{formatCustomCycleDates(plan)}
|
||||
</p>
|
||||
)}
|
||||
@@ -289,12 +289,12 @@ const AdminPlans = () => {
|
||||
|
||||
{/* Actions */}
|
||||
{hasPermission('subscriptions.plans') && (
|
||||
<div className="flex flex-col sm:flex-row gap-2 pt-4 border-t border-[var(--neutral-800)]">
|
||||
<div className="flex flex-col sm:flex-row gap-2 pt-4 border-t border-chart-6">
|
||||
<Button
|
||||
onClick={() => handleEditPlan(plan)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 border-brand-purple text-brand-purple hover:bg-brand-purple hover:text-white rounded-full dark:hover:text-background"
|
||||
className="flex-1 border-muted-foreground text-muted-foreground hover:bg-muted-foreground hover:text-white rounded-full"
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-1" />
|
||||
Edit
|
||||
@@ -303,7 +303,7 @@ const AdminPlans = () => {
|
||||
onClick={() => handleDeleteClick(plan)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 border-red-500 text-red-500 hover:bg-red-500 dark:hover:bg-red-500/10 hover:text-white rounded-full"
|
||||
className="flex-1 border-red-500 text-red-500 hover:bg-red-500 hover:text-white rounded-full"
|
||||
disabled={plan.subscriber_count > 0}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-1" />
|
||||
@@ -314,7 +314,7 @@ const AdminPlans = () => {
|
||||
|
||||
{/* Warning for plans with subscribers */}
|
||||
{plan.subscriber_count > 0 && (
|
||||
<p className="text-xs text-brand-purple mt-2 text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-xs text-muted-foreground mt-2 text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Cannot delete plan with active subscribers
|
||||
</p>
|
||||
)}
|
||||
@@ -323,11 +323,11 @@ const AdminPlans = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-20">
|
||||
<CreditCard className="h-20 w-20 text-[var(--neutral-800)] mx-auto mb-6" />
|
||||
<h3 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<CreditCard className="h-20 w-20 text-chart-6 mx-auto mb-6" />
|
||||
<h3 className="text-2xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
No Plans Found
|
||||
</h3>
|
||||
<p className="text-brand-purple mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{searchQuery || activeFilter !== 'all'
|
||||
? 'Try adjusting your filters'
|
||||
: 'Create your first subscription plan to get started'}
|
||||
@@ -335,7 +335,7 @@ const AdminPlans = () => {
|
||||
{!searchQuery && activeFilter === 'all' && hasPermission('subscriptions.plans') && (
|
||||
<Button
|
||||
onClick={handleCreatePlan}
|
||||
className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background rounded-full px-8"
|
||||
className="bg-chart-6 text-primary hover:bg-background rounded-full px-8"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Create First Plan
|
||||
@@ -356,10 +356,10 @@ const AdminPlans = () => {
|
||||
{deleteDialogOpen && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<Card className="p-6 sm:p-8 bg-background rounded-2xl max-w-md w-full">
|
||||
<h2 className="text-xl sm:text-2xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h2 className="text-xl sm:text-2xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Delete Plan
|
||||
</h2>
|
||||
<p className="text-sm sm:text-base text-brand-purple mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm sm:text-base text-muted-foreground mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Are you sure you want to delete "{planToDelete?.name}"? This action
|
||||
will deactivate the plan and it won't be available for new subscriptions.
|
||||
</p>
|
||||
|
||||
@@ -188,12 +188,12 @@ const AdminRoles = () => {
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-[var(--purple-ink)]">Role Management</h1>
|
||||
<h1 className="text-3xl font-bold">Role Management</h1>
|
||||
<p className="text-gray-600 mt-1">
|
||||
Create and manage custom roles with specific permissions
|
||||
</p>
|
||||
</div>
|
||||
<Button className="btn-lavender " onClick={() => setShowCreateModal(true)}>
|
||||
<Button onClick={() => setShowCreateModal(true)}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Create Role
|
||||
</Button>
|
||||
@@ -273,7 +273,7 @@ const AdminRoles = () => {
|
||||
|
||||
{/* Create Role Modal */}
|
||||
<Dialog open={showCreateModal} onOpenChange={setShowCreateModal}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto scrollbar-dashboard scrollbar-dashboard">
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Role</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -317,7 +317,7 @@ const AdminRoles = () => {
|
||||
<p className="text-sm text-gray-600 mb-3">
|
||||
Select permissions for this role. You can also add permissions later.
|
||||
</p>
|
||||
<div className="border rounded-lg p-4 max-h-64 overflow-y-auto scrollbar-dashboard">
|
||||
<div className="border rounded-lg p-4 max-h-64 overflow-y-auto">
|
||||
{Object.entries(groupedPermissions).map(([module, perms]) => (
|
||||
<div key={module} className="mb-4">
|
||||
<button
|
||||
@@ -417,7 +417,7 @@ const AdminRoles = () => {
|
||||
|
||||
{/* Manage Permissions Modal */}
|
||||
<Dialog open={showPermissionsModal} onOpenChange={setShowPermissionsModal}>
|
||||
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto scrollbar-dashboard">
|
||||
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Manage Permissions: {selectedRole?.name}</DialogTitle>
|
||||
<DialogDescription>
|
||||
|
||||
@@ -1,506 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../components/ui/card';
|
||||
import { Button } from '../../components/ui/button';
|
||||
import { Input } from '../../components/ui/input';
|
||||
import { Label } from '../../components/ui/label';
|
||||
import { AlertCircle, CheckCircle, Settings as SettingsIcon, RefreshCw, Zap, Edit, Save, X, Copy, Eye, EyeOff, ExternalLink } from 'lucide-react';
|
||||
import api from '../../utils/api';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function AdminSettings() {
|
||||
const [stripeStatus, setStripeStatus] = useState(null);
|
||||
const [loadingStatus, setLoadingStatus] = useState(true);
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// Form state
|
||||
const [formData, setFormData] = useState({
|
||||
secret_key: '',
|
||||
webhook_secret: ''
|
||||
});
|
||||
|
||||
// Show/hide sensitive values
|
||||
const [showSecretKey, setShowSecretKey] = useState(false);
|
||||
const [showWebhookSecret, setShowWebhookSecret] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStripeStatus();
|
||||
}, []);
|
||||
|
||||
const fetchStripeStatus = async () => {
|
||||
setLoadingStatus(true);
|
||||
try {
|
||||
const response = await api.get('/admin/settings/stripe/status');
|
||||
setStripeStatus(response.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch Stripe status:', error);
|
||||
toast.error('Failed to load Stripe status');
|
||||
} finally {
|
||||
setLoadingStatus(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
setTesting(true);
|
||||
try {
|
||||
const response = await api.post('/admin/settings/stripe/test-connection');
|
||||
toast.success(response.data.message);
|
||||
} catch (error) {
|
||||
const message = error.response?.data?.detail || 'Connection test failed';
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditClick = () => {
|
||||
setIsEditing(true);
|
||||
setFormData({
|
||||
secret_key: '',
|
||||
webhook_secret: ''
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setIsEditing(false);
|
||||
setFormData({
|
||||
secret_key: '',
|
||||
webhook_secret: ''
|
||||
});
|
||||
setShowSecretKey(false);
|
||||
setShowWebhookSecret(false);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
// Validate inputs
|
||||
if (!formData.secret_key || !formData.webhook_secret) {
|
||||
toast.error('Both Secret Key and Webhook Secret are required');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.secret_key.startsWith('sk_test_') && !formData.secret_key.startsWith('sk_live_')) {
|
||||
toast.error('Invalid Secret Key format. Must start with sk_test_ or sk_live_');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.webhook_secret.startsWith('whsec_')) {
|
||||
toast.error('Invalid Webhook Secret format. Must start with whsec_');
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
await api.put('/admin/settings/stripe', formData);
|
||||
toast.success('Stripe settings updated successfully');
|
||||
setIsEditing(false);
|
||||
setFormData({
|
||||
secret_key: '',
|
||||
webhook_secret: ''
|
||||
});
|
||||
setShowSecretKey(false);
|
||||
setShowWebhookSecret(false);
|
||||
// Refresh status
|
||||
await fetchStripeStatus();
|
||||
} catch (error) {
|
||||
const message = error.response?.data?.detail || 'Failed to update Stripe settings';
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = (text, label) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
toast.success(`${label} copied to clipboard`);
|
||||
};
|
||||
|
||||
if (loadingStatus) {
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<RefreshCw className="h-8 w-8 animate-spin text-brand-purple" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6 max-w-4xl">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 flex items-center gap-3">
|
||||
<SettingsIcon className="h-8 w-8 text-brand-purple" />
|
||||
Settings
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Manage system configuration and integrations
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stripe Integration Card */}
|
||||
<Card className="border-2 border-[var(--lavender-200)] shadow-sm">
|
||||
<CardHeader className="bg-gradient-to-r from-[var(--lavender-100)] to-white border-b-2 border-[var(--lavender-200)]">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Zap className="h-5 w-5 text-brand-purple" />
|
||||
Stripe Integration
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Payment processing and subscription management
|
||||
</CardDescription>
|
||||
</div>
|
||||
{!isEditing && (
|
||||
<Button
|
||||
onClick={handleEditClick}
|
||||
variant="outline"
|
||||
className="border-2 border-brand-purple text-brand-purple hover:bg-[#f1eef9] rounded-full"
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
Edit Settings
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="pt-6 space-y-6">
|
||||
{isEditing ? (
|
||||
/* Edit Mode */
|
||||
<div className="space-y-6">
|
||||
{/* Secret Key Input */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="secret_key">Stripe Secret Key</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="secret_key"
|
||||
type={showSecretKey ? 'text' : 'password'}
|
||||
value={formData.secret_key}
|
||||
onChange={(e) => setFormData({ ...formData, secret_key: e.target.value })}
|
||||
placeholder="sk_test_... or sk_live_..."
|
||||
className="pr-10"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowSecretKey(!showSecretKey)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
{showSecretKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
Get this from your Stripe Dashboard → Developers → API keys
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Webhook Secret Input */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="webhook_secret">Stripe Webhook Secret</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="webhook_secret"
|
||||
type={showWebhookSecret ? 'text' : 'password'}
|
||||
value={formData.webhook_secret}
|
||||
onChange={(e) => setFormData({ ...formData, webhook_secret: e.target.value })}
|
||||
placeholder="whsec_..."
|
||||
className="pr-10"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowWebhookSecret(!showWebhookSecret)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
{showWebhookSecret ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
Get this from your Stripe Dashboard → Developers → Webhooks → Add endpoint
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-end gap-3 pt-4 border-t-2 border-gray-100">
|
||||
<Button
|
||||
onClick={handleCancelEdit}
|
||||
variant="outline"
|
||||
className="border-2 border-gray-300 rounded-full"
|
||||
disabled={saving}
|
||||
>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="bg-brand-purple hover:bg-[var(--purple-dark)] text-white rounded-full"
|
||||
>
|
||||
{saving ? (
|
||||
<>
|
||||
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
Save Settings
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* View Mode */
|
||||
<>
|
||||
{/* Status Display */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">Configuration Status</p>
|
||||
<p className="text-sm text-gray-600">Credentials stored in database (encrypted)</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{stripeStatus?.configured ? (
|
||||
<>
|
||||
<CheckCircle className="h-5 w-5 text-green-600" />
|
||||
<span className="text-green-600 font-semibold">Configured</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<AlertCircle className="h-5 w-5 text-amber-600" />
|
||||
<span className="text-amber-600 font-semibold">Not Configured</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{stripeStatus?.configured && (
|
||||
<>
|
||||
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">Environment</p>
|
||||
<p className="text-sm text-gray-600">Detected from secret key prefix</p>
|
||||
</div>
|
||||
<span className={`px-3 py-1 rounded-full text-sm font-semibold ${
|
||||
stripeStatus.environment === 'live'
|
||||
? 'bg-green-100 text-green-800 border-2 border-green-300'
|
||||
: 'bg-blue-100 text-blue-800 border-2 border-blue-300'
|
||||
}`}>
|
||||
{stripeStatus.environment === 'live' ? 'Live' : 'Test'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">Secret Key</p>
|
||||
<p className="text-sm text-gray-600 font-mono">
|
||||
{stripeStatus.secret_key_prefix}...
|
||||
</p>
|
||||
</div>
|
||||
<CheckCircle className="h-5 w-5 text-green-600" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">Webhook Secret</p>
|
||||
<p className="text-sm text-gray-600">Webhook endpoint configuration</p>
|
||||
</div>
|
||||
{stripeStatus.webhook_secret_set ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="h-5 w-5 text-green-600" />
|
||||
<span className="text-green-600 font-semibold text-sm">Set</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle className="h-5 w-5 text-amber-600" />
|
||||
<span className="text-amber-600 font-semibold text-sm">Not Set</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Webhook URL */}
|
||||
<div className="p-4 bg-blue-50 border-2 border-blue-200 rounded-lg">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-blue-900 mb-2 flex items-center gap-2">
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
Webhook URL
|
||||
</p>
|
||||
<p className="text-sm text-blue-700 mb-2">
|
||||
Configure this webhook endpoint in your Stripe Dashboard:
|
||||
</p>
|
||||
<div className="bg-white p-2 rounded border border-blue-300 font-mono text-sm break-all">
|
||||
{stripeStatus.webhook_url}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => copyToClipboard(stripeStatus.webhook_url, 'Webhook URL')}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-blue-300 text-blue-700 hover:bg-blue-100"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-3 text-xs text-blue-600">
|
||||
<p className="font-semibold mb-1">Webhook Events to Configure:</p>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<p className="font-medium text-blue-700">✅ Required (Configure in Stripe):</p>
|
||||
<ul className="list-disc list-inside ml-2">
|
||||
<li>checkout.session.completed - Handles subscriptions & donations</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="opacity-80">
|
||||
<p className="font-medium text-blue-700">🔔 Automatically Triggered:</p>
|
||||
<ul className="list-disc list-inside ml-2 text-xs">
|
||||
<li>payment_intent.created</li>
|
||||
<li>payment_intent.succeeded</li>
|
||||
<li>charge.succeeded</li>
|
||||
<li>charge.updated</li>
|
||||
</ul>
|
||||
<p className="text-xs italic mt-1">These fire automatically with checkout.session.completed</p>
|
||||
</div>
|
||||
<div className="opacity-70">
|
||||
<p className="font-medium text-blue-700">🔄 Coming Soon (Recurring Subscriptions):</p>
|
||||
<ul className="list-disc list-inside ml-2">
|
||||
<li>invoice.payment_succeeded</li>
|
||||
<li>invoice.payment_failed</li>
|
||||
<li>customer.subscription.updated</li>
|
||||
<li>customer.subscription.deleted</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Configuration Instructions (Not Configured) */}
|
||||
{!stripeStatus?.configured && (
|
||||
<>
|
||||
<div className="p-4 bg-amber-50 border-2 border-amber-200 rounded-lg">
|
||||
<div className="flex items-start gap-2">
|
||||
<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 mb-2">Configuration Required</p>
|
||||
<p className="text-amber-700 mb-2">
|
||||
Click "Edit Settings" above to configure your Stripe credentials.
|
||||
</p>
|
||||
<p className="text-amber-700">
|
||||
Get your API keys from{' '}
|
||||
<a
|
||||
href="https://dashboard.stripe.com/apikeys"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-semibold underline"
|
||||
>
|
||||
Stripe Dashboard
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Webhook URL Info (Always visible) */}
|
||||
<div className="p-4 bg-blue-50 border-2 border-blue-200 rounded-lg">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-blue-900 mb-2 flex items-center gap-2">
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
Webhook URL Configuration
|
||||
</p>
|
||||
<p className="text-sm text-blue-700 mb-2">
|
||||
After configuring your API keys, set up this webhook endpoint in your Stripe Dashboard:
|
||||
</p>
|
||||
<div className="bg-white p-2 rounded border border-blue-300 font-mono text-sm break-all">
|
||||
{stripeStatus?.webhook_url || 'http://localhost:8000/api/webhooks/stripe'}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => copyToClipboard(stripeStatus?.webhook_url || 'http://localhost:8000/api/webhooks/stripe', 'Webhook URL')}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-blue-300 text-blue-700 hover:bg-blue-100"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-3 text-xs text-blue-600">
|
||||
<p className="font-semibold mb-1">Webhook Events to Configure:</p>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<p className="font-medium text-blue-700">✅ Required (Configure in Stripe):</p>
|
||||
<ul className="list-disc list-inside ml-2">
|
||||
<li>checkout.session.completed - Handles subscriptions & donations</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="opacity-80">
|
||||
<p className="font-medium text-blue-700">🔔 Automatically Triggered:</p>
|
||||
<ul className="list-disc list-inside ml-2 text-xs">
|
||||
<li>payment_intent.created</li>
|
||||
<li>payment_intent.succeeded</li>
|
||||
<li>charge.succeeded</li>
|
||||
<li>charge.updated</li>
|
||||
</ul>
|
||||
<p className="text-xs italic mt-1">These fire automatically with checkout.session.completed</p>
|
||||
</div>
|
||||
<div className="opacity-70">
|
||||
<p className="font-medium text-blue-700">🔄 Coming Soon (Recurring Subscriptions):</p>
|
||||
<ul className="list-disc list-inside ml-2">
|
||||
<li>invoice.payment_succeeded</li>
|
||||
<li>invoice.payment_failed</li>
|
||||
<li>customer.subscription.updated</li>
|
||||
<li>customer.subscription.deleted</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Test Connection Button */}
|
||||
{stripeStatus?.configured && (
|
||||
<div className="flex justify-end gap-3 pt-4 border-t-2 border-gray-100">
|
||||
<Button
|
||||
onClick={fetchStripeStatus}
|
||||
variant="outline"
|
||||
className="border-2 border-gray-300 rounded-full"
|
||||
disabled={loadingStatus}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${loadingStatus ? 'animate-spin' : ''}`} />
|
||||
Refresh Status
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleTestConnection}
|
||||
disabled={testing}
|
||||
className="bg-brand-purple hover:bg-[var(--purple-dark)] text-white rounded-full"
|
||||
>
|
||||
{testing ? (
|
||||
<>
|
||||
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
|
||||
Testing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Zap className="h-4 w-4 mr-2" />
|
||||
Test Connection
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Future Settings Sections Placeholder */}
|
||||
<div className="mt-6 p-6 border-2 border-dashed border-gray-300 rounded-lg text-center text-gray-500">
|
||||
<p className="text-sm">Additional settings sections will be added here</p>
|
||||
<p className="text-xs mt-1">(Email, Storage, Notifications, etc.)</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -99,16 +99,16 @@ const AdminStaff = () => {
|
||||
|
||||
const getRoleBadge = (role) => {
|
||||
const config = {
|
||||
superadmin: { label: 'Superadmin', variant: 'purple' },
|
||||
admin: { label: 'Admin', variant: 'green' },
|
||||
moderator: { label: 'Moderator', variant: 'bg-[var(--neutral-800)] text-[var(--purple-ink)]' },
|
||||
staff: { label: 'Staff', variant: 'gray' },
|
||||
media: { label: 'Media', variant: 'gray2' }
|
||||
superadmin: { label: 'Superadmin', className: 'bg-muted-foreground text-white' },
|
||||
admin: { label: 'Admin', className: 'bg-success text-white' },
|
||||
moderator: { label: 'Moderator', className: 'bg-chart-6 text-primary' },
|
||||
staff: { label: 'Staff', className: 'bg-gray-200 text-gray-700' },
|
||||
media: { label: 'Media', className: 'bg-gray-400 text-white' }
|
||||
};
|
||||
|
||||
const roleConfig = config[role] || { label: role, className: 'bg-gray-500 text-white' };
|
||||
return (
|
||||
<Badge variant={roleConfig.variant} className={`${roleConfig.className} px-3 py-1 rounded-full text-sm`}>
|
||||
<Badge className={`${roleConfig.className} px-3 py-1 rounded-full text-sm`}>
|
||||
<Shield className="h-3 w-3 mr-1 inline" />
|
||||
{roleConfig.label}
|
||||
</Badge>
|
||||
@@ -117,13 +117,13 @@ const AdminStaff = () => {
|
||||
|
||||
const getStatusBadge = (status) => {
|
||||
const config = {
|
||||
active: { label: 'Active', variant: 'green' },
|
||||
active: { label: 'Active', className: 'bg-success text-white' },
|
||||
inactive: { label: 'Inactive', className: 'bg-gray-400 text-white ' }
|
||||
};
|
||||
|
||||
const statusConfig = config[status] || config.inactive;
|
||||
return (
|
||||
<Badge variant={statusConfig.variant} className={` px-3 py-1 rounded-full text-sm`}>
|
||||
<Badge className={`${statusConfig.className} px-3 py-1 rounded-full text-sm`}>
|
||||
{statusConfig.label}
|
||||
</Badge>
|
||||
);
|
||||
@@ -134,10 +134,10 @@ const AdminStaff = () => {
|
||||
<div className="mb-8">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Staff Management
|
||||
</h1>
|
||||
<p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Manage internal team members and their roles.
|
||||
</p>
|
||||
</div>
|
||||
@@ -145,7 +145,7 @@ const AdminStaff = () => {
|
||||
{hasPermission('users.create') && (
|
||||
<Button
|
||||
onClick={() => setInviteDialogOpen(true)}
|
||||
className="btn-util-purple h-12 px-6"
|
||||
className="bg-muted-foreground hover:bg-primary text-white rounded-xl h-12 px-6"
|
||||
>
|
||||
<Mail className="h-5 w-5 mr-2" />
|
||||
Invite Staff
|
||||
@@ -154,7 +154,7 @@ const AdminStaff = () => {
|
||||
{hasPermission('users.create') && (
|
||||
<Button
|
||||
onClick={() => setCreateDialogOpen(true)}
|
||||
className="btn-util-green h-12 px-6"
|
||||
className="bg-success hover:bg-muted-teal-balanced text-white rounded-xl h-12 px-6"
|
||||
>
|
||||
<UserPlus className="h-5 w-5 mr-2" />
|
||||
Create Staff
|
||||
@@ -166,27 +166,27 @@ const AdminStaff = () => {
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid md:grid-cols-4 gap-4 mb-8">
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<p className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total Staff</p>
|
||||
<p className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6">
|
||||
<p className="text-sm text-muted-foreground mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total Staff</p>
|
||||
<p className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{users.length}
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<p className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Admins</p>
|
||||
<p className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6">
|
||||
<p className="text-sm text-muted-foreground mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Admins</p>
|
||||
<p className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{users.filter(u => ['admin', 'superadmin'].includes(u.role)).length}
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<p className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Moderators</p>
|
||||
<p className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6">
|
||||
<p className="text-sm text-muted-foreground mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Moderators</p>
|
||||
<p className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{users.filter(u => u.role === 'moderator').length}
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<p className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Active</p>
|
||||
<p className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6">
|
||||
<p className="text-sm text-muted-foreground mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Active</p>
|
||||
<p className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{users.filter(u => u.status === 'active').length}
|
||||
</p>
|
||||
</Card>
|
||||
@@ -207,20 +207,20 @@ const AdminStaff = () => {
|
||||
|
||||
<TabsContent value="staff-list">
|
||||
{/* Filters */}
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] mb-8">
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6 mb-8">
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-brand-purple " />
|
||||
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search by name or email..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-12 h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="pl-12 h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
data-testid="search-staff-input"
|
||||
/>
|
||||
</div>
|
||||
<Select value={roleFilter} onValueChange={setRoleFilter}>
|
||||
<SelectTrigger className="h-14 rounded-xl border-2 border-[var(--neutral-800)]" data-testid="role-filter-select">
|
||||
<SelectTrigger className="h-14 rounded-xl border-2 border-chart-6" data-testid="role-filter-select">
|
||||
<SelectValue placeholder="Filter by role" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -238,38 +238,36 @@ const AdminStaff = () => {
|
||||
{/* Staff List */}
|
||||
{loading ? (
|
||||
<div className="text-center py-20">
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading staff...</p>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading staff...</p>
|
||||
</div>
|
||||
) : filteredUsers.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{filteredUsers.map((user) => {
|
||||
const joinedDate = user.member_since || user.created_at;
|
||||
return (
|
||||
{filteredUsers.map((user) => (
|
||||
<Card
|
||||
key={user.id}
|
||||
className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-md transition-shadow"
|
||||
className="p-6 bg-background rounded-2xl border border-chart-6 hover:shadow-md transition-shadow"
|
||||
data-testid={`staff-card-${user.id}`}
|
||||
>
|
||||
<div className="flex justify-between items-start flex-wrap gap-4">
|
||||
<div className="flex items-start gap-4 flex-1">
|
||||
{/* Avatar */}
|
||||
<div className="h-14 w-14 rounded-full bg-[var(--neutral-800)] flex items-center justify-center text-[var(--purple-ink)] font-semibold text-lg flex-shrink-0">
|
||||
<div className="h-14 w-14 rounded-full bg-chart-6 flex items-center justify-center text-primary font-semibold text-lg flex-shrink-0">
|
||||
{user.first_name?.[0]}{user.last_name?.[0]}
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 mb-2 flex-wrap">
|
||||
<h3 className="text-xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{user.first_name} {user.last_name}
|
||||
</h3>
|
||||
{getRoleBadge(user.role)}
|
||||
{getStatusBadge(user.status)}
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 gap-2 text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="grid md:grid-cols-2 gap-2 text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p>Email: {user.email}</p>
|
||||
<p>Phone: {user.phone}</p>
|
||||
<p>Joined: {joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}</p>
|
||||
<p>Joined: {new Date(user.created_at).toLocaleDateString()}</p>
|
||||
{user.last_login && (
|
||||
<p>Last Login: {new Date(user.last_login).toLocaleDateString()}</p>
|
||||
)}
|
||||
@@ -282,7 +280,7 @@ const AdminStaff = () => {
|
||||
<Button
|
||||
onClick={() => navigate(`/admin/users/${user.id}`)}
|
||||
variant="outline"
|
||||
className="border-2 border-brand-purple text-brand-purple hover:bg-[var(--lavender-300)] rounded-full px-4 py-2"
|
||||
className="border-2 border-muted-foreground text-muted-foreground hover:bg-muted rounded-full px-4 py-2"
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
Manage
|
||||
@@ -293,8 +291,8 @@ const AdminStaff = () => {
|
||||
onClick={() => handleToggleStatus(user.id, user.status)}
|
||||
variant="outline"
|
||||
className={`border-2 rounded-full px-4 py-2 ${user.status === 'active'
|
||||
? 'border-orange-500 text-orange-600 hover:bg-orange-50 dark:hover:bg-orange-600/10'
|
||||
: 'border-green-500 text-green-600 hover:bg-green-50 hover:dark:bg-green-600/10'
|
||||
? 'border-orange-500 text-orange-600 hover:bg-orange-50'
|
||||
: 'border-green-500 text-green-600 hover:bg-green-50'
|
||||
}`}
|
||||
>
|
||||
{user.status === 'active' ? (
|
||||
@@ -315,7 +313,7 @@ const AdminStaff = () => {
|
||||
<Button
|
||||
onClick={() => handleDeleteUser(user.id, `${user.first_name} ${user.last_name}`)}
|
||||
variant="outline"
|
||||
className="border-2 border-red-500 text-red-600 hover:bg-red-50 dark:hover:bg-red-600/10 rounded-full px-4 py-2"
|
||||
className="border-2 border-red-500 text-red-600 hover:bg-red-50 rounded-full px-4 py-2"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
@@ -324,16 +322,15 @@ const AdminStaff = () => {
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-20">
|
||||
<UserCog className="h-20 w-20 text-[var(--neutral-800)] mx-auto mb-6" />
|
||||
<h3 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<UserCog className="h-20 w-20 text-chart-6 mx-auto mb-6" />
|
||||
<h3 className="text-2xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
No Staff Found
|
||||
</h3>
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{searchQuery || roleFilter !== 'all'
|
||||
? 'Try adjusting your filters'
|
||||
: 'No staff members yet'}
|
||||
|
||||
@@ -35,11 +35,7 @@ import {
|
||||
Download,
|
||||
FileDown,
|
||||
AlertTriangle,
|
||||
Info,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
ExternalLink,
|
||||
Copy
|
||||
Info
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -59,7 +55,6 @@ const AdminSubscriptions = () => {
|
||||
const [statusFilter, setStatusFilter] = useState('all');
|
||||
const [planFilter, setPlanFilter] = useState('all');
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [expandedRows, setExpandedRows] = useState(new Set());
|
||||
|
||||
// Edit subscription dialog state
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
@@ -270,38 +265,6 @@ Proceed with activation?`;
|
||||
});
|
||||
};
|
||||
|
||||
const formatDateTime = (dateString) => {
|
||||
if (!dateString) return 'N/A';
|
||||
return new Date(dateString).toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
const toggleRowExpansion = (subscriptionId) => {
|
||||
setExpandedRows((prev) => {
|
||||
const newExpanded = new Set(prev);
|
||||
if (newExpanded.has(subscriptionId)) {
|
||||
newExpanded.delete(subscriptionId);
|
||||
} else {
|
||||
newExpanded.add(subscriptionId);
|
||||
}
|
||||
return newExpanded;
|
||||
});
|
||||
};
|
||||
|
||||
const copyToClipboard = async (text, label) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
toast.success(`${label} copied to clipboard`);
|
||||
} catch (error) {
|
||||
toast.error('Failed to copy to clipboard');
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadgeVariant = (status) => {
|
||||
const variants = {
|
||||
active: 'default',
|
||||
@@ -314,7 +277,7 @@ Proceed with activation?`;
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<Loader2 className="h-12 w-12 animate-spin text-brand-purple " />
|
||||
<Loader2 className="h-12 w-12 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -323,93 +286,93 @@ Proceed with activation?`;
|
||||
<div className="space-y-8">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Subscription Management
|
||||
</h1>
|
||||
<p className="text-brand-purple mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
View and manage all member subscriptions
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid md:grid-cols-4 gap-6">
|
||||
<Card className="p-6 bg-background rounded-2xl border-2 border-[var(--neutral-800)]">
|
||||
<Card className="p-6 bg-background rounded-2xl border-2 border-chart-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Total Subscriptions
|
||||
</p>
|
||||
<p className="text-3xl font-bold text-[var(--purple-ink)] mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="text-3xl font-bold text-primary mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{stats.total || 0}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-[var(--neutral-800)]/20 rounded-full">
|
||||
<CreditCard className="h-6 w-6 text-brand-purple " />
|
||||
<div className="p-3 bg-chart-6/20 rounded-full">
|
||||
<CreditCard className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 bg-background rounded-2xl border-2 border-[var(--neutral-800)]">
|
||||
<Card className="p-6 bg-background rounded-2xl border-2 border-chart-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Active Members
|
||||
</p>
|
||||
<p className="text-3xl font-bold text-[var(--green-light)] mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="text-3xl font-bold text-success mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{stats.active || 0}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-[var(--green-light)]/10 rounded-full">
|
||||
<TrendingUp className="h-6 w-6 text-[var(--green-light)]" />
|
||||
<div className="p-3 bg-success/10 rounded-full">
|
||||
<TrendingUp className="h-6 w-6 text-success" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 bg-background rounded-2xl border-2 border-[var(--neutral-800)]">
|
||||
<Card className="p-6 bg-background rounded-2xl border-2 border-chart-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Total Revenue
|
||||
</p>
|
||||
<p className="text-3xl font-bold text-[var(--purple-ink)] mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="text-3xl font-bold text-primary mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{formatPrice(stats.total_revenue || 0)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-[var(--neutral-800)]/20 rounded-full">
|
||||
<DollarSign className="h-6 w-6 text-brand-purple " />
|
||||
<div className="p-3 bg-chart-6/20 rounded-full">
|
||||
<DollarSign className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 bg-background rounded-2xl border-2 border-[var(--neutral-800)]">
|
||||
<Card className="p-6 bg-background rounded-2xl border-2 border-chart-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Total Donations
|
||||
</p>
|
||||
<p className="text-3xl font-bold text-[var(--orange-light)] mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="text-3xl font-bold text-accent mt-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{formatPrice(stats.total_donations || 0)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-[var(--orange-light)]/10 rounded-full">
|
||||
<Heart className="h-6 w-6 text-[var(--orange-light)]" />
|
||||
<div className="p-3 bg-accent/10 rounded-full">
|
||||
<Heart className="h-6 w-6 text-accent" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Search & Filter Bar */}
|
||||
<Card className="p-6 bg-background rounded-2xl border-2 border-[var(--neutral-800)]">
|
||||
<Card className="p-6 bg-background rounded-2xl border-2 border-chart-6">
|
||||
<div className="grid md:grid-cols-3 gap-4">
|
||||
{/* Search */}
|
||||
<div className="md:col-span-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-brand-purple " />
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-muted-foreground" />
|
||||
<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 "
|
||||
className="pl-10 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -417,7 +380,7 @@ Proceed with activation?`;
|
||||
{/* Status Filter */}
|
||||
<div>
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="rounded-xl border-2 border-[var(--neutral-800)]">
|
||||
<SelectTrigger className="rounded-xl border-2 border-chart-6">
|
||||
<SelectValue placeholder="All Statuses" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -432,7 +395,7 @@ Proceed with activation?`;
|
||||
{/* Plan Filter */}
|
||||
<div>
|
||||
<Select value={planFilter} onValueChange={setPlanFilter}>
|
||||
<SelectTrigger className="rounded-xl border-2 border-[var(--neutral-800)]">
|
||||
<SelectTrigger className="rounded-xl border-2 border-chart-6">
|
||||
<SelectValue placeholder="All Plans" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -446,7 +409,7 @@ Proceed with activation?`;
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Showing {filteredSubscriptions.length} of {subscriptions.length} subscriptions
|
||||
</div>
|
||||
|
||||
@@ -456,26 +419,26 @@ Proceed with activation?`;
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
disabled={exporting}
|
||||
className="btn-green py-2 flex items-center gap-2"
|
||||
className="bg-success text-white hover:bg-muted-teal-dark rounded-full px-6 py-2 flex items-center gap-2"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
{exporting ? 'Exporting...' : 'Export'}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56 bg-background rounded-xl border-2 border-[var(--neutral-800)] shadow-lg">
|
||||
<DropdownMenuContent align="end" className="w-56 bg-background rounded-xl border-2 border-chart-6 shadow-lg">
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleExport('all')}
|
||||
className="cursor-pointer hover:bg-[var(--lavender-300)] rounded-lg p-3"
|
||||
className="cursor-pointer hover:bg-muted rounded-lg p-3"
|
||||
>
|
||||
<FileDown className="h-4 w-4 mr-2 text-brand-purple " />
|
||||
<span className="text-[var(--purple-ink)]">Export All Subscriptions</span>
|
||||
<FileDown className="h-4 w-4 mr-2 text-muted-foreground" />
|
||||
<span className="text-primary">Export All Subscriptions</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleExport('current')}
|
||||
className="cursor-pointer hover:bg-[var(--lavender-300)] rounded-lg p-3"
|
||||
className="cursor-pointer hover:bg-muted rounded-lg p-3"
|
||||
>
|
||||
<FileDown className="h-4 w-4 mr-2 text-brand-purple " />
|
||||
<span className="text-[var(--purple-ink)]">Export Current View</span>
|
||||
<FileDown className="h-4 w-4 mr-2 text-muted-foreground" />
|
||||
<span className="text-primary">Export Current View</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -484,20 +447,20 @@ Proceed with activation?`;
|
||||
</Card>
|
||||
|
||||
{/* Subscriptions Table */}
|
||||
<Card className="bg-background rounded-2xl border-2 border-[var(--neutral-800)] overflow-hidden">
|
||||
<Card className="bg-background rounded-2xl border-2 border-chart-6 overflow-hidden">
|
||||
{/* Mobile Card View */}
|
||||
<div className="md:hidden p-4 space-y-4">
|
||||
{filteredSubscriptions.length > 0 ? (
|
||||
filteredSubscriptions.map((sub) => (
|
||||
<Card key={sub.id} className="p-4 border border-[var(--neutral-800)] bg-[var(--lavender-400)]/30">
|
||||
<Card key={sub.id} className="p-4 border border-chart-6 bg-lavender-mist/30">
|
||||
<div className="space-y-3">
|
||||
{/* Member Info */}
|
||||
<div className="flex justify-between items-start border-b border-[var(--neutral-800)] pb-3">
|
||||
<div className="flex justify-between items-start border-b border-chart-6 pb-3">
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{sub.user.first_name} {sub.user.last_name}
|
||||
</p>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{sub.user.email}
|
||||
</p>
|
||||
</div>
|
||||
@@ -507,13 +470,13 @@ Proceed with activation?`;
|
||||
{/* Plan & Period */}
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<p className="text-xs text-brand-purple mb-1">Plan</p>
|
||||
<p className="font-medium text-[var(--purple-ink)]">{sub.plan.name}</p>
|
||||
<p className="text-xs text-brand-purple ">{sub.plan.billing_cycle}</p>
|
||||
<p className="text-xs text-muted-foreground mb-1">Plan</p>
|
||||
<p className="font-medium text-primary">{sub.plan.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{sub.plan.billing_cycle}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-brand-purple mb-1">Period</p>
|
||||
<p className="text-[var(--purple-ink)]">
|
||||
<p className="text-xs text-muted-foreground mb-1">Period</p>
|
||||
<p className="text-primary">
|
||||
{new Date(sub.current_period_start).toLocaleDateString()} -
|
||||
{new Date(sub.current_period_end).toLocaleDateString()}
|
||||
</p>
|
||||
@@ -523,20 +486,20 @@ Proceed with activation?`;
|
||||
{/* Pricing */}
|
||||
<div className="grid grid-cols-3 gap-2 text-sm bg-background/50 p-3 rounded">
|
||||
<div>
|
||||
<p className="text-xs text-brand-purple mb-1">Base Fee</p>
|
||||
<p className="font-medium text-[var(--purple-ink)]">
|
||||
<p className="text-xs text-muted-foreground mb-1">Base Fee</p>
|
||||
<p className="font-medium text-primary">
|
||||
${(sub.base_fee_cents / 100).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-brand-purple mb-1">Donation</p>
|
||||
<p className="font-medium text-[var(--purple-ink)]">
|
||||
<p className="text-xs text-muted-foreground mb-1">Donation</p>
|
||||
<p className="font-medium text-primary">
|
||||
${(sub.donation_cents / 100).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-brand-purple mb-1">Total</p>
|
||||
<p className="font-semibold text-[var(--purple-ink)]">
|
||||
<p className="text-xs text-muted-foreground mb-1">Total</p>
|
||||
<p className="font-semibold text-primary">
|
||||
${(sub.total_cents / 100).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -549,7 +512,7 @@ Proceed with activation?`;
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleEdit(sub)}
|
||||
className="flex-1 text-brand-purple hover:bg-[var(--neutral-800)]"
|
||||
className="flex-1 text-muted-foreground hover:bg-chart-6"
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
@@ -558,9 +521,9 @@ Proceed with activation?`;
|
||||
{sub.status === 'active' && hasPermission('subscriptions.cancel') && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline-destructive"
|
||||
variant="outline"
|
||||
onClick={() => handleCancelSubscription(sub.id)}
|
||||
className="flex-1 "
|
||||
className="flex-1 text-red-600 hover:bg-red-50"
|
||||
>
|
||||
<XCircle className="h-4 w-4 mr-2" />
|
||||
Cancel
|
||||
@@ -571,7 +534,7 @@ Proceed with activation?`;
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<div className="p-12 text-center text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="p-12 text-center text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
No subscriptions found
|
||||
</div>
|
||||
)}
|
||||
@@ -581,56 +544,50 @@ Proceed with activation?`;
|
||||
<div className="hidden md:block overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-[var(--neutral-800)]/20 border-b border-[var(--neutral-800)]">
|
||||
<th className="text-left p-4 text-[var(--purple-ink)] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<tr className="bg-chart-6/20 border-b border-chart-6">
|
||||
<th className="text-left p-4 text-primary font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Member
|
||||
</th>
|
||||
<th className="text-left p-4 text-[var(--purple-ink)] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<th className="text-left p-4 text-primary font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Plan
|
||||
</th>
|
||||
<th className="text-left p-4 text-[var(--purple-ink)] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<th className="text-left p-4 text-primary font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Status
|
||||
</th>
|
||||
<th className="text-left p-4 text-[var(--purple-ink)] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<th className="text-left p-4 text-primary font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Period
|
||||
</th>
|
||||
<th className="text-right p-4 text-[var(--purple-ink)] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<th className="text-right p-4 text-primary font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Base Fee
|
||||
</th>
|
||||
<th className="text-right p-4 text-[var(--purple-ink)] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<th className="text-right p-4 text-primary font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Donation
|
||||
</th>
|
||||
<th className="text-right p-4 text-[var(--purple-ink)] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<th className="text-right p-4 text-primary font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Total
|
||||
</th>
|
||||
<th className="text-center p-4 text-[var(--purple-ink)] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Details
|
||||
</th>
|
||||
<th className="text-center p-4 text-[var(--purple-ink)] font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<th className="text-center p-4 text-primary font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredSubscriptions.length > 0 ? (
|
||||
filteredSubscriptions.map((sub) => {
|
||||
const isExpanded = expandedRows.has(sub.id);
|
||||
return (
|
||||
<React.Fragment key={sub.id}>
|
||||
<tr className="border-b border-[var(--neutral-800)] hover:bg-[var(--lavender-400)] transition-colors">
|
||||
filteredSubscriptions.map((sub) => (
|
||||
<tr key={sub.id} className="border-b border-chart-6 hover:bg-lavender-mist transition-colors">
|
||||
<td className="p-4">
|
||||
<div className="font-medium text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<div className="font-medium text-primary" 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" }}>
|
||||
<div className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{sub.user.email}
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<div className="text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="text-primary" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{sub.plan.name}
|
||||
</div>
|
||||
<div className="text-xs text-brand-purple ">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{sub.plan.billing_cycle}
|
||||
</div>
|
||||
</td>
|
||||
@@ -640,30 +597,20 @@ Proceed with activation?`;
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<div className="text-sm text-[var(--purple-ink)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="text-sm text-primary" 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 className="text-xs text-muted-foreground">to {formatDate(sub.end_date)}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4 text-right text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<td className="p-4 text-right text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{formatPrice(sub.base_subscription_cents || 0)}
|
||||
</td>
|
||||
<td className="p-4 text-right text-[var(--orange-light)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<td className="p-4 text-right text-accent" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{formatPrice(sub.donation_cents || 0)}
|
||||
</td>
|
||||
<td className="p-4 text-right font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<td className="p-4 text-right font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{formatPrice(sub.amount_paid_cents || 0)}
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => toggleRowExpansion(sub.id)}
|
||||
className="text-brand-purple hover:bg-[var(--neutral-800)]"
|
||||
>
|
||||
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</Button>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{hasPermission('subscriptions.edit') && (
|
||||
@@ -671,7 +618,7 @@ Proceed with activation?`;
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleEdit(sub)}
|
||||
className="text-brand-purple hover:bg-[var(--neutral-800)]"
|
||||
className="text-muted-foreground hover:bg-chart-6"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -679,9 +626,9 @@ Proceed with activation?`;
|
||||
{sub.status === 'active' && hasPermission('subscriptions.cancel') && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline-destructive"
|
||||
variant="outline"
|
||||
onClick={() => handleCancelSubscription(sub.id)}
|
||||
className=""
|
||||
className="text-red-600 hover:bg-red-50"
|
||||
>
|
||||
<XCircle className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -689,162 +636,10 @@ Proceed with activation?`;
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/* Expandable Details Row */}
|
||||
{isExpanded && (
|
||||
<tr className="bg-[var(--lavender-400)]/30">
|
||||
<td 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">
|
||||
{/* Payment Information */}
|
||||
<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>
|
||||
|
||||
{/* Stripe Transaction IDs */}
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="9" className="p-12 text-center text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<td colSpan="8" className="p-12 text-center text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
No subscriptions found
|
||||
</td>
|
||||
</tr>
|
||||
@@ -858,10 +653,10 @@ Proceed with activation?`;
|
||||
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[500px] bg-background rounded-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<DialogTitle className="text-2xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Edit Subscription
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<DialogDescription className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Update subscription status or end date for {selectedSubscription?.user.first_name} {selectedSubscription?.user.last_name}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
@@ -869,14 +664,14 @@ Proceed with activation?`;
|
||||
<div className="space-y-6 py-4">
|
||||
{/* Status */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="status" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Label htmlFor="status" className="text-primary font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Status
|
||||
</Label>
|
||||
<Select
|
||||
value={editFormData.status}
|
||||
onValueChange={(value) => setEditFormData({ ...editFormData, status: value })}
|
||||
>
|
||||
<SelectTrigger className="rounded-xl border-2 border-[var(--neutral-800)]">
|
||||
<SelectTrigger className="rounded-xl border-2 border-chart-6">
|
||||
<SelectValue placeholder="Select status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -941,17 +736,17 @@ Proceed with activation?`;
|
||||
|
||||
{/* End Date */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="end_date" className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Label htmlFor="end_date" className="text-primary font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
End 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 " />
|
||||
<Calendar className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-muted-foreground" />
|
||||
<Input
|
||||
id="end_date"
|
||||
type="date"
|
||||
value={editFormData.end_date}
|
||||
onChange={(e) => setEditFormData({ ...editFormData, end_date: e.target.value })}
|
||||
className="pl-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="pl-12 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -962,7 +757,7 @@ Proceed with activation?`;
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setEditDialogOpen(false)}
|
||||
className="rounded-full border-2 border-[var(--neutral-800)]"
|
||||
className="rounded-full border-2 border-chart-6"
|
||||
disabled={isUpdating}
|
||||
>
|
||||
Cancel
|
||||
@@ -971,7 +766,7 @@ Proceed with activation?`;
|
||||
type="button"
|
||||
onClick={handleSaveSubscription}
|
||||
disabled={isUpdating}
|
||||
className="bg-[var(--green-light)] text-white hover:bg-[var(--green-mint)] rounded-full"
|
||||
className="bg-success text-white hover:bg-[#6FA087] rounded-full"
|
||||
>
|
||||
{isUpdating ? (
|
||||
<>
|
||||
|
||||
@@ -5,12 +5,9 @@ import { Card } from '../../components/ui/card';
|
||||
import { Button } from '../../components/ui/button';
|
||||
import { Badge } from '../../components/ui/badge';
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '../../components/ui/avatar';
|
||||
import { Input } from '../../components/ui/input';
|
||||
import { ArrowLeft, Mail, Phone, MapPin, Calendar, Lock, AlertTriangle, Camera, Upload, Trash2, Shield } from 'lucide-react';
|
||||
import { ArrowLeft, Mail, Phone, MapPin, Calendar, Lock, AlertTriangle, Camera, Upload, Trash2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import ConfirmationDialog from '../../components/ConfirmationDialog';
|
||||
import ChangeRoleDialog from '../../components/ChangeRoleDialog';
|
||||
import TransactionHistory from '../../components/TransactionHistory';
|
||||
|
||||
const AdminUserView = () => {
|
||||
const { userId } = useParams();
|
||||
@@ -19,65 +16,21 @@ const AdminUserView = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [resetPasswordLoading, setResetPasswordLoading] = useState(false);
|
||||
const [resendVerificationLoading, setResendVerificationLoading] = useState(false);
|
||||
const [transactions, setTransactions] = useState({ subscriptions: [], donations: [] });
|
||||
const [transactionsLoading, setTransactionsLoading] = useState(true);
|
||||
const [subscriptions, setSubscriptions] = useState([]);
|
||||
const [subscriptionsLoading, setSubscriptionsLoading] = useState(true);
|
||||
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
|
||||
const [pendingAction, setPendingAction] = useState(null);
|
||||
const [uploadingPhoto, setUploadingPhoto] = useState(false);
|
||||
const [maxFileSizeMB, setMaxFileSizeMB] = useState(50);
|
||||
const [maxFileSizeBytes, setMaxFileSizeBytes] = useState(52428800);
|
||||
const [memberSince, setMemberSince] = useState('');
|
||||
const [memberSinceSaving, setMemberSinceSaving] = useState(false);
|
||||
const fileInputRef = useRef(null);
|
||||
const [changeRoleDialogOpen, setChangeRoleDialogOpen] = useState(false);
|
||||
|
||||
const formatLocalDateInputValue = (date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
const formatDateInputValue = (value) => {
|
||||
if (!value) return '';
|
||||
if (typeof value === 'string') {
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
||||
return value;
|
||||
}
|
||||
const parsed = new Date(value);
|
||||
if (Number.isNaN(parsed.getTime())) {
|
||||
return value.slice(0, 10);
|
||||
}
|
||||
return formatLocalDateInputValue(parsed);
|
||||
}
|
||||
const parsed = new Date(value);
|
||||
if (Number.isNaN(parsed.getTime())) return '';
|
||||
return formatLocalDateInputValue(parsed);
|
||||
};
|
||||
|
||||
const formatDateDisplayValue = (value) => {
|
||||
if (!value) return 'N/A';
|
||||
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
||||
const [year, month, day] = value.split('-').map(Number);
|
||||
return new Date(year, month - 1, day).toLocaleDateString();
|
||||
}
|
||||
const parsed = new Date(value);
|
||||
if (Number.isNaN(parsed.getTime())) return 'N/A';
|
||||
return parsed.toLocaleDateString();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfig();
|
||||
fetchUserProfile();
|
||||
fetchTransactions();
|
||||
fetchSubscriptions();
|
||||
}, [userId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setMemberSince(formatDateInputValue(user.member_since));
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const fetchUserProfile = async () => {
|
||||
try {
|
||||
const response = await api.get(`/admin/users/${userId}`);
|
||||
@@ -90,15 +43,14 @@ const AdminUserView = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTransactions = async () => {
|
||||
const fetchSubscriptions = async () => {
|
||||
try {
|
||||
setTransactionsLoading(true);
|
||||
const response = await api.get(`/admin/users/${userId}/transactions`);
|
||||
setTransactions(response.data);
|
||||
const response = await api.get(`/admin/subscriptions?user_id=${userId}`);
|
||||
setSubscriptions(response.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch transactions:', error);
|
||||
console.error('Failed to fetch subscriptions:', error);
|
||||
} finally {
|
||||
setTransactionsLoading(false);
|
||||
setSubscriptionsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -223,27 +175,6 @@ const AdminUserView = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleMemberSinceSave = async () => {
|
||||
if (!user) return;
|
||||
setMemberSinceSaving(true);
|
||||
try {
|
||||
const payload = {
|
||||
member_since: memberSince ? memberSince : null
|
||||
};
|
||||
const response = await api.put(`/admin/users/${userId}`, payload);
|
||||
setUser(prev => ({
|
||||
...prev,
|
||||
...(response?.data || {}),
|
||||
member_since: payload.member_since
|
||||
}));
|
||||
toast.success('Member since updated successfully');
|
||||
} catch (error) {
|
||||
toast.error(error.response?.data?.detail || 'Failed to update member since');
|
||||
} finally {
|
||||
setMemberSinceSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getActionMessage = () => {
|
||||
if (!pendingAction || !user) return {};
|
||||
|
||||
@@ -271,18 +202,9 @@ const AdminUserView = () => {
|
||||
return {};
|
||||
};
|
||||
|
||||
const handleRoleChanged = () => {
|
||||
// Refresh user data after role change
|
||||
fetchUserProfile();
|
||||
};
|
||||
|
||||
if (loading) return <div>Loading...</div>;
|
||||
if (!user) return null;
|
||||
|
||||
const joinedDate = user.member_since || user.created_at;
|
||||
const memberSinceBaseline = formatDateInputValue(user.member_since);
|
||||
const memberSinceHasChanges = memberSince !== memberSinceBaseline;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Back Button */}
|
||||
@@ -296,12 +218,12 @@ const AdminUserView = () => {
|
||||
</Button>
|
||||
|
||||
{/* User Profile Header */}
|
||||
<Card className="p-8 bg-background rounded-2xl border border-[var(--neutral-800)] mb-8">
|
||||
<Card className="p-8 bg-background rounded-2xl border border-chart-6 mb-8">
|
||||
<div className="flex items-start gap-6">
|
||||
{/* Avatar */}
|
||||
<Avatar className="h-24 w-24 border-4 border-[var(--neutral-800)]">
|
||||
<Avatar className="h-24 w-24 border-4 border-chart-6">
|
||||
<AvatarImage src={user.profile_photo_url} alt={`${user.first_name} ${user.last_name}`} />
|
||||
<AvatarFallback className="bg-[var(--neutral-800)] text-[var(--purple-ink)] font-semibold text-3xl">
|
||||
<AvatarFallback className="bg-chart-6 text-primary font-semibold text-3xl">
|
||||
{user.first_name?.[0]}{user.last_name?.[0]}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
@@ -309,7 +231,7 @@ const AdminUserView = () => {
|
||||
{/* User Info */}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<h1 className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{user.first_name} {user.last_name}
|
||||
</h1>
|
||||
{/* Status & Role Badges */}
|
||||
@@ -318,7 +240,7 @@ const AdminUserView = () => {
|
||||
</div>
|
||||
|
||||
{/* Contact Info */}
|
||||
<div className="grid md:grid-cols-2 gap-4 text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="grid md:grid-cols-2 gap-4 text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Mail className="h-4 w-4" />
|
||||
<span>{user.email}</span>
|
||||
@@ -333,7 +255,7 @@ const AdminUserView = () => {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span>Joined {formatDateDisplayValue(joinedDate)}</span>
|
||||
<span>Joined {new Date(user.created_at).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -341,8 +263,8 @@ const AdminUserView = () => {
|
||||
</Card>
|
||||
|
||||
{/* Admin Actions */}
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] mb-8">
|
||||
<h2 className="text-lg font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6 mb-8">
|
||||
<h2 className="text-lg font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Admin Actions
|
||||
</h2>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
@@ -350,27 +272,18 @@ const AdminUserView = () => {
|
||||
onClick={handleResetPasswordRequest}
|
||||
disabled={resetPasswordLoading}
|
||||
variant="outline"
|
||||
className="border-2 border-brand-purple text-brand-purple hover:bg-[var(--lavender-300)] rounded-full px-4 py-2 disabled:opacity-50"
|
||||
className="border-2 border-muted-foreground text-muted-foreground hover:bg-muted rounded-full px-4 py-2 disabled:opacity-50"
|
||||
>
|
||||
<Lock className="h-4 w-4 mr-2" />
|
||||
{resetPasswordLoading ? 'Resetting...' : 'Reset Password'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => setChangeRoleDialogOpen(true)}
|
||||
variant="outline"
|
||||
className="border-2 border-brand-purple text-brand-purple hover:bg-[var(--lavender-300)] rounded-full px-4 py-2"
|
||||
>
|
||||
<Shield className="h-4 w-4 mr-2" />
|
||||
Change Role
|
||||
</Button>
|
||||
|
||||
{!user.email_verified && (
|
||||
<Button
|
||||
onClick={handleResendVerificationRequest}
|
||||
disabled={resendVerificationLoading}
|
||||
variant="outline"
|
||||
className="border-2 border-[var(--orange-light)] text-[var(--orange-light)] hover:bg-[#FFF3E0] rounded-full px-4 py-2 disabled:opacity-50"
|
||||
className="border-2 border-accent text-accent hover:bg-[#FFF3E0] rounded-full px-4 py-2 disabled:opacity-50"
|
||||
>
|
||||
<Mail className="h-4 w-4 mr-2" />
|
||||
{resendVerificationLoading ? 'Sending...' : 'Resend Verification Email'}
|
||||
@@ -390,7 +303,7 @@ const AdminUserView = () => {
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={uploadingPhoto}
|
||||
variant="outline"
|
||||
className="border-2 border-[var(--green-light)] text-[var(--green-light)] hover:bg-[var(--green-bg)] rounded-full px-4 py-2 disabled:opacity-50"
|
||||
className="border-2 border-success text-success hover:bg-[#E8F5E9] rounded-full px-4 py-2 disabled:opacity-50"
|
||||
>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
{uploadingPhoto ? 'Uploading...' : 'Upload Photo'}
|
||||
@@ -408,7 +321,7 @@ const AdminUserView = () => {
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 text-sm text-brand-purple ml-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground ml-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<span>User will receive a temporary password via email</span>
|
||||
</div>
|
||||
@@ -416,49 +329,28 @@ const AdminUserView = () => {
|
||||
</Card>
|
||||
|
||||
{/* Additional Details */}
|
||||
<Card className="p-8 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Card className="p-8 bg-background rounded-2xl border border-chart-6">
|
||||
<h2 className="text-2xl font-semibold text-primary mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Additional Information
|
||||
</h2>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Address</label>
|
||||
<p className="text-[var(--purple-ink)] mt-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{user.address}</p>
|
||||
<label className="text-sm font-medium text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Address</label>
|
||||
<p className="text-primary mt-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{user.address}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Date of Birth</label>
|
||||
<p className="text-[var(--purple-ink)] mt-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{formatDateDisplayValue(user.date_of_birth)}
|
||||
<label className="text-sm font-medium text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Date of Birth</label>
|
||||
<p className="text-primary mt-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{new Date(user.date_of_birth).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Member Since</label>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
type="date"
|
||||
value={memberSince}
|
||||
onChange={(e) => setMemberSince(e.target.value)}
|
||||
className="max-w-[200px] border-[var(--neutral-800)]"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={handleMemberSinceSave}
|
||||
disabled={memberSinceSaving || !memberSinceHasChanges}
|
||||
className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background"
|
||||
>
|
||||
{memberSinceSaving ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{user.partner_first_name && (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Partner</label>
|
||||
<p className="text-[var(--purple-ink)] mt-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<label className="text-sm font-medium text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Partner</label>
|
||||
<p className="text-primary mt-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{user.partner_first_name} {user.partner_last_name}
|
||||
</p>
|
||||
</div>
|
||||
@@ -466,14 +358,14 @@ const AdminUserView = () => {
|
||||
|
||||
{user.referred_by_member_name && (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Referred By</label>
|
||||
<p className="text-[var(--purple-ink)] mt-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{user.referred_by_member_name}</p>
|
||||
<label className="text-sm font-medium text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Referred By</label>
|
||||
<p className="text-primary mt-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{user.referred_by_member_name}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{user.lead_sources && user.lead_sources.length > 0 && (
|
||||
<div className="md:col-span-2">
|
||||
<label className="text-sm font-medium text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Lead Sources</label>
|
||||
<label className="text-sm font-medium text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Lead Sources</label>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{user.lead_sources.map((source, idx) => (
|
||||
<Badge key={idx} variant="outline">{source}</Badge>
|
||||
@@ -484,17 +376,97 @@ const AdminUserView = () => {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Transaction History */}
|
||||
<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={true}
|
||||
/>
|
||||
{/* Subscription Info (if applicable) */}
|
||||
{user.role === 'member' && (
|
||||
<Card className="p-8 bg-background rounded-2xl border border-chart-6 mt-8">
|
||||
<h2 className="text-2xl font-semibold text-primary mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Subscription Information
|
||||
</h2>
|
||||
|
||||
{subscriptionsLoading ? (
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading subscriptions...</p>
|
||||
) : subscriptions.length === 0 ? (
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>No subscriptions found for this member.</p>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{subscriptions.map((sub) => (
|
||||
<div key={sub.id} className="p-6 bg-chart-7 rounded-xl border border-chart-6">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{sub.plan.name}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{sub.plan.billing_cycle}
|
||||
</p>
|
||||
</div>
|
||||
<Badge className={
|
||||
sub.status === 'active' ? 'bg-success text-white' :
|
||||
sub.status === 'expired' ? 'bg-red-500 text-white' :
|
||||
'bg-gray-400 text-white'
|
||||
}>
|
||||
{sub.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<label className="text-muted-foreground font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Start Date</label>
|
||||
<p className="text-primary" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{new Date(sub.start_date).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
{sub.end_date && (
|
||||
<div>
|
||||
<label className="text-muted-foreground font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>End Date</label>
|
||||
<p className="text-primary" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{new Date(sub.end_date).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="text-muted-foreground font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Base Amount</label>
|
||||
<p className="text-primary" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
${(sub.base_subscription_cents / 100).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
{sub.donation_cents > 0 && (
|
||||
<div>
|
||||
<label className="text-muted-foreground font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Donation</label>
|
||||
<p className="text-primary" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
${(sub.donation_cents / 100).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="text-muted-foreground font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total Paid</label>
|
||||
<p className="text-primary font-semibold" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
${(sub.amount_paid_cents / 100).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
{sub.payment_method && (
|
||||
<div>
|
||||
<label className="text-muted-foreground font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Payment Method</label>
|
||||
<p className="text-primary" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{sub.payment_method}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{sub.stripe_subscription_id && (
|
||||
<div className="md:col-span-2">
|
||||
<label className="text-muted-foreground font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Stripe Subscription ID</label>
|
||||
<p className="text-primary text-xs font-mono" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{sub.stripe_subscription_id}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Admin Action Confirmation Dialog */}
|
||||
<ConfirmationDialog
|
||||
@@ -504,14 +476,6 @@ const AdminUserView = () => {
|
||||
loading={resetPasswordLoading || resendVerificationLoading}
|
||||
{...getActionMessage()}
|
||||
/>
|
||||
|
||||
{/* Change Role Dialog */}
|
||||
<ChangeRoleDialog
|
||||
open={changeRoleDialogOpen}
|
||||
onClose={() => setChangeRoleDialogOpen(false)}
|
||||
user={user}
|
||||
onSuccess={handleRoleChanged}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -239,7 +239,7 @@ const AdminValidations = () => {
|
||||
const config = {
|
||||
pending_email: { label: 'Awaiting Email', className: 'bg-orange-100 text-orange-700' },
|
||||
pending_validation: { label: 'Pending Validation', className: 'bg-gray-200 text-gray-700' },
|
||||
pre_validated: { label: 'Pre-Validated', className: 'bg-[var(--green-light)] text-white' },
|
||||
pre_validated: { label: 'Pre-Validated', className: 'bg-success text-white' },
|
||||
payment_pending: { label: 'Payment Pending', className: 'bg-orange-500 text-white' },
|
||||
rejected: { label: 'Rejected', className: 'bg-red-100 text-red-700' }
|
||||
};
|
||||
@@ -279,44 +279,44 @@ const AdminValidations = () => {
|
||||
<>
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Validation Queue
|
||||
</h1>
|
||||
<p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Review and validate pending membership applications.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Card */}
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] mb-8">
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6 mb-8">
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total Pending</p>
|
||||
<p className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total Pending</p>
|
||||
<p className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{pendingUsers.length}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Awaiting Email</p>
|
||||
<p className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Awaiting Email</p>
|
||||
<p className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{pendingUsers.filter(u => u.status === 'pending_email').length}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Pending Validation</p>
|
||||
<p className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Pending Validation</p>
|
||||
<p className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{pendingUsers.filter(u => u.status === 'pending_validation').length}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Pre-Validated</p>
|
||||
<p className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Pre-Validated</p>
|
||||
<p className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{pendingUsers.filter(u => u.status === 'pre_validated').length}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Payment Pending</p>
|
||||
<p className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Payment Pending</p>
|
||||
<p className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{pendingUsers.filter(u => u.status === 'payment_pending').length}
|
||||
</p>
|
||||
</div>
|
||||
@@ -330,20 +330,20 @@ const AdminValidations = () => {
|
||||
</Card>
|
||||
|
||||
{/* Filter Card */}
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] mb-8">
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6 mb-8">
|
||||
<div className="grid md:grid-cols-3 gap-4">
|
||||
<div className="relative md:col-span-2">
|
||||
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-brand-purple " />
|
||||
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search by name, email, or phone..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-12 h-14 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="pl-12 h-14 rounded-xl border-2 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="h-14 rounded-xl border-2 border-[var(--neutral-800)]">
|
||||
<SelectTrigger className="h-14 rounded-xl border-2 border-chart-6">
|
||||
<SelectValue placeholder="Filter by status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -361,16 +361,16 @@ const AdminValidations = () => {
|
||||
{/* Table */}
|
||||
{loading ? (
|
||||
<div className="text-center py-20">
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading pending applications...</p>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading pending applications...</p>
|
||||
</div>
|
||||
) : filteredUsers.length > 0 ? (
|
||||
<>
|
||||
<Card className="bg-background rounded-2xl border border-[var(--neutral-800)] overflow-hidden">
|
||||
<Card className="bg-background rounded-2xl border border-chart-6 overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead
|
||||
className="cursor-pointer hover:bg-[var(--neutral-800)]/20"
|
||||
className="cursor-pointer hover:bg-chart-6/20"
|
||||
onClick={() => handleSort('first_name')}
|
||||
>
|
||||
Name {renderSortIcon('first_name')}
|
||||
@@ -378,13 +378,13 @@ const AdminValidations = () => {
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Phone</TableHead>
|
||||
<TableHead
|
||||
className="cursor-pointer hover:bg-[var(--neutral-800)]/20"
|
||||
className="cursor-pointer hover:bg-chart-6/20"
|
||||
onClick={() => handleSort('status')}
|
||||
>
|
||||
Status {renderSortIcon('status')}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="cursor-pointer hover:bg-[var(--neutral-800)]/20"
|
||||
className="cursor-pointer hover:bg-chart-6/20"
|
||||
onClick={() => handleSort('created_at')}
|
||||
>
|
||||
Registered {renderSortIcon('created_at')}
|
||||
@@ -415,7 +415,7 @@ const AdminValidations = () => {
|
||||
onClick={() => handleReactivateUser(user)}
|
||||
disabled={actionLoading === user.id}
|
||||
size="sm"
|
||||
className="bg-[var(--green-light)] text-white hover:bg-[var(--green-mint)]"
|
||||
className="bg-success text-white hover:bg-[#6FA087]"
|
||||
>
|
||||
{actionLoading === user.id ? 'Reactivating...' : 'Reactivate'}
|
||||
</Button>
|
||||
@@ -426,7 +426,7 @@ const AdminValidations = () => {
|
||||
onClick={() => handleBypassAndValidateRequest(user)}
|
||||
disabled={actionLoading === user.id}
|
||||
size="sm"
|
||||
className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-background"
|
||||
className="bg-chart-6 text-primary hover:bg-background"
|
||||
>
|
||||
{actionLoading === user.id ? 'Validating...' : 'Bypass & Validate'}
|
||||
</Button>
|
||||
@@ -437,7 +437,7 @@ const AdminValidations = () => {
|
||||
disabled={actionLoading === user.id}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="border-2 border-red-500 text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10"
|
||||
className="border-2 border-red-500 text-red-500 hover:bg-red-50"
|
||||
>
|
||||
<X className="h-4 w-4 mr-1" />
|
||||
Reject
|
||||
@@ -450,7 +450,7 @@ const AdminValidations = () => {
|
||||
<Button
|
||||
onClick={() => handleActivatePayment(user)}
|
||||
size="sm"
|
||||
className="btn-light-lavender"
|
||||
className="bg-chart-6 text-primary hover:bg-background"
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-1" />
|
||||
Activate Payment
|
||||
@@ -462,7 +462,7 @@ const AdminValidations = () => {
|
||||
disabled={actionLoading === user.id}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="border-2 border-red-500 text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10"
|
||||
className="border-2 border-red-500 text-red-500 hover:bg-red-50"
|
||||
>
|
||||
<X className="h-4 w-4 mr-1" />
|
||||
Reject
|
||||
@@ -476,7 +476,7 @@ const AdminValidations = () => {
|
||||
onClick={() => handleValidateRequest(user)}
|
||||
disabled={actionLoading === user.id}
|
||||
size="sm"
|
||||
className="bg-[var(--green-light)] text-white hover:bg-[var(--green-mint)]"
|
||||
className="bg-success text-white hover:bg-[#6FA087]"
|
||||
>
|
||||
{actionLoading === user.id ? 'Validating...' : 'Validate'}
|
||||
</Button>
|
||||
@@ -487,7 +487,7 @@ const AdminValidations = () => {
|
||||
disabled={actionLoading === user.id}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="border-2 border-red-500 text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10"
|
||||
className="border-2 border-red-500 text-red-500 hover:bg-red-50"
|
||||
>
|
||||
<X className="h-4 w-4 mr-1" />
|
||||
Reject
|
||||
@@ -507,7 +507,7 @@ const AdminValidations = () => {
|
||||
<div className="mt-8 flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
{/* Page size selector */}
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Show</p>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Show</p>
|
||||
<Select
|
||||
value={itemsPerPage.toString()}
|
||||
onValueChange={(val) => {
|
||||
@@ -525,7 +525,7 @@ const AdminValidations = () => {
|
||||
<SelectItem value="100">100</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
entries (showing {(currentPage - 1) * itemsPerPage + 1}-
|
||||
{Math.min(currentPage * itemsPerPage, filteredUsers.length)} of {filteredUsers.length})
|
||||
</p>
|
||||
@@ -582,11 +582,11 @@ const AdminValidations = () => {
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-20">
|
||||
<Clock className="h-20 w-20 text-[var(--neutral-800)] mx-auto mb-6" />
|
||||
<h3 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Clock className="h-20 w-20 text-chart-6 mx-auto mb-6" />
|
||||
<h3 className="text-2xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
No Pending Validations
|
||||
</h3>
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{searchQuery || statusFilter !== 'all'
|
||||
? 'Try adjusting your filters'
|
||||
: 'All applications have been reviewed!'}
|
||||
|
||||
@@ -54,7 +54,7 @@ export default function Bylaws() {
|
||||
<div className="min-h-screen bg-background">
|
||||
<Navbar />
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Loading bylaws...
|
||||
</p>
|
||||
</div>
|
||||
@@ -69,32 +69,32 @@ export default function Bylaws() {
|
||||
<div className="max-w-5xl mx-auto px-6 py-12">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
LOAF Bylaws
|
||||
</h1>
|
||||
<p className="text-brand-purple mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Review the official governing bylaws and policies of the LOAF community.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Current Bylaws */}
|
||||
{currentBylaws ? (
|
||||
<Card className="p-8 bg-background rounded-2xl border-2 border-brand-purple mb-6">
|
||||
<Card className="p-8 bg-background rounded-2xl border-2 border-muted-foreground mb-6">
|
||||
<div className="flex items-start gap-4 mb-6">
|
||||
<div className="bg-gradient-to-br from-brand-purple to-[var(--purple-ink)] p-4 rounded-xl">
|
||||
<div className="bg-gradient-to-br from-muted-foreground to-primary p-4 rounded-xl">
|
||||
<Scale className="h-8 w-8 text-white" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h2 className="text-2xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{currentBylaws.title}
|
||||
</h2>
|
||||
<Badge className="bg-[var(--green-light)] text-white">
|
||||
<Badge className="bg-success text-white">
|
||||
<Check className="h-3 w-3 mr-1" />
|
||||
Current Version
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-brand-purple mb-4">
|
||||
<div className="flex items-center gap-4 text-muted-foreground mb-4">
|
||||
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Version: <strong>{currentBylaws.version}</strong>
|
||||
</span>
|
||||
@@ -106,7 +106,7 @@ export default function Bylaws() {
|
||||
<Button
|
||||
onClick={() => window.open(currentBylaws.document_url, '_blank')}
|
||||
size="lg"
|
||||
className="bg-brand-purple text-white hover:bg-[var(--purple-muted)] rounded-full flex items-center gap-2"
|
||||
className="bg-muted-foreground text-white hover:bg-[#533a82] rounded-full flex items-center gap-2"
|
||||
>
|
||||
<ExternalLink className="h-5 w-5" />
|
||||
View Current Bylaws
|
||||
@@ -115,9 +115,9 @@ export default function Bylaws() {
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="p-12 text-center bg-background rounded-2xl border border-[var(--neutral-800)] mb-6">
|
||||
<Scale className="h-16 w-16 text-[var(--neutral-800)] mx-auto mb-4" />
|
||||
<p className="text-brand-purple text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Card className="p-12 text-center bg-background rounded-2xl border border-chart-6 mb-6">
|
||||
<Scale className="h-16 w-16 text-chart-6 mx-auto mb-4" />
|
||||
<p className="text-muted-foreground text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
No current bylaws document available
|
||||
</p>
|
||||
</Card>
|
||||
@@ -129,7 +129,7 @@ export default function Bylaws() {
|
||||
<Button
|
||||
onClick={() => setShowHistory(!showHistory)}
|
||||
variant="outline"
|
||||
className="w-full border-[var(--neutral-800)] text-brand-purple hover:bg-[var(--lavender-300)] rounded-full flex items-center justify-center gap-2"
|
||||
className="w-full border-chart-6 text-muted-foreground hover:bg-muted rounded-full flex items-center justify-center gap-2"
|
||||
>
|
||||
<History className="h-4 w-4" />
|
||||
{showHistory ? 'Hide' : 'View'} Version History ({history.length - 1} previous {history.length - 1 === 1 ? 'version' : 'versions'})
|
||||
@@ -140,17 +140,17 @@ export default function Bylaws() {
|
||||
{/* Version History */}
|
||||
{showHistory && history.length > 1 && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Previous Versions
|
||||
</h3>
|
||||
{history.filter(b => !b.is_current).map(bylaws => (
|
||||
<Card key={bylaws.id} className="p-6 bg-[var(--lavender-600)] rounded-xl border border-[var(--neutral-800)]">
|
||||
<Card key={bylaws.id} className="p-6 bg-[#f9f7fc] rounded-xl border border-chart-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold text-[var(--purple-ink)] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h4 className="text-lg font-semibold text-primary mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{bylaws.title}
|
||||
</h4>
|
||||
<div className="flex items-center gap-3 text-sm text-brand-purple ">
|
||||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
||||
<span>Version {bylaws.version}</span>
|
||||
<span>•</span>
|
||||
<span>Effective {formatDate(bylaws.effective_date)}</span>
|
||||
@@ -160,7 +160,7 @@ export default function Bylaws() {
|
||||
onClick={() => window.open(bylaws.document_url, '_blank')}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-brand-purple text-brand-purple hover:bg-[var(--lavender-300)] rounded-full flex items-center gap-2"
|
||||
className="border-muted-foreground text-muted-foreground hover:bg-muted rounded-full flex items-center gap-2"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
View
|
||||
@@ -172,14 +172,14 @@ export default function Bylaws() {
|
||||
)}
|
||||
|
||||
{/* Information Card */}
|
||||
<Card className="mt-8 p-6 bg-[var(--lavender-600)] border border-[var(--neutral-800)]">
|
||||
<Card className="mt-8 p-6 bg-[#f9f7fc] border border-chart-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<Scale className="h-5 w-5 text-brand-purple mt-1" />
|
||||
<Scale className="h-5 w-5 text-muted-foreground mt-1" />
|
||||
<div>
|
||||
<h4 className="font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h4 className="font-semibold text-primary mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
About LOAF Bylaws
|
||||
</h4>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
The bylaws serve as the governing document for LOAF, outlining the organization's structure,
|
||||
membership requirements, officer responsibilities, and operational procedures. All members are
|
||||
encouraged to familiarize themselves with these guidelines.
|
||||
|
||||
@@ -107,11 +107,11 @@ const EventGallery = () => {
|
||||
|
||||
const EventCard = ({ event }) => (
|
||||
<Card
|
||||
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"
|
||||
className="p-6 bg-background rounded-2xl border border-chart-6 hover:shadow-lg hover:-translate-y-1 transition-all cursor-pointer h-full"
|
||||
onClick={() => handleEventClick(event)}
|
||||
>
|
||||
{/* Thumbnail */}
|
||||
<div className="relative h-48 mb-4 rounded-xl overflow-hidden bg-[var(--lavender-500)]">
|
||||
<div className="relative h-48 mb-4 rounded-xl overflow-hidden bg-chart-7">
|
||||
{event.thumbnail_url ? (
|
||||
<img
|
||||
src={event.thumbnail_url}
|
||||
@@ -120,35 +120,35 @@ const EventGallery = () => {
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<ImageIcon className="h-16 w-16 text-[var(--neutral-800)]" />
|
||||
<ImageIcon className="h-16 w-16 text-chart-6" />
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute top-3 right-3">
|
||||
<Badge className="bg-brand-purple text-white px-3 py-1 rounded-full">
|
||||
<Badge className="bg-muted-foreground text-white px-3 py-1 rounded-full">
|
||||
{event.gallery_count} {event.gallery_count === 1 ? 'photo' : 'photos'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Event Info */}
|
||||
<h3 className="text-xl font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-xl font-semibold text-primary mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{event.title}
|
||||
</h3>
|
||||
|
||||
{event.description && (
|
||||
<p className="text-brand-purple mb-3 line-clamp-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mb-3 line-clamp-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{event.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2 text-brand-purple ">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{moment(event.start_at).format('MMMM D, YYYY')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-brand-purple ">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<MapPin className="h-4 w-4" />
|
||||
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{event.location}</span>
|
||||
</div>
|
||||
@@ -165,10 +165,10 @@ const EventGallery = () => {
|
||||
<div className="max-w-7xl mx-auto px-6 py-12">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Event Gallery
|
||||
</h1>
|
||||
<p className="text-lg text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Browse photos from past LOAF events.
|
||||
</p>
|
||||
</div>
|
||||
@@ -176,7 +176,7 @@ const EventGallery = () => {
|
||||
{/* Events Grid */}
|
||||
{loading ? (
|
||||
<div className="text-center py-20">
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading galleries...</p>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading galleries...</p>
|
||||
</div>
|
||||
) : events.length > 0 ? (
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
@@ -186,11 +186,11 @@ const EventGallery = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-20">
|
||||
<ImageIcon className="h-20 w-20 text-[var(--neutral-800)] mx-auto mb-6" />
|
||||
<h3 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<ImageIcon className="h-20 w-20 text-chart-6 mx-auto mb-6" />
|
||||
<h3 className="text-2xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
No Event Galleries Yet
|
||||
</h3>
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Event photos will appear here once admins upload them.
|
||||
</p>
|
||||
</div>
|
||||
@@ -210,7 +210,7 @@ const EventGallery = () => {
|
||||
<Button
|
||||
onClick={handleBackToEvents}
|
||||
variant="ghost"
|
||||
className="mb-6 text-brand-purple hover:text-[var(--purple-ink)] hover:bg-[var(--lavender-500)]"
|
||||
className="mb-6 text-muted-foreground hover:text-primary hover:bg-chart-7"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
@@ -219,10 +219,10 @@ const EventGallery = () => {
|
||||
|
||||
{/* Event Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{selectedEvent.title}
|
||||
</h1>
|
||||
<div className="flex flex-wrap gap-4 text-brand-purple ">
|
||||
<div className="flex flex-wrap gap-4 text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="h-5 w-5" />
|
||||
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
@@ -233,7 +233,7 @@ const EventGallery = () => {
|
||||
<MapPin className="h-5 w-5" />
|
||||
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{selectedEvent.location}</span>
|
||||
</div>
|
||||
<Badge className="bg-brand-purple text-white px-3 py-1 rounded-full">
|
||||
<Badge className="bg-muted-foreground text-white px-3 py-1 rounded-full">
|
||||
{selectedEvent.gallery_count} {selectedEvent.gallery_count === 1 ? 'photo' : 'photos'}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -242,7 +242,7 @@ const EventGallery = () => {
|
||||
{/* Gallery Grid */}
|
||||
{galleryLoading ? (
|
||||
<div className="text-center py-20">
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading images...</p>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading images...</p>
|
||||
</div>
|
||||
) : galleryImages.length > 0 ? (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
@@ -272,11 +272,11 @@ const EventGallery = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-20">
|
||||
<ImageIcon className="h-20 w-20 text-[var(--neutral-800)] mx-auto mb-6" />
|
||||
<h3 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<ImageIcon className="h-20 w-20 text-chart-6 mx-auto mb-6" />
|
||||
<h3 className="text-2xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
No Photos Yet
|
||||
</h3>
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Photos from this event will appear here once uploaded.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function Financials() {
|
||||
<div className="min-h-screen bg-background">
|
||||
<Navbar />
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Loading financial reports...
|
||||
</p>
|
||||
</div>
|
||||
@@ -47,29 +47,29 @@ export default function Financials() {
|
||||
<div className="max-w-5xl mx-auto px-6 py-12">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Financial Reports
|
||||
</h1>
|
||||
<p className="text-brand-purple mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Access annual financial reports and stay informed about LOAF's fiscal responsibility.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Reports List */}
|
||||
{reports.length === 0 ? (
|
||||
<Card className="p-12 text-center bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<TrendingUp className="h-16 w-16 text-[var(--neutral-800)] mx-auto mb-4" />
|
||||
<p className="text-brand-purple text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Card className="p-12 text-center bg-background rounded-2xl border border-chart-6">
|
||||
<TrendingUp className="h-16 w-16 text-chart-6 mx-auto mb-4" />
|
||||
<p className="text-muted-foreground text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
No financial reports available yet
|
||||
</p>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{reports.map(report => (
|
||||
<Card key={report.id} className="p-8 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-lg transition-shadow">
|
||||
<Card key={report.id} className="p-8 bg-background rounded-2xl border border-chart-6 hover:shadow-lg transition-shadow">
|
||||
<div className="flex items-center gap-6">
|
||||
{/* Year Badge */}
|
||||
<div className="bg-gradient-to-br from-brand-purple to-[var(--purple-ink)] p-6 rounded-xl text-white min-w-[120px] text-center">
|
||||
<div className="bg-gradient-to-br from-muted-foreground to-primary p-6 rounded-xl text-white min-w-[120px] text-center">
|
||||
<DollarSign className="h-8 w-8 mx-auto mb-2" />
|
||||
<div className="text-3xl font-bold" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{report.year}
|
||||
@@ -79,17 +79,17 @@ export default function Financials() {
|
||||
|
||||
{/* Report Details */}
|
||||
<div className="flex-1">
|
||||
<h3 className="text-2xl font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-2xl font-semibold text-primary mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{report.title}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Badge variant="outline" className="border-brand-purple text-brand-purple ">
|
||||
<Badge variant="outline" className="border-muted-foreground text-muted-foreground">
|
||||
{report.document_type === 'google_drive' ? 'Google Drive' : report.document_type.toUpperCase()}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => window.open(report.document_url, '_blank')}
|
||||
className="bg-brand-purple text-white hover:bg-[var(--purple-muted)] rounded-full flex items-center gap-2"
|
||||
className="bg-muted-foreground text-white hover:bg-[#533a82] rounded-full flex items-center gap-2"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
View Report
|
||||
@@ -103,14 +103,14 @@ export default function Financials() {
|
||||
|
||||
{/* Transparency Note */}
|
||||
{reports.length > 0 && (
|
||||
<Card className="mt-8 p-6 bg-[var(--lavender-600)] border border-[var(--neutral-800)]">
|
||||
<Card className="mt-8 p-6 bg-[#f9f7fc] border border-chart-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<TrendingUp className="h-5 w-5 text-brand-purple mt-1" />
|
||||
<TrendingUp className="h-5 w-5 text-muted-foreground mt-1" />
|
||||
<div>
|
||||
<h4 className="font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h4 className="font-semibold text-primary mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Transparency & Accountability
|
||||
</h4>
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
LOAF is committed to financial transparency. These reports provide detailed information about our
|
||||
revenue, expenses, and how member contributions support our community programs and operations.
|
||||
</p>
|
||||
|
||||
@@ -127,7 +127,7 @@ export default function MemberCalendar() {
|
||||
<div className="min-h-screen bg-background">
|
||||
<Navbar />
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Loading calendar...
|
||||
</p>
|
||||
</div>
|
||||
@@ -141,10 +141,10 @@ export default function MemberCalendar() {
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6 py-12">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Event Calendar
|
||||
</h1>
|
||||
<p className="text-brand-purple mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
View and manage your event RSVPs. Click on any event to see details and update your RSVP.
|
||||
</p>
|
||||
|
||||
@@ -156,26 +156,26 @@ export default function MemberCalendar() {
|
||||
|
||||
<div className="flex gap-4 ml-auto">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded bg-[var(--green-light)]"></div>
|
||||
<span className="text-sm text-brand-purple ">Going</span>
|
||||
<div className="w-4 h-4 rounded bg-success"></div>
|
||||
<span className="text-sm text-muted-foreground">Going</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded bg-[var(--orange-400)]"></div>
|
||||
<span className="text-sm text-brand-purple ">Maybe</span>
|
||||
<div className="w-4 h-4 rounded bg-[#fb923c]"></div>
|
||||
<span className="text-sm text-muted-foreground">Maybe</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded bg-[var(--slate-400)]"></div>
|
||||
<span className="text-sm text-brand-purple ">Not Going</span>
|
||||
<div className="w-4 h-4 rounded bg-[#9ca3af]"></div>
|
||||
<span className="text-sm text-muted-foreground">Not Going</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded bg-[var(--neutral-800)]"></div>
|
||||
<span className="text-sm text-brand-purple ">No RSVP</span>
|
||||
<div className="w-4 h-4 rounded bg-chart-6"></div>
|
||||
<span className="text-sm text-muted-foreground">No RSVP</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] shadow-lg">
|
||||
<Card className="p-6 bg-background rounded-2xl border border-chart-6 shadow-lg">
|
||||
<Calendar
|
||||
localizer={localizer}
|
||||
events={calendarEvents}
|
||||
@@ -195,18 +195,18 @@ export default function MemberCalendar() {
|
||||
</Card>
|
||||
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto scrollbar-dashboard">
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
{selectedEvent && (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="bg-[var(--neutral-800)]/20 p-3 rounded-lg">
|
||||
<CalendarIcon className="h-6 w-6 text-brand-purple " />
|
||||
<div className="bg-chart-6/20 p-3 rounded-lg">
|
||||
<CalendarIcon className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
{selectedEvent.user_rsvp_status && (
|
||||
<Badge
|
||||
className={`px-3 py-1 rounded-full text-sm ${selectedEvent.user_rsvp_status === 'yes'
|
||||
? 'bg-[var(--green-light)] text-white'
|
||||
? 'bg-success text-white'
|
||||
: selectedEvent.user_rsvp_status === 'no'
|
||||
? 'bg-gray-400 text-white'
|
||||
: 'bg-orange-400 text-white'
|
||||
@@ -218,14 +218,14 @@ export default function MemberCalendar() {
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<DialogTitle className="text-2xl text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<DialogTitle className="text-2xl text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{selectedEvent.title}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3 text-brand-purple ">
|
||||
<div className="flex items-center gap-3 text-muted-foreground">
|
||||
<CalendarIcon className="h-5 w-5" />
|
||||
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{new Date(selectedEvent.start_at).toLocaleDateString('en-US', {
|
||||
@@ -236,17 +236,17 @@ export default function MemberCalendar() {
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-brand-purple ">
|
||||
<div className="flex items-center gap-3 text-muted-foreground">
|
||||
<Clock className="h-5 w-5" />
|
||||
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{new Date(selectedEvent.start_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} - {new Date(selectedEvent.end_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-brand-purple ">
|
||||
<div className="flex items-center gap-3 text-muted-foreground">
|
||||
<MapPin className="h-5 w-5" />
|
||||
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{selectedEvent.location}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-brand-purple ">
|
||||
<div className="flex items-center gap-3 text-muted-foreground">
|
||||
<Users className="h-5 w-5" />
|
||||
<span style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{selectedEvent.rsvp_count || 0} {selectedEvent.rsvp_count === 1 ? 'person' : 'people'} attending
|
||||
@@ -256,18 +256,18 @@ export default function MemberCalendar() {
|
||||
</div>
|
||||
|
||||
{selectedEvent.description && (
|
||||
<div className="pt-4 border-t border-[var(--neutral-800)]">
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<div className="pt-4 border-t border-chart-6">
|
||||
<h3 className="text-lg font-semibold text-primary mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
About This Event
|
||||
</h3>
|
||||
<p className="text-brand-purple leading-relaxed whitespace-pre-line" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground leading-relaxed whitespace-pre-line" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{selectedEvent.description}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-4 border-t border-[var(--neutral-800)]">
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<div className="pt-4 border-t border-chart-6">
|
||||
<h3 className="text-lg font-semibold text-primary mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Your RSVP
|
||||
</h3>
|
||||
<div className="flex gap-3 flex-wrap">
|
||||
@@ -276,8 +276,8 @@ export default function MemberCalendar() {
|
||||
disabled={rsvpLoading}
|
||||
size="sm"
|
||||
className={`rounded-full px-6 flex items-center gap-2 ${selectedEvent.user_rsvp_status === 'yes'
|
||||
? 'bg-[var(--green-light)] text-white hover:bg-[var(--green-muted)]'
|
||||
: 'bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-[var(--neutral-400:)]'
|
||||
? 'bg-success text-white hover:bg-[#66927e]'
|
||||
: 'bg-chart-6 text-primary hover:bg-[#c4bed8]'
|
||||
}`}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
@@ -290,7 +290,7 @@ export default function MemberCalendar() {
|
||||
variant="outline"
|
||||
className={`rounded-full px-6 flex items-center gap-2 border-2 ${selectedEvent.user_rsvp_status === 'maybe'
|
||||
? 'border-orange-400 bg-orange-100 text-orange-700'
|
||||
: 'border-brand-purple text-brand-purple hover:bg-[var(--lavender-300)]'
|
||||
: 'border-muted-foreground text-muted-foreground hover:bg-muted'
|
||||
}`}
|
||||
>
|
||||
<HelpCircle className="h-4 w-4" />
|
||||
@@ -312,8 +312,8 @@ export default function MemberCalendar() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-[var(--neutral-800)]">
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<div className="pt-4 border-t border-chart-6">
|
||||
<h3 className="text-lg font-semibold text-primary mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Add to Your Calendar
|
||||
</h3>
|
||||
<AddToCalendarButton
|
||||
|
||||
@@ -195,7 +195,7 @@ const MemberProfile = () => {
|
||||
<Navbar />
|
||||
<div className="max-w-4xl mx-auto px-6 py-12">
|
||||
<div className="text-center py-20">
|
||||
<p className="text-[var(--purple-lavender)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading profile...</p>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading profile...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -209,18 +209,18 @@ const MemberProfile = () => {
|
||||
<div className="max-w-4xl mx-auto px-6 py-12">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl md:text-5xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Member Profile
|
||||
</h1>
|
||||
<p className="text-lg text-[var(--purple-lavender)]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-lg text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Enhance your profile with a photo and social media links.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-8">
|
||||
{/* Profile Photo Section */}
|
||||
<Card className="p-8 bg-background border-[var(--neutral-800)] rounded-2xl">
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Card className="p-8 bg-background border-chart-6 rounded-2xl">
|
||||
<h2 className="text-2xl font-semibold text-primary mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Profile Photo
|
||||
</h2>
|
||||
|
||||
@@ -231,7 +231,7 @@ const MemberProfile = () => {
|
||||
<img
|
||||
src={previewImage}
|
||||
alt="Profile"
|
||||
className="w-40 h-40 rounded-full object-cover border-4 border-[var(--neutral-800)]"
|
||||
className="w-40 h-40 rounded-full object-cover border-4 border-chart-6"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -242,8 +242,8 @@ const MemberProfile = () => {
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-40 h-40 rounded-full bg-[var(--lavender-500)] border-4 border-[var(--neutral-800)] flex items-center justify-center">
|
||||
<User className="h-20 w-20 text-[var(--neutral-800)]" />
|
||||
<div className="w-40 h-40 rounded-full bg-chart-7 border-4 border-chart-6 flex items-center justify-center">
|
||||
<User className="h-20 w-20 text-chart-6" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -260,7 +260,7 @@ const MemberProfile = () => {
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={uploading}
|
||||
className="bg-[var(--purple-lavender)] hover:bg-[var(--purple-ink)] text-white rounded-xl px-6 py-3 flex items-center gap-2"
|
||||
className="bg-muted-foreground hover:bg-primary text-white rounded-xl px-6 py-3 flex items-center gap-2"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
{uploading ? (
|
||||
@@ -275,7 +275,7 @@ const MemberProfile = () => {
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<p className="text-sm text-[var(--purple-lavender)] mt-3" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground mt-3" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
JPG, PNG, WebP, or GIF. Max 50MB.
|
||||
</p>
|
||||
</div>
|
||||
@@ -283,15 +283,15 @@ const MemberProfile = () => {
|
||||
</Card>
|
||||
|
||||
{/* Social Media Section */}
|
||||
<Card className="p-8 bg-background border-[var(--neutral-800)] rounded-2xl">
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Card className="p-8 bg-background border-chart-6 rounded-2xl">
|
||||
<h2 className="text-2xl font-semibold text-primary mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Social Media Links
|
||||
</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Label className="flex items-center gap-2 text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Facebook className="h-4 w-4 text-[var(--blue-facebook)]" />
|
||||
<Label className="flex items-center gap-2 text-primary mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Facebook className="h-4 w-4 text-[#1877F2]" />
|
||||
Facebook Profile URL
|
||||
</Label>
|
||||
<Input
|
||||
@@ -300,14 +300,14 @@ const MemberProfile = () => {
|
||||
value={formData.social_media_facebook}
|
||||
onChange={handleInputChange}
|
||||
placeholder="https://facebook.com/yourprofile"
|
||||
className="border-[var(--neutral-800)] rounded-xl focus:border-[var(--purple-lavender)] focus:ring-[var(--purple-lavender)]"
|
||||
className="border-chart-6 rounded-xl focus:border-muted-foreground focus:ring-muted-foreground"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="flex items-center gap-2 text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Instagram className="h-4 w-4 text-[var(--red-instagram)]" />
|
||||
<Label className="flex items-center gap-2 text-primary mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Instagram className="h-4 w-4 text-[#E4405F]" />
|
||||
Instagram Profile URL
|
||||
</Label>
|
||||
<Input
|
||||
@@ -316,14 +316,14 @@ const MemberProfile = () => {
|
||||
value={formData.social_media_instagram}
|
||||
onChange={handleInputChange}
|
||||
placeholder="https://instagram.com/yourprofile"
|
||||
className="border-[var(--neutral-800)] rounded-xl focus:border-[var(--purple-lavender)] focus:ring-[var(--purple-lavender)]"
|
||||
className="border-chart-6 rounded-xl focus:border-muted-foreground focus:ring-muted-foreground"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="flex items-center gap-2 text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Twitter className="h-4 w-4 text-[var(--blue-twitter)]" />
|
||||
<Label className="flex items-center gap-2 text-primary mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Twitter className="h-4 w-4 text-[#1DA1F2]" />
|
||||
Twitter/X Profile URL
|
||||
</Label>
|
||||
<Input
|
||||
@@ -332,14 +332,14 @@ const MemberProfile = () => {
|
||||
value={formData.social_media_twitter}
|
||||
onChange={handleInputChange}
|
||||
placeholder="https://twitter.com/yourprofile"
|
||||
className="border-[var(--neutral-800)] rounded-xl focus:border-[var(--purple-lavender)] focus:ring-[var(--purple-lavender)]"
|
||||
className="border-chart-6 rounded-xl focus:border-muted-foreground focus:ring-muted-foreground"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="flex items-center gap-2 text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Linkedin className="h-4 w-4 text-[var(--blue-linkedin)]" />
|
||||
<Label className="flex items-center gap-2 text-primary mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Linkedin className="h-4 w-4 text-[#0A66C2]" />
|
||||
LinkedIn Profile URL
|
||||
</Label>
|
||||
<Input
|
||||
@@ -348,7 +348,7 @@ const MemberProfile = () => {
|
||||
value={formData.social_media_linkedin}
|
||||
onChange={handleInputChange}
|
||||
placeholder="https://linkedin.com/in/yourprofile"
|
||||
className="border-[var(--neutral-800)] rounded-xl focus:border-[var(--purple-lavender)] focus:ring-[var(--purple-lavender)]"
|
||||
className="border-chart-6 rounded-xl focus:border-muted-foreground focus:ring-muted-foreground"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
/>
|
||||
</div>
|
||||
@@ -356,33 +356,33 @@ const MemberProfile = () => {
|
||||
</Card>
|
||||
|
||||
{/* Directory Settings Section */}
|
||||
<Card className="p-8 bg-background border-[var(--neutral-800)] rounded-2xl">
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Card className="p-8 bg-background border-chart-6 rounded-2xl">
|
||||
<h2 className="text-2xl font-semibold text-primary mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Directory Settings
|
||||
</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between p-4 bg-[var(--lavender-500)] rounded-xl">
|
||||
<div className="flex items-center justify-between p-4 bg-chart-7 rounded-xl">
|
||||
<div className="flex-1">
|
||||
<Label className="text-[var(--purple-ink)] font-medium flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Eye className="h-4 w-4 text-[var(--purple-lavender)]" />
|
||||
<Label className="text-primary font-medium flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Eye className="h-4 w-4 text-muted-foreground" />
|
||||
Show in Members Directory
|
||||
</Label>
|
||||
<p className="text-sm text-[var(--purple-lavender)] mt-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground mt-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Allow other members to see your profile in the directory
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={formData.show_in_directory}
|
||||
onCheckedChange={handleSwitchChange}
|
||||
className="data-[state=checked]:bg-[var(--purple-lavender)]"
|
||||
className="data-[state=checked]:bg-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{formData.show_in_directory && (
|
||||
<>
|
||||
<div>
|
||||
<Label className="text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Label className="text-primary mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Directory Email (visible to members)
|
||||
</Label>
|
||||
<Input
|
||||
@@ -391,13 +391,13 @@ const MemberProfile = () => {
|
||||
value={formData.directory_email}
|
||||
onChange={handleInputChange}
|
||||
placeholder="public.email@example.com"
|
||||
className="border-[var(--neutral-800)] rounded-xl focus:border-[var(--purple-lavender)] focus:ring-[var(--purple-lavender)]"
|
||||
className="border-chart-6 rounded-xl focus:border-muted-foreground focus:ring-muted-foreground"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Label className="text-primary mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Bio (visible to members)
|
||||
</Label>
|
||||
<Textarea
|
||||
@@ -406,14 +406,14 @@ const MemberProfile = () => {
|
||||
onChange={handleInputChange}
|
||||
placeholder="Tell other members about yourself..."
|
||||
rows={4}
|
||||
className="border-[var(--neutral-800)] rounded-xl focus:border-[var(--purple-lavender)] focus:ring-[var(--purple-lavender)]"
|
||||
className="border-chart-6 rounded-xl focus:border-muted-foreground focus:ring-muted-foreground"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label className="text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Label className="text-primary mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Directory Address (optional)
|
||||
</Label>
|
||||
<Input
|
||||
@@ -422,13 +422,13 @@ const MemberProfile = () => {
|
||||
value={formData.directory_address}
|
||||
onChange={handleInputChange}
|
||||
placeholder="123 Main St, City, State"
|
||||
className="border-[var(--neutral-800)] rounded-xl focus:border-[var(--purple-lavender)] focus:ring-[var(--purple-lavender)]"
|
||||
className="border-chart-6 rounded-xl focus:border-muted-foreground focus:ring-muted-foreground"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Label className="text-primary mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Directory Phone (optional)
|
||||
</Label>
|
||||
<Input
|
||||
@@ -437,14 +437,14 @@ const MemberProfile = () => {
|
||||
value={formData.directory_phone}
|
||||
onChange={handleInputChange}
|
||||
placeholder="(555) 123-4567"
|
||||
className="border-[var(--neutral-800)] rounded-xl focus:border-[var(--purple-lavender)] focus:ring-[var(--purple-lavender)]"
|
||||
className="border-chart-6 rounded-xl focus:border-muted-foreground focus:ring-muted-foreground"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Label className="text-primary mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Partner Name (if applicable)
|
||||
</Label>
|
||||
<Input
|
||||
@@ -453,7 +453,7 @@ const MemberProfile = () => {
|
||||
value={formData.directory_partner_name}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Partner's name"
|
||||
className="border-[var(--neutral-800)] rounded-xl focus:border-[var(--purple-lavender)] focus:ring-[var(--purple-lavender)]"
|
||||
className="border-chart-6 rounded-xl focus:border-muted-foreground focus:ring-muted-foreground"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
/>
|
||||
</div>
|
||||
@@ -467,7 +467,7 @@ const MemberProfile = () => {
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
className="bg-[var(--orange-light)] hover:bg-[var(--orange-apricot)] text-white rounded-xl px-8 py-3 text-lg"
|
||||
className="bg-accent hover:bg-[#ff8c5a] text-white rounded-xl px-8 py-3 text-lg"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
{saving ? (
|
||||
|
||||
@@ -114,25 +114,23 @@ const MembersDirectory = () => {
|
||||
const Border = ({ yaxis = false }) => {
|
||||
return (
|
||||
yaxis ?
|
||||
<div className=' border-2 w-full border-brand-purple my-24' />
|
||||
: <div className=' border-2 w-full border-brand-purple mb-24' />
|
||||
<div className=' border-2 w-full border-muted-foreground my-24' />
|
||||
: <div className=' border-2 w-full border-muted-foreground mb-24' />
|
||||
)
|
||||
}
|
||||
const MemberCard = ({ member }) => {
|
||||
const joinedDate = 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">
|
||||
const MemberCard = ({ member }) => (
|
||||
<Card className="p-6 bg-background rounded-3xl border border-chart-6 hover:shadow-lg transition-all h-full">
|
||||
{/* Profile Photo */}
|
||||
<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)]"
|
||||
className="w-32 h-32 rounded-full object-cover border-4 border-chart-6"
|
||||
/>
|
||||
) : (
|
||||
<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" }}>
|
||||
<div className="w-32 h-32 rounded-full bg-chart-6 border-4 border-chart-6 flex items-center justify-center">
|
||||
<span className="text-4xl font-semibold text-muted-foreground" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{getInitials(member.first_name, member.last_name)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -140,15 +138,15 @@ const MembersDirectory = () => {
|
||||
</div>
|
||||
|
||||
{/* Name */}
|
||||
<h3 className="text-2xl font-semibold text-[var(--purple-ink)] text-center mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-2xl font-semibold text-primary 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" }}>
|
||||
<Heart className="h-4 w-4 text-accent" />
|
||||
<span className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Partner: {member.directory_partner_name}
|
||||
</span>
|
||||
</div>
|
||||
@@ -156,17 +154,17 @@ const MembersDirectory = () => {
|
||||
|
||||
{/* Bio */}
|
||||
{member.directory_bio && (
|
||||
<p className="text-brand-purple text-center mb-4 line-clamp-3" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground text-center mb-4 line-clamp-3" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{member.directory_bio}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Member Since */}
|
||||
{joinedDate && (
|
||||
{member.created_at && (
|
||||
<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(joinedDate).toLocaleDateString('en-US', {
|
||||
<Calendar className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Member since {new Date(member.created_at).toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
})}
|
||||
@@ -178,10 +176,10 @@ const MembersDirectory = () => {
|
||||
<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" />
|
||||
<Mail className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||
<a
|
||||
href={`mailto:${member.directory_email}`}
|
||||
className="text-brand-purple hover:text-[var(--purple-ink)] truncate"
|
||||
className="text-muted-foreground hover:text-primary truncate"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
>
|
||||
{member.directory_email}
|
||||
@@ -191,10 +189,10 @@ const MembersDirectory = () => {
|
||||
|
||||
{member.directory_phone && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Phone className="h-4 w-4 text-brand-purple flex-shrink-0" />
|
||||
<Phone className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||
<a
|
||||
href={`tel:${member.directory_phone}`}
|
||||
className="text-brand-purple hover:text-[var(--purple-ink)]"
|
||||
className="text-muted-foreground hover:text-primary"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
>
|
||||
{member.directory_phone}
|
||||
@@ -204,8 +202,8 @@ const MembersDirectory = () => {
|
||||
|
||||
{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" }}>
|
||||
<MapPin className="h-4 w-4 text-muted-foreground flex-shrink-0 mt-0.5" />
|
||||
<span className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{member.directory_address}
|
||||
</span>
|
||||
</div>
|
||||
@@ -214,17 +212,17 @@ const MembersDirectory = () => {
|
||||
|
||||
{/* 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="pt-4 border-t border-chart-6">
|
||||
<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"
|
||||
className="p-2 rounded-lg bg-chart-7 hover:bg-chart-6 transition-colors"
|
||||
title="Facebook"
|
||||
>
|
||||
<Facebook className="h-5 w-5 text-[var(--blue-facebook)]" />
|
||||
<Facebook className="h-5 w-5 text-[#1877F2]" />
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -233,10 +231,10 @@ const MembersDirectory = () => {
|
||||
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"
|
||||
className="p-2 rounded-lg bg-chart-7 hover:bg-chart-6 transition-colors"
|
||||
title="Instagram"
|
||||
>
|
||||
<Instagram className="h-5 w-5 text-[var(--red-instagram)]" />
|
||||
<Instagram className="h-5 w-5 text-[#E4405F]" />
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -245,10 +243,10 @@ const MembersDirectory = () => {
|
||||
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"
|
||||
className="p-2 rounded-lg bg-chart-7 hover:bg-chart-6 transition-colors"
|
||||
title="Twitter/X"
|
||||
>
|
||||
<Twitter className="h-5 w-5 text-[var(--blue-twitter)]" />
|
||||
<Twitter className="h-5 w-5 text-[#1DA1F2]" />
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -257,10 +255,10 @@ const MembersDirectory = () => {
|
||||
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"
|
||||
className="p-2 rounded-lg bg-chart-7 hover:bg-chart-6 transition-colors"
|
||||
title="LinkedIn"
|
||||
>
|
||||
<Linkedin className="h-5 w-5 text-[var(--blue-linkedin)]" />
|
||||
<Linkedin className="h-5 w-5 text-[#0A66C2]" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
@@ -268,10 +266,10 @@ const MembersDirectory = () => {
|
||||
)}
|
||||
|
||||
{/* View Profile Button */}
|
||||
<div className="pt-4 mt-4 border-t border-[var(--neutral-800)]">
|
||||
<div className="pt-4 mt-4 border-t border-chart-6">
|
||||
<Button
|
||||
onClick={() => handleViewProfile(member.id)}
|
||||
className="w-full bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-brand-purple hover:text-white rounded-full py-5"
|
||||
className="w-full bg-chart-6 text-primary hover:bg-muted-foreground hover:text-white rounded-full py-5"
|
||||
>
|
||||
<UserCircle className="h-4 w-4 mr-2" />
|
||||
View Full Profile
|
||||
@@ -279,10 +277,9 @@ const MembersDirectory = () => {
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-bl from-[var(--neutral-100:)] to-[var(--neutral-800)]">
|
||||
<div className="min-h-screen bg-gradient-to-bl from-[#F9FAFB] to-chart-6">
|
||||
<Navbar />
|
||||
|
||||
<div className="max-w-7xl mx-auto py-12">
|
||||
@@ -292,29 +289,29 @@ const MembersDirectory = () => {
|
||||
|
||||
{/* Header */}
|
||||
<div className="m-8 mt-14 flex flex-col sm:flex-row justify-between items-center ">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-primary mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
LOAF Members
|
||||
</h1>
|
||||
<p className="text-lg " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<span className='text-foreground'>Number of current memebers in the directory: </span> <span className='text-brand-purple font-medium'>{totalMembers}</span>
|
||||
<span className='text-foreground'>Number of current memebers in the directory: </span> <span className='text-muted-foreground font-medium'>{totalMembers}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="mb-24 mx-10">
|
||||
<div className="relative w-full ">
|
||||
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-brand-purple " />
|
||||
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search by name or bio..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-12 pr-4 py-6 text-3xl font-medium bg-background border-foreground rounded-full focus:border-brand-purple focus:ring-brand-purple "
|
||||
className="pl-12 pr-4 py-6 text-3xl font-medium bg-background border-foreground rounded-full focus:border-muted-foreground focus:ring-muted-foreground"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
/>
|
||||
</div>
|
||||
{searchQuery && (
|
||||
<p className="mt-3 text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="mt-3 text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Found {filteredMembers.length} {filteredMembers.length === 1 ? 'member' : 'members'}
|
||||
</p>
|
||||
)}
|
||||
@@ -328,7 +325,7 @@ const MembersDirectory = () => {
|
||||
{/* Members Grid */}
|
||||
{loading ? (
|
||||
<div className="text-center py-20">
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading members...</p>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading members...</p>
|
||||
</div>
|
||||
) : filteredMembers.length > 0 ? (
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
@@ -338,11 +335,11 @@ const MembersDirectory = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-20">
|
||||
<User className="h-20 w-20 text-[var(--neutral-800)] mx-auto mb-6" />
|
||||
<h3 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<User className="h-20 w-20 text-chart-6 mx-auto mb-6" />
|
||||
<h3 className="text-2xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{searchQuery ? 'No Members Found' : 'No Members in Directory'}
|
||||
</h3>
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{searchQuery
|
||||
? 'Try adjusting your search query.'
|
||||
: 'Members who opt in to the directory will appear here.'}
|
||||
@@ -357,18 +354,18 @@ const MembersDirectory = () => {
|
||||
|
||||
{/* Info Card */}
|
||||
{!loading && members.length > 0 && (
|
||||
<Card className="mt-12 p-6 bg-[var(--lavender-500)] border-[var(--neutral-800)]">
|
||||
<Card className="mt-12 p-6 bg-chart-7 border-chart-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="bg-[var(--neutral-800)]/20 p-3 rounded-lg">
|
||||
<User className="h-6 w-6 text-brand-purple " />
|
||||
<div className="bg-chart-6/20 p-3 rounded-lg">
|
||||
<User className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-lg font-semibold text-primary mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Want to appear in the directory?
|
||||
</h3>
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Update your profile settings to show in the directory and add your photo, bio, and contact information.{' '}
|
||||
<a href="/members/profile" className="text-[var(--orange-light)] hover:underline font-medium">
|
||||
<a href="/members/profile" className="text-accent hover:underline font-medium">
|
||||
Edit your profile →
|
||||
</a>
|
||||
</p>
|
||||
@@ -380,17 +377,17 @@ const MembersDirectory = () => {
|
||||
|
||||
{/* Profile Detail Dialog */}
|
||||
<Dialog open={profileDialogOpen} onOpenChange={setProfileDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[600px] bg-background rounded-2xl max-h-[90vh] overflow-y-auto scrollbar-dashboard">
|
||||
<DialogContent className="sm:max-w-[600px] bg-background rounded-2xl max-h-[90vh] overflow-y-auto">
|
||||
{selectedMember && (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<DialogTitle className="text-3xl font-semibold text-primary" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{selectedMember.first_name} {selectedMember.last_name}
|
||||
</DialogTitle>
|
||||
{selectedMember.directory_partner_name && (
|
||||
<DialogDescription className="flex items-center gap-2 text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Heart className="h-5 w-5 text-[var(--orange-light)]" />
|
||||
<span className="text-brand-purple ">Partner: {selectedMember.directory_partner_name}</span>
|
||||
<Heart className="h-5 w-5 text-accent" />
|
||||
<span className="text-muted-foreground">Partner: {selectedMember.directory_partner_name}</span>
|
||||
</DialogDescription>
|
||||
)}
|
||||
</DialogHeader>
|
||||
@@ -399,10 +396,10 @@ const MembersDirectory = () => {
|
||||
{/* Bio */}
|
||||
{selectedMember.directory_bio && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-lg font-semibold text-primary mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
About
|
||||
</h3>
|
||||
<p className="text-brand-purple leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{selectedMember.directory_bio}
|
||||
</p>
|
||||
</div>
|
||||
@@ -410,20 +407,20 @@ const MembersDirectory = () => {
|
||||
|
||||
{/* Contact Information */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-lg font-semibold text-primary mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Contact Information
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{selectedMember.directory_email && (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-[var(--lavender-500)]">
|
||||
<Mail className="h-5 w-5 text-brand-purple " />
|
||||
<div className="p-2 rounded-lg bg-chart-7">
|
||||
<Mail className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Email</p>
|
||||
<p className="text-xs text-muted-foreground mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Email</p>
|
||||
<a
|
||||
href={`mailto:${selectedMember.directory_email}`}
|
||||
className="text-[var(--purple-ink)] hover:text-brand-purple font-medium"
|
||||
className="text-primary hover:text-muted-foreground font-medium"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
>
|
||||
{selectedMember.directory_email}
|
||||
@@ -434,14 +431,14 @@ const MembersDirectory = () => {
|
||||
|
||||
{selectedMember.directory_phone && (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-[var(--lavender-500)]">
|
||||
<Phone className="h-5 w-5 text-brand-purple " />
|
||||
<div className="p-2 rounded-lg bg-chart-7">
|
||||
<Phone className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Phone</p>
|
||||
<p className="text-xs text-muted-foreground mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Phone</p>
|
||||
<a
|
||||
href={`tel:${selectedMember.directory_phone}`}
|
||||
className="text-[var(--purple-ink)] hover:text-brand-purple font-medium"
|
||||
className="text-primary hover:text-muted-foreground font-medium"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
>
|
||||
{selectedMember.directory_phone}
|
||||
@@ -452,12 +449,12 @@ const MembersDirectory = () => {
|
||||
|
||||
{selectedMember.directory_address && (
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="p-2 rounded-lg bg-[var(--lavender-500)]">
|
||||
<MapPin className="h-5 w-5 text-brand-purple " />
|
||||
<div className="p-2 rounded-lg bg-chart-7">
|
||||
<MapPin className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Address</p>
|
||||
<p className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-xs text-muted-foreground mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Address</p>
|
||||
<p className="text-primary font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{selectedMember.directory_address}
|
||||
</p>
|
||||
</div>
|
||||
@@ -466,12 +463,12 @@ const MembersDirectory = () => {
|
||||
|
||||
{selectedMember.directory_dob && (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-[var(--lavender-500)]">
|
||||
<Heart className="h-5 w-5 text-[var(--orange-light)]" />
|
||||
<div className="p-2 rounded-lg bg-chart-7">
|
||||
<Heart className="h-5 w-5 text-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-brand-purple mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Birthday</p>
|
||||
<p className="text-[var(--purple-ink)] font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-xs text-muted-foreground mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Birthday</p>
|
||||
<p className="text-primary font-medium" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{formatDate(selectedMember.directory_dob)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -483,14 +480,14 @@ const MembersDirectory = () => {
|
||||
{/* Volunteer Interests */}
|
||||
{selectedMember.volunteer_interests && selectedMember.volunteer_interests.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-lg font-semibold text-primary mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Volunteer Interests
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedMember.volunteer_interests.map((interest, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-brand-purple hover:text-white"
|
||||
className="bg-chart-6 text-primary hover:bg-muted-foreground hover:text-white"
|
||||
>
|
||||
{interest}
|
||||
</Badge>
|
||||
@@ -503,7 +500,7 @@ const MembersDirectory = () => {
|
||||
{(selectedMember.social_media_facebook || selectedMember.social_media_instagram ||
|
||||
selectedMember.social_media_twitter || selectedMember.social_media_linkedin) && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-lg font-semibold text-primary mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Connect on Social Media
|
||||
</h3>
|
||||
<div className="flex gap-3">
|
||||
@@ -512,10 +509,10 @@ const MembersDirectory = () => {
|
||||
href={getSocialMediaLink(selectedMember.social_media_facebook)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-3 rounded-lg bg-[var(--lavender-500)] hover:bg-[var(--neutral-800)] transition-colors"
|
||||
className="p-3 rounded-lg bg-chart-7 hover:bg-chart-6 transition-colors"
|
||||
title="Facebook"
|
||||
>
|
||||
<Facebook className="h-6 w-6 text-[var(--blue-facebook)]" />
|
||||
<Facebook className="h-6 w-6 text-[#1877F2]" />
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -524,10 +521,10 @@ const MembersDirectory = () => {
|
||||
href={getSocialMediaLink(selectedMember.social_media_instagram)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-3 rounded-lg bg-[var(--lavender-500)] hover:bg-[var(--neutral-800)] transition-colors"
|
||||
className="p-3 rounded-lg bg-chart-7 hover:bg-chart-6 transition-colors"
|
||||
title="Instagram"
|
||||
>
|
||||
<Instagram className="h-6 w-6 text-[var(--red-instagram)]" />
|
||||
<Instagram className="h-6 w-6 text-[#E4405F]" />
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -536,10 +533,10 @@ const MembersDirectory = () => {
|
||||
href={getSocialMediaLink(selectedMember.social_media_twitter)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-3 rounded-lg bg-[var(--lavender-500)] hover:bg-[var(--neutral-800)] transition-colors"
|
||||
className="p-3 rounded-lg bg-chart-7 hover:bg-chart-6 transition-colors"
|
||||
title="Twitter/X"
|
||||
>
|
||||
<Twitter className="h-6 w-6 text-[var(--blue-twitter)]" />
|
||||
<Twitter className="h-6 w-6 text-[#1DA1F2]" />
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -548,10 +545,10 @@ const MembersDirectory = () => {
|
||||
href={getSocialMediaLink(selectedMember.social_media_linkedin)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-3 rounded-lg bg-[var(--lavender-500)] hover:bg-[var(--neutral-800)] transition-colors"
|
||||
className="p-3 rounded-lg bg-chart-7 hover:bg-chart-6 transition-colors"
|
||||
title="LinkedIn"
|
||||
>
|
||||
<Linkedin className="h-6 w-6 text-[var(--blue-linkedin)]" />
|
||||
<Linkedin className="h-6 w-6 text-[#0A66C2]" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
@@ -568,21 +565,21 @@ const MembersDirectory = () => {
|
||||
{/* Pagination */}
|
||||
{!loading && filteredMembers.length > 0 && (
|
||||
<div className="mt-10 flex flex-col items-center gap-4 pb-12">
|
||||
<p className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Showing {pageStart + 1}–{Math.min(pageStart + pageSize, filteredMembers.length)} of {filteredMembers.length}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center justify-center gap-2">
|
||||
<Button
|
||||
onClick={() => setCurrentPage(1)}
|
||||
disabled={currentPage === 1}
|
||||
className="bg-[var(--neutral-800)] rounded-full text-[var(--purple-ink)] hover:bg-brand-purple hover:text-white"
|
||||
className="bg-chart-6 rounded-full text-primary hover:bg-muted-foreground hover:text-white"
|
||||
>
|
||||
First Page
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setCurrentPage((page) => Math.max(1, page - 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="bg-[var(--neutral-800)] rounded-full text-[var(--purple-ink)] hover:bg-brand-purple hover:text-white"
|
||||
className="bg-chart-6 rounded-full text-primary hover:bg-muted-foreground hover:text-white"
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
@@ -596,8 +593,8 @@ const MembersDirectory = () => {
|
||||
onClick={() => setCurrentPage(pageNumber)}
|
||||
className={
|
||||
isActive
|
||||
? "bg-brand-purple text-white hover:bg-[var(--purple-ink)] rounded-full"
|
||||
: "bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-brand-purple hover:text-white rounded-full"
|
||||
? "bg-muted-foreground text-white hover:bg-primary rounded-full"
|
||||
: "bg-chart-6 text-primary hover:bg-muted-foreground hover:text-white rounded-full"
|
||||
}
|
||||
>
|
||||
{pageNumber}
|
||||
@@ -608,14 +605,14 @@ const MembersDirectory = () => {
|
||||
<Button
|
||||
onClick={() => setCurrentPage((page) => Math.min(totalPages, page + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-brand-purple rounded-full hover:text-white"
|
||||
className="bg-chart-6 text-primary hover:bg-muted-foreground rounded-full hover:text-white"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setCurrentPage(totalPages)}
|
||||
disabled={currentPage === totalPages}
|
||||
className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-brand-purple rounded-full hover:text-white"
|
||||
className="bg-chart-6 text-primary hover:bg-muted-foreground rounded-full hover:text-white"
|
||||
>
|
||||
Last Page
|
||||
</Button>
|
||||
|
||||
@@ -86,7 +86,7 @@ export default function NewsletterArchive() {
|
||||
<div className="min-h-screen bg-background">
|
||||
<Navbar />
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<p className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Loading newsletters...
|
||||
</p>
|
||||
</div>
|
||||
@@ -101,10 +101,10 @@ export default function NewsletterArchive() {
|
||||
<div className="max-w-7xl mx-auto px-6 py-12">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h1 className="text-4xl font-semibold text-primary mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Newsletter Archive
|
||||
</h1>
|
||||
<p className="text-brand-purple mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Browse past monthly newsletters and stay informed about LOAF community updates.
|
||||
</p>
|
||||
|
||||
@@ -112,13 +112,13 @@ export default function NewsletterArchive() {
|
||||
<div className="flex gap-4 flex-wrap items-center">
|
||||
{/* Search */}
|
||||
<div className="relative flex-1 min-w-[300px]">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-brand-purple " />
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search newsletters..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10 border-[var(--neutral-800)] focus:border-brand-purple "
|
||||
className="pl-10 border-chart-6 focus:border-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -128,7 +128,7 @@ export default function NewsletterArchive() {
|
||||
onClick={clearFilter}
|
||||
variant={selectedYear === null ? "default" : "outline"}
|
||||
size="sm"
|
||||
className={selectedYear === null ? "bg-brand-purple hover:bg-[var(--purple-muted)] text-white" : "border-brand-lavender text-brand-lavender "}
|
||||
className={selectedYear === null ? "bg-muted-foreground text-white" : "border-muted-foreground text-muted-foreground"}
|
||||
>
|
||||
All Years
|
||||
</Button>
|
||||
@@ -138,7 +138,7 @@ export default function NewsletterArchive() {
|
||||
onClick={() => handleYearFilter(year)}
|
||||
variant={selectedYear === year ? "default" : "outline"}
|
||||
size="sm"
|
||||
className={selectedYear === year ? "bg-brand-purple text-white" : "border-brand-purple text-brand-purple "}
|
||||
className={selectedYear === year ? "bg-muted-foreground text-white" : "border-muted-foreground text-muted-foreground"}
|
||||
>
|
||||
{year}
|
||||
</Button>
|
||||
@@ -149,9 +149,9 @@ export default function NewsletterArchive() {
|
||||
|
||||
{/* Newsletter List */}
|
||||
{filteredNewsletters.length === 0 ? (
|
||||
<Card className="p-12 text-center bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<FileText className="h-16 w-16 text-[var(--neutral-800)] mx-auto mb-4" />
|
||||
<p className="text-brand-purple text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<Card className="p-12 text-center bg-background rounded-2xl border border-chart-6">
|
||||
<FileText className="h-16 w-16 text-chart-6 mx-auto mb-4" />
|
||||
<p className="text-muted-foreground text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
No newsletters found
|
||||
</p>
|
||||
</Card>
|
||||
@@ -159,37 +159,37 @@ export default function NewsletterArchive() {
|
||||
<div className="space-y-8">
|
||||
{sortedYears.map(year => (
|
||||
<div key={year}>
|
||||
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h2 className="text-2xl font-semibold text-primary mb-4 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<Calendar className="h-6 w-6" />
|
||||
{year}
|
||||
</h2>
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{groupedNewsletters[year].map(newsletter => (
|
||||
<Card key={newsletter.id} className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-lg transition-shadow">
|
||||
<Card key={newsletter.id} className="p-6 bg-background rounded-2xl border border-chart-6 hover:shadow-lg transition-shadow">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="bg-[var(--neutral-800)]/20 p-3 rounded-lg flex-shrink-0">
|
||||
<FileText className="h-6 w-6 text-brand-purple " />
|
||||
<div className="bg-chart-6/20 p-3 rounded-lg flex-shrink-0">
|
||||
<FileText className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-xl font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
<h3 className="text-xl font-semibold text-primary mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{newsletter.title}
|
||||
</h3>
|
||||
{newsletter.description && (
|
||||
<p className="text-brand-purple mb-3 line-clamp-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p className="text-muted-foreground mb-3 line-clamp-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{newsletter.description}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Badge className="bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-[var(--neutral-800)]">
|
||||
<Badge className="bg-chart-6 text-primary hover:bg-chart-6">
|
||||
{formatDate(newsletter.published_date)}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="border-brand-purple text-brand-purple ">
|
||||
<Badge variant="outline" className="border-muted-foreground text-muted-foreground">
|
||||
{newsletter.document_type === 'google_docs' ? 'Google Docs' : newsletter.document_type.toUpperCase()}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => window.open(newsletter.document_url, '_blank')}
|
||||
className="w-full bg-brand-purple text-white hover:bg-[var(--purple-muted)] rounded-full flex items-center justify-center gap-2"
|
||||
className="w-full bg-muted-foreground text-white hover:bg-[#533a82] rounded-full flex items-center justify-center gap-2"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
View Newsletter
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap");
|
||||
|
||||
@import url("https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap");
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||
monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Nunito Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
"Roboto", "Oxygen", sans-serif;
|
||||
background-color: #ffffff;
|
||||
color: #422268;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: "Poppins", sans-serif;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.inter {
|
||||
font-family: "Poppins", 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%
|
||||
);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
@tailwind base;
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.btn {
|
||||
@apply 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;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply bg-primary text-primary-foreground shadow hover:bg-primary/90 rounded-full px-6 disabled:opacity-50 px-6 transition-transform;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80 rounded-full disabled:opacity-50 px-6 transition-transform;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
@apply hover:bg-brand-purple bg-brand-purple/10 rounded-full disabled:opacity-50 px-6 transition-transform text-brand-purple hover:text-background;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
@apply border border-primary border-2 text-primary shadow-sm hover:bg-primary/10 rounded-full disabled:opacity-50 px-6 transition-transform;
|
||||
}
|
||||
|
||||
.btn-accent {
|
||||
@apply bg-accent text-accent-foreground shadow hover:bg-accent/90 rounded-full disabled:opacity-50 px-6 transition-transform;
|
||||
}
|
||||
|
||||
.btn-destructive {
|
||||
@apply bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90 rounded-full disabled:opacity-50 px-6 transition-transform;
|
||||
}
|
||||
|
||||
.btn-outline-destructive {
|
||||
@apply border border-destructive bg-none border-2 text-destructive shadow-sm hover:bg-destructive/10 dark:hover:bg-destructive/10 rounded-full disabled:opacity-50 px-6 transition-transform;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
@apply text-primary underline-offset-4 hover:underline disabled:opacity-50 px-6 transition-transform;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
@apply h-8 rounded-full px-3 text-xs disabled:opacity-50 px-6;
|
||||
}
|
||||
|
||||
.btn-md {
|
||||
@apply h-9 px-4 py-2 rounded-full disabled:opacity-50 px-6;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
@apply h-10 rounded-full px-8 disabled:opacity-50;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
@apply h-9 w-9 rounded-full disabled:opacity-50 px-6;
|
||||
}
|
||||
.btn-green {
|
||||
@apply bg-[var(--green-light)] hover:bg-[var(--green-forest)] text-white transition-transform rounded-full px-6;
|
||||
}
|
||||
.btn-util-green {
|
||||
@apply bg-[var(--green-light)] hover:bg-[var(--green-forest)] text-white transition-transform rounded-xl h-12 px-6;
|
||||
}
|
||||
|
||||
.btn-util-purple {
|
||||
@apply bg-[var(--purple-lavender)] hover:bg-[var(--purple-ink-2)] text-background dark:text-white dark:hover:text-white rounded-xl h-12 px-6 transition-transform;
|
||||
}
|
||||
|
||||
.btn-light-orange {
|
||||
@apply bg-brand-light-orange hover:bg-brand-orange text-background dark:hover:text-white rounded-xl h-12 px-6 transition-transform;
|
||||
}
|
||||
.btn-pink {
|
||||
@apply bg-brand-pink hover:bg-brand-dark-rose dark:text-[var(--lavender-100)] text-background dark:hover:text-white rounded-xl h-12 px-6 transition-transform;
|
||||
}
|
||||
.btn-lavender {
|
||||
@apply bg-[var(--purple-lavender)] text-white hover:bg-[var(--purple-muted)] rounded-full flex items-center gap-2 dark:hover:bg-foreground dark:hover:text-background;
|
||||
}
|
||||
.btn-light-lavender {
|
||||
@apply bg-[var(--neutral-800)] text-[var(--purple-ink)] hover:bg-brand-lavender rounded-full px-6 transition-transform dark:hover:bg-brand-lavender dark:hover:text-brand-dark-lavender;
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
|
||||
.badge {
|
||||
@apply px-3 py-1 rounded-full text-sm font-medium transition-transform;
|
||||
}
|
||||
.badge-green {
|
||||
@apply bg-[var(--green-light)] text-white transition-transform;
|
||||
}
|
||||
/* Backgrounds */
|
||||
.bg-light-lavender {
|
||||
@apply bg-gradient-to-br from-brand-purple to-[var(--purple-ink)] text-white bg-[var(--neutral-800)] transition-transform dark:bg-brand-lavender dark:text-brand-dark-lavender;
|
||||
}
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
@tailwind base;
|
||||
|
||||
@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: var(--brand-orange);
|
||||
--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;
|
||||
|
||||
/* =========================
|
||||
Brand Colors
|
||||
========================= */
|
||||
--brand-dark-lavender: 267 47% 29%;
|
||||
--brand-purple: 256 35% 47%;
|
||||
--brand-lavender: 262 46% 80%;
|
||||
--brand-light-lavender: 256 32% 88%;
|
||||
--brand-white: 0 0% 100%;
|
||||
|
||||
--brand-dark-orange: 13 100% 42%;
|
||||
--brand-orange: 24 86% 55%;
|
||||
--brand-light-orange: 24 100% 67%;
|
||||
|
||||
--brand-pink: 324 55% 60%;
|
||||
--dusty-pink: 323 39% 52%;
|
||||
--dark-rose: 324 98% 32%;
|
||||
|
||||
/*
|
||||
==========================
|
||||
Color Patch
|
||||
==========================
|
||||
*/
|
||||
|
||||
--blue-linkedin: #0a66c2;
|
||||
--blue-facebook: #1877f2;
|
||||
--blue-twitter: #1da1f2;
|
||||
--red-instagram: #e4405f;
|
||||
--purple-ink: #422268;
|
||||
--purple-ink-2: #422268;
|
||||
--purple-deep: #48286e;
|
||||
--purple-muted: #533a82;
|
||||
--purple-plum: #553d8a;
|
||||
--purple-soft: #5a4290;
|
||||
--purple-amethyst: #644c9f;
|
||||
--purple-lilac: #664ea2;
|
||||
--purple-lavender: #664fa3;
|
||||
--purple-electric: #865edf;
|
||||
--slate-dark: #3d405b;
|
||||
--slate-muted: #6b708d;
|
||||
--slate-600: #6b7280;
|
||||
--slate-400: #9ca3af;
|
||||
--slate-dark: #3d405b;
|
||||
--slate-muted: #6b708d;
|
||||
--slate-600: #6b7280;
|
||||
--slate-400: #9ca3af;
|
||||
--green-success: #4caf50;
|
||||
--green-sage: #5a8f72;
|
||||
--green-muted: #66927e;
|
||||
--green-soft: #6a9680;
|
||||
--green-eucalyptus: #6a9a83;
|
||||
--green-forest: #5a7d68;
|
||||
--green-fern: #6da085;
|
||||
--green-mint: #6fa087;
|
||||
--green-pastel: #6fa188;
|
||||
--green-light: #81b29a;
|
||||
--green-bg: #e8f5e9;
|
||||
--orange-rust: #d16b54;
|
||||
--orange-soft: #e07a5f;
|
||||
--orange-peach: #e88a63;
|
||||
--orange-sand: #e88d66;
|
||||
--orange-apricot: #ff8c5a;
|
||||
--orange-coral: #ff8c64;
|
||||
--orange-light: #ff9e77;
|
||||
--orange-500: #ea580c;
|
||||
--orange-400: #fb923c;
|
||||
--gold-soft: #e8bf7a;
|
||||
--gold-warm: #f2cc8f;
|
||||
--gold-soft: #e8bf7a;
|
||||
--gold-warm: #f2cc8f;
|
||||
--red-soft: #ffebee;
|
||||
--lavender-100: #e8e0f5;
|
||||
--lavender-200: #eeebf4;
|
||||
--lavender-300: #f1eef9;
|
||||
--lavender-400: #f9f5ff;
|
||||
--lavender-500: #f8f7fb;
|
||||
--lavender-600: #f9f7fc;
|
||||
--lavender-700: #f9f8fb;
|
||||
--lavender-800: #eaedf4;
|
||||
--neutral-50: #fafafa;
|
||||
--neutral-100: #f9fafb;
|
||||
--neutral-200: #fdfcf8;
|
||||
--neutral-300: #eae0d5;
|
||||
--neutral-400: #c4bed8;
|
||||
--neutral-500: #c5b4e3;
|
||||
--neutral-600: #c5bfd9;
|
||||
--neutral-700: #dcd7ea;
|
||||
--neutral-800: #ddd8eb;
|
||||
--neutral-900: #ffffff;
|
||||
}
|
||||
.dark {
|
||||
/*
|
||||
==========================
|
||||
Dark Theme Colors
|
||||
==========================
|
||||
*/
|
||||
|
||||
--background: 246 28% 8%;
|
||||
--foreground: 255 30% 92%;
|
||||
|
||||
--card: 248 26% 11%;
|
||||
--card-foreground: 255 30% 92%;
|
||||
|
||||
--popover: 248 26% 12%;
|
||||
--popover-foreground: 255 30% 92%;
|
||||
|
||||
--primary: 262 70% 72%;
|
||||
--primary-foreground: 246 28% 10%;
|
||||
|
||||
--secondary: 262 22% 18%;
|
||||
--secondary-foreground: 255 30% 92%;
|
||||
|
||||
--muted: 252 18% 15%;
|
||||
--muted-foreground: var(--brand-lavender);
|
||||
|
||||
--accent: var(--brand-light-orange);
|
||||
--accent-foreground: 246 28% 10%;
|
||||
|
||||
--destructive: 0 78% 56%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--border: 255 14% 22%;
|
||||
--input: 255 14% 20%;
|
||||
--ring: 262 70% 72%;
|
||||
|
||||
/* charts (tuned for dark backgrounds) */
|
||||
--chart-1: 262 70% 72%;
|
||||
--chart-2: var(--dark-rose);
|
||||
--chart-3: 168 48% 52%;
|
||||
--chart-4: 286 64% 70%;
|
||||
--chart-5: 210 70% 66%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
/*
|
||||
=========================
|
||||
Brand Colors
|
||||
=========================
|
||||
*/
|
||||
|
||||
--brand-purple: 262 46% 80%;
|
||||
|
||||
/* -------- Purples (Primary UI) -------- */
|
||||
--purple-ink: #422268; /* deepest background */
|
||||
--purple-ink: hsl(var(--brand-light-lavender)); /* deepest background */
|
||||
--purple-ink-2: #422268;
|
||||
--purple-deep: #24153a; /* app shell */
|
||||
--purple-muted: #34204f; /* panels */
|
||||
--purple-plum: #3f2760;
|
||||
--purple-soft: #4b3374;
|
||||
--purple-amethyst: #5c45a0;
|
||||
--purple-lilac: #6a55b8;
|
||||
--purple-lavender: #664fa3;
|
||||
--purple-electric: #9b7cff; /* accents / focus */
|
||||
|
||||
/* -------- Slate / Text Neutrals -------- */
|
||||
--slate-dark: #0f1020; /* page background */
|
||||
--slate-muted: #2b2f44; /* borders / dividers */
|
||||
--slate-600: #8b90a8; /* secondary text */
|
||||
--slate-400: #c7cad6; /* primary text */
|
||||
|
||||
/* -------- Greens (Success / Calm) -------- */
|
||||
--green-success: #3ddc84;
|
||||
--green-sage: #4fa38a;
|
||||
--green-muted: #5fb39a;
|
||||
--green-soft: #6fc4aa;
|
||||
--green-eucalyptus: #7dd3b0;
|
||||
--green-fern: #315446;
|
||||
--green-mint: #8be0bc;
|
||||
--green-forest: #5e8374;
|
||||
--green-pastel: #a6ecd1;
|
||||
--green-light: #66a38b;
|
||||
--green-bg: #0f2a1e;
|
||||
|
||||
/* -------- Oranges (Warnings / Energy) -------- */
|
||||
--orange-rust: #b4533c;
|
||||
--orange-soft: #c8654c;
|
||||
--orange-peach: #d97757;
|
||||
--orange-sand: #e08966;
|
||||
--orange-apricot: #ff9f6e;
|
||||
--orange-coral: #ff8a75;
|
||||
--orange-light: #ffab91;
|
||||
--orange-500: #f97316;
|
||||
--orange-400: #fb923c;
|
||||
|
||||
/* -------- Golds -------- */
|
||||
--gold-soft: #e3c07a;
|
||||
--gold-warm: #f5d89c;
|
||||
|
||||
/* -------- Reds -------- */
|
||||
|
||||
--red-soft: #3a141c;
|
||||
|
||||
/* -------- Lavender UI Layers -------- */
|
||||
--lavender-100: #1c142c;
|
||||
--lavender-200: #241a38;
|
||||
--lavender-300: #2c2145;
|
||||
--lavender-400: #342952;
|
||||
--lavender-500: #211c35;
|
||||
--lavender-600: #45396e;
|
||||
--lavender-700: #4e417d;
|
||||
--lavender-800: #584a8c;
|
||||
|
||||
/* -------- Neutral Surfaces -------- */
|
||||
--neutral-50: #0b0b10; /* deepest bg */
|
||||
--neutral-100: #12121a;
|
||||
--neutral-200: #1a1a24;
|
||||
--neutral-300: #232330;
|
||||
--neutral-400: #2f2f40;
|
||||
--neutral-500: #3b3b52;
|
||||
--neutral-600: #515170;
|
||||
--neutral-700: #77779a;
|
||||
--neutral-800: var(--lavender-300); /* lightest bg */
|
||||
--neutral-900: #f4f4ff; /* highest contrast text */
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user