Compare commits
46 Commits
41d2466cbf
...
templates
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f71931d4a7 | ||
|
|
97cc5bdedf | ||
|
|
8011913c4d | ||
|
|
40a0e3f342 | ||
|
|
968eaccac2 | ||
|
|
11de3d1eed | ||
|
|
11142ec50e | ||
|
|
0249cad261 | ||
|
|
56711e9136 | ||
|
|
03b76a8e58 | ||
|
|
1acb13ba79 | ||
|
|
fa9a1d1d1d | ||
|
|
48802fe0c6 | ||
|
|
8c351773ba | ||
|
|
3511e7a9c8 | ||
|
|
33a4d8f4c4 | ||
|
|
1d70ac4ec7 | ||
|
|
a6c2475092 | ||
|
|
6d777ed583 | ||
|
|
99d65c917f | ||
|
|
0f16264656 | ||
|
|
33fc3a101d | ||
|
|
4093c1603e | ||
| 035cc896df | |||
|
|
8ffa97bcd1 | ||
|
|
b6d25cdab7 | ||
|
|
f3610282f2 | ||
|
|
f1dd7fe75b | ||
|
|
37ccfe7767 | ||
|
|
93cd4c1316 | ||
|
|
a6656b1ff0 | ||
|
|
1d4ed96dc9 | ||
|
|
a9bdd1d0a6 | ||
| 4848ec3942 | |||
|
|
f7fef8572a | ||
|
|
23163a7a2b | ||
|
|
4b0517b92c | ||
|
|
bebbba1ece | ||
|
|
5a46375212 | ||
|
|
d683ec6b5b | ||
|
|
03eb349f0e | ||
|
|
b842130b62 | ||
|
|
eee26cf108 | ||
|
|
ac850d65d3 | ||
|
|
40a8930b93 | ||
|
|
4d80f9aca5 |
3
.env.development
Normal file
3
.env.development
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
REACT_APP_BACKEND_URL=http://localhost:8000
|
||||||
|
REACT_APP_BASENAME=/membership
|
||||||
|
PUBLIC_URL=/membership
|
||||||
17
.env.example
Normal file
17
.env.example
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Server Configuration
|
||||||
|
PORT=3000
|
||||||
|
WDS_SOCKET_PORT=443
|
||||||
|
|
||||||
|
# Backend API URL
|
||||||
|
REACT_APP_BACKEND_URL=http://localhost:8000
|
||||||
|
|
||||||
|
# App Base Path Configuration
|
||||||
|
# Examples:
|
||||||
|
# - For root path: REACT_APP_BASENAME=
|
||||||
|
# - For subpath: REACT_APP_BASENAME=/membership
|
||||||
|
# - For production: REACT_APP_BASENAME=/membership
|
||||||
|
REACT_APP_BASENAME=
|
||||||
|
|
||||||
|
# Feature Flags
|
||||||
|
REACT_APP_ENABLE_VISUAL_EDITS=false
|
||||||
|
ENABLE_HEALTH_CHECK=false
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
.env
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "/membership",
|
"homepage": "/",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/dm-sans": "^5.2.8",
|
"@fontsource/dm-sans": "^5.2.8",
|
||||||
"@fontsource/fraunces": "^5.2.9",
|
"@fontsource/fraunces": "^5.2.9",
|
||||||
@@ -53,9 +53,11 @@
|
|||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.56.2",
|
"react-hook-form": "^7.56.2",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
"react-resizable-panels": "^3.0.1",
|
"react-resizable-panels": "^3.0.1",
|
||||||
"react-router-dom": "^7.5.1",
|
"react-router-dom": "^7.5.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.3",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
@@ -83,6 +85,7 @@
|
|||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@craco/craco": "^7.1.0",
|
"@craco/craco": "^7.1.0",
|
||||||
"@eslint/js": "9.23.0",
|
"@eslint/js": "9.23.0",
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "9.23.0",
|
"eslint": "9.23.0",
|
||||||
"eslint-plugin-import": "2.31.0",
|
"eslint-plugin-import": "2.31.0",
|
||||||
|
|||||||
BIN
public/friendships.png
Normal file
BIN
public/friendships.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -25,7 +25,6 @@
|
|||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
-->
|
-->
|
||||||
<title>LOAF - Lesbians Over Age Fifty</title>
|
<title>LOAF - Lesbians Over Age Fifty</title>
|
||||||
<script src="#"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
|||||||
BIN
public/shooting_star_2.png
Normal file
BIN
public/shooting_star_2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
public/web_elements_tagline.png
Normal file
BIN
public/web_elements_tagline.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 320 KiB |
20
src/App.js
20
src/App.js
@@ -23,6 +23,7 @@ import AdminMembers from './pages/admin/AdminMembers';
|
|||||||
import AdminPermissions from './pages/admin/AdminPermissions';
|
import AdminPermissions from './pages/admin/AdminPermissions';
|
||||||
import AdminRoles from './pages/admin/AdminRoles';
|
import AdminRoles from './pages/admin/AdminRoles';
|
||||||
import AdminEvents from './pages/admin/AdminEvents';
|
import AdminEvents from './pages/admin/AdminEvents';
|
||||||
|
import AdminEventAttendance from './pages/admin/AdminEventAttendance';
|
||||||
import AdminValidations from './pages/admin/AdminValidations';
|
import AdminValidations from './pages/admin/AdminValidations';
|
||||||
import AdminPlans from './pages/admin/AdminPlans';
|
import AdminPlans from './pages/admin/AdminPlans';
|
||||||
import AdminSubscriptions from './pages/admin/AdminSubscriptions';
|
import AdminSubscriptions from './pages/admin/AdminSubscriptions';
|
||||||
@@ -50,6 +51,8 @@ import Resources from './pages/Resources';
|
|||||||
import ContactUs from './pages/ContactUs';
|
import ContactUs from './pages/ContactUs';
|
||||||
import TermsOfService from './pages/TermsOfService';
|
import TermsOfService from './pages/TermsOfService';
|
||||||
import PrivacyPolicy from './pages/PrivacyPolicy';
|
import PrivacyPolicy from './pages/PrivacyPolicy';
|
||||||
|
import AcceptInvitation from './pages/AcceptInvitation';
|
||||||
|
import NotFound from './pages/NotFound';
|
||||||
|
|
||||||
const PrivateRoute = ({ children, adminOnly = false }) => {
|
const PrivateRoute = ({ children, adminOnly = false }) => {
|
||||||
const { user, loading } = useAuth();
|
const { user, loading } = useAuth();
|
||||||
@@ -70,14 +73,19 @@ const PrivateRoute = ({ children, adminOnly = false }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
// Read basename from environment variable (defaults to empty string for root path)
|
||||||
|
// Set REACT_APP_BASENAME in .env to use a subpath (e.g., "/membership")
|
||||||
|
const basename = process.env.REACT_APP_BASENAME || '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<BrowserRouter basename="/membership">
|
<BrowserRouter basename={basename}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Landing />} />
|
<Route path="/" element={<Landing />} />
|
||||||
<Route path="/register" element={<Register />} />
|
<Route path="/register" element={<Register />} />
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/verify-email" element={<VerifyEmail />} />
|
<Route path="/verify-email" element={<VerifyEmail />} />
|
||||||
|
<Route path="/accept-invitation" element={<AcceptInvitation />} />
|
||||||
<Route path="/forgot-password" element={<ForgotPassword />} />
|
<Route path="/forgot-password" element={<ForgotPassword />} />
|
||||||
<Route path="/reset-password" element={<ResetPassword />} />
|
<Route path="/reset-password" element={<ResetPassword />} />
|
||||||
<Route path="/change-password-required" element={
|
<Route path="/change-password-required" element={
|
||||||
@@ -211,6 +219,13 @@ function App() {
|
|||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
} />
|
} />
|
||||||
|
<Route path="/admin/events/:eventId/attendance" element={
|
||||||
|
<PrivateRoute adminOnly>
|
||||||
|
<AdminLayout>
|
||||||
|
<AdminEventAttendance />
|
||||||
|
</AdminLayout>
|
||||||
|
</PrivateRoute>
|
||||||
|
} />
|
||||||
<Route path="/admin/validations" element={
|
<Route path="/admin/validations" element={
|
||||||
<PrivateRoute adminOnly>
|
<PrivateRoute adminOnly>
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
@@ -274,6 +289,9 @@ function App() {
|
|||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
} />
|
} />
|
||||||
|
|
||||||
|
{/* 404 - Catch all undefined routes */}
|
||||||
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
<Toaster position="top-right" />
|
<Toaster position="top-right" />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
Scale,
|
Scale,
|
||||||
HardDrive,
|
HardDrive,
|
||||||
Repeat,
|
Repeat,
|
||||||
Heart
|
Heart,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
||||||
@@ -40,7 +40,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
|||||||
try {
|
try {
|
||||||
const response = await api.get('/admin/users');
|
const response = await api.get('/admin/users');
|
||||||
const pending = response.data.filter(u =>
|
const pending = response.data.filter(u =>
|
||||||
['pending_validation', 'pre_validated'].includes(u.status)
|
['pending_email', 'pending_validation', 'pre_validated', 'payment_pending'].includes(u.status)
|
||||||
);
|
);
|
||||||
setPendingCount(pending.length);
|
setPendingCount(pending.length);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -265,20 +265,24 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
|||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between p-4 border-b border-[#ddd8eb]">
|
<div className="flex items-center justify-between p-4 border-b border-[#ddd8eb]">
|
||||||
<div className="flex items-center gap-3">
|
<Link to="/" className="flex items-center gap-3 group flex-1 min-w-0">
|
||||||
<img
|
<img
|
||||||
src={`${process.env.PUBLIC_URL}/loaf-logo.png`}
|
src={`${process.env.PUBLIC_URL}/loaf-logo.png`}
|
||||||
alt="LOAF Logo"
|
alt="LOAF Logo"
|
||||||
className={`object-contain transition-all duration-200 ${
|
className={`object-contain transition-all duration-200 ${isOpen ? 'h-10 w-10' : 'h-8 w-8'
|
||||||
isOpen ? 'h-10 w-10' : 'h-8 w-8'
|
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
<h2 className="text-xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h2 className="text-xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
Admin
|
Admin
|
||||||
</h2>
|
</h2>
|
||||||
)}
|
<p className="text-xs text-[#664fa3] group-hover:text-[#ff9e77] transition-colors" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
View Public Site
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
<button
|
<button
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
className="p-2 rounded-lg hover:bg-[#DDD8EB]/20 transition-colors"
|
className="p-2 rounded-lg hover:bg-[#DDD8EB]/20 transition-colors"
|
||||||
@@ -295,7 +299,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<nav className="flex-1 overflow-y-auto p-4">
|
<nav className="flex-1 overflow-y-auto p-4 scrollbar-dashboard scrollbar-x-dashboard">
|
||||||
{/* Dashboard - Standalone */}
|
{/* Dashboard - Standalone */}
|
||||||
{renderNavItem(filteredNavItems.find(item => item.name === 'Dashboard'))}
|
{renderNavItem(filteredNavItems.find(item => item.name === 'Dashboard'))}
|
||||||
|
|
||||||
@@ -365,7 +369,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
|||||||
{/* User Section */}
|
{/* User Section */}
|
||||||
<div className="border-t border-[#ddd8eb] p-4 space-y-2">
|
<div className="border-t border-[#ddd8eb] p-4 space-y-2">
|
||||||
{isOpen && user && (
|
{isOpen && user && (
|
||||||
<div className="px-4 py-3 mb-2">
|
<div className="px-4 py-3 mb-2 flex justify-between items-center">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="h-10 w-10 rounded-full bg-[#DDD8EB] flex items-center justify-center text-[#422268] font-semibold">
|
<div className="h-10 w-10 rounded-full bg-[#DDD8EB] flex items-center justify-center text-[#422268] font-semibold">
|
||||||
{user.first_name?.[0]}{user.last_name?.[0]}
|
{user.first_name?.[0]}{user.last_name?.[0]}
|
||||||
@@ -379,6 +383,8 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Link to='/profile'><Settings size={16} />
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -392,8 +398,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-[#ddd8eb] rounded-full h-2">
|
<div className="w-full bg-[#ddd8eb] rounded-full h-2">
|
||||||
<div
|
<div
|
||||||
className={`h-2 rounded-full transition-all ${
|
className={`h-2 rounded-full transition-all ${storagePercentage > 90 ? 'bg-red-500' :
|
||||||
storagePercentage > 90 ? 'bg-red-500' :
|
|
||||||
storagePercentage > 75 ? 'bg-yellow-500' :
|
storagePercentage > 75 ? 'bg-yellow-500' :
|
||||||
'bg-[#81B29A]'
|
'bg-[#81B29A]'
|
||||||
}`}
|
}`}
|
||||||
@@ -407,8 +412,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<HardDrive className={`h-5 w-5 ${
|
<HardDrive className={`h-5 w-5 ${storagePercentage > 90 ? 'text-red-500' :
|
||||||
storagePercentage > 90 ? 'text-red-500' :
|
|
||||||
storagePercentage > 75 ? 'text-yellow-500' :
|
storagePercentage > 75 ? 'text-yellow-500' :
|
||||||
'text-[#664fa3]'
|
'text-[#664fa3]'
|
||||||
}`} />
|
}`} />
|
||||||
|
|||||||
@@ -40,15 +40,14 @@ const InviteStaffDialog = ({ open, onOpenChange, onSuccess }) => {
|
|||||||
const fetchRoles = async () => {
|
const fetchRoles = async () => {
|
||||||
setLoadingRoles(true);
|
setLoadingRoles(true);
|
||||||
try {
|
try {
|
||||||
const response = await api.get('/admin/roles');
|
// New endpoint returns roles based on user's permission level
|
||||||
// Filter to show only admin-type roles (not guest or member)
|
// Superadmin: all roles
|
||||||
const staffRoles = response.data.filter(role =>
|
// Admin: admin, finance, and non-elevated custom roles
|
||||||
['admin', 'superadmin', 'finance'].includes(role.code) || !role.is_system_role
|
const response = await api.get('/admin/roles/assignable');
|
||||||
);
|
setRoles(response.data);
|
||||||
setRoles(staffRoles);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch roles:', error);
|
console.error('Failed to fetch assignable roles:', error);
|
||||||
toast.error('Failed to load roles');
|
toast.error('Failed to load roles. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingRoles(false);
|
setLoadingRoles(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,13 +141,35 @@ const Navbar = () => {
|
|||||||
>
|
>
|
||||||
Gallery
|
Gallery
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<DropdownMenu>
|
||||||
to="/members/newsletters"
|
<DropdownMenuTrigger asChild>
|
||||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
<button className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity flex items-center gap-1 bg-transparent border-none cursor-pointer"
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
>
|
|
||||||
Documents
|
Documents
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="bg-white min-w-[220px]">
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link to="/members/newsletters" className="w-full px-3 py-2 text-[#48286e] hover:bg-[#f1eef9] cursor-pointer"
|
||||||
|
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
|
Newsletters
|
||||||
</Link>
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link to="/members/financials" className="w-full px-3 py-2 text-[#48286e] hover:bg-[#f1eef9] cursor-pointer"
|
||||||
|
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
|
Financials
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link to="/members/bylaws" className="w-full px-3 py-2 text-[#48286e] hover:bg-[#f1eef9] cursor-pointer"
|
||||||
|
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
|
Bylaws
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
<Link
|
<Link
|
||||||
to="/profile"
|
to="/profile"
|
||||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
||||||
@@ -297,14 +319,36 @@ const Navbar = () => {
|
|||||||
Gallery
|
Gallery
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
{/* Documents Section */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="px-4 py-2 text-white/70 text-sm font-semibold uppercase tracking-wider" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
|
Documents
|
||||||
|
</p>
|
||||||
<Link
|
<Link
|
||||||
to="/members/newsletters"
|
to="/members/newsletters"
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
className="block px-4 py-3 text-white text-base font-medium hover:bg-white/10 rounded-lg transition-colors"
|
className="block px-6 py-2 text-white text-base hover:bg-white/10 rounded-lg transition-colors"
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||||
>
|
>
|
||||||
Documents
|
Newsletters
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/members/financials"
|
||||||
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
|
className="block px-6 py-2 text-white text-base hover:bg-white/10 rounded-lg transition-colors"
|
||||||
|
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||||
|
>
|
||||||
|
Financials
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/members/bylaws"
|
||||||
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
|
className="block px-6 py-2 text-white text-base hover:bg-white/10 rounded-lg transition-colors"
|
||||||
|
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||||
|
>
|
||||||
|
Bylaws
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
to="/profile"
|
to="/profile"
|
||||||
|
|||||||
@@ -8,37 +8,39 @@ const PublicFooter = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Main Footer */}
|
{/* Main Footer */}
|
||||||
<footer className="bg-[#644c9f] px-4 sm:px-8 md:px-16 py-12 md:py-20 flex items-center justify-center min-h-[420px]">
|
<footer className="bg-[#644c9f] border-t border-[rgba(0,0,0,0.1)] py-8 px-2 lg:px-10 flex items-center justify-between">
|
||||||
<div className="border-t border-[rgba(0,0,0,0.1)] py-8 md:py-12 lg:py-20 flex flex-col lg:flex-row gap-8 sm:gap-12 md:gap-16 lg:gap-20 xl:gap-30 items-center justify-center w-full max-w-7xl">
|
<div className=" flex flex-col md:flex-row gap-14 md:gap-2 lg:gap-32 xl:gap-40 items-center justify-center text-left md:justify-between w-full max-w-7xl mx-auto">
|
||||||
<div className="w-32 sm:w-40 md:w-48 lg:w-[232px] flex-shrink-0">
|
<div className="w-40 sm:w-40 md:w-48 lg:w-[180px] flex-shrink-0">
|
||||||
<img src={loafLogo} alt="LOAF Logo" className="w-full h-auto aspect-square object-contain" />
|
<img src={loafLogo} alt="LOAF Logo" className="w-full h-auto aspect-square object-contain" />
|
||||||
</div>
|
</div>
|
||||||
<nav className="flex flex-col sm:flex-row gap-8 sm:gap-12 md:gap-16 lg:gap-20 xl:gap-28 items-start justify-center w-full lg:w-auto">
|
<nav className="flex flex-col sm:flex-row sm:flex-nowrap gap-8 sm:gap-4 lg:gap-20 xl:gap-28 items-start justify-center w-full lg:w-auto">
|
||||||
<div className="flex flex-col gap-2 w-full sm:w-auto sm:min-w-[163px]">
|
|
||||||
<div className="pb-4">
|
<div className="md:flex hidden flex-col gap-2 items-start text-left w-full sm:w-auto sm:min-w-[163px]">
|
||||||
<p className="text-white text-base font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>About</p>
|
<div className="pb-2 lg:pb-4">
|
||||||
|
<p className="text-white text-xl font-medium" style={{ fontFamily: "'Poppins', sans-serif" }}>About</p>
|
||||||
</div>
|
</div>
|
||||||
<Link to="/about/history" className="text-[#ddd8eb] text-sm sm:text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Inter', sans-serif" }}>History</Link>
|
<Link to="/about/history" className="text-[#ddd8eb] text-sm sm:text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>History</Link>
|
||||||
<Link to="/about/mission-values" className="text-[#ddd8eb] text-sm sm:text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Inter', sans-serif" }}>Mission and Values</Link>
|
<Link to="/about/mission-values" className="text-[#ddd8eb] text-sm sm:text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Mission and Values</Link>
|
||||||
<Link to="/about/board" className="text-[#ddd8eb] text-sm sm:text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Inter', sans-serif" }}>Board of Directors</Link>
|
<Link to="/about/board" className="text-[#ddd8eb] text-sm sm:text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Board of Directors</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2 w-full sm:w-auto sm:min-w-[148px]">
|
<div className="hidden md:flex flex-col gap-2 items-start text-left w-full sm:w-auto sm:min-w-[148px]">
|
||||||
<div className="pb-4">
|
<div className="pb-2 lg:pb-4">
|
||||||
<p className="text-white text-base font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>Connect</p>
|
<p className="text-white text-xl font-medium" style={{ fontFamily: "'Poppins', sans-serif" }}>Connect</p>
|
||||||
</div>
|
</div>
|
||||||
<Link to="/become-a-member" className="text-[#ddd8eb] text-sm sm:text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Inter', sans-serif" }}>Become a Member</Link>
|
<Link to="/become-a-member" className="text-[#ddd8eb] text-sm sm:text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Become a Member</Link>
|
||||||
<Link to="/contact-us" className="text-[#ddd8eb] text-sm sm:text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Inter', sans-serif" }}>Contact Us</Link>
|
<Link to="/contact-us" className="text-[#ddd8eb] text-sm sm:text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Contact Us</Link>
|
||||||
<Link to="/resources" className="text-[#ddd8eb] text-sm sm:text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Inter', sans-serif" }}>Resources</Link>
|
<Link to="/resources" className="text-[#ddd8eb] text-sm sm:text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Resources</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2 items-center sm:items-start w-full sm:w-auto sm:min-w-[220px] md:min-w-[271px]">
|
|
||||||
<div className="pb-4 w-full">
|
<div className="flex flex-col gap-2 items-center justify-center md:items-start text-left w-full sm:w-auto sm:min-w-[200px] md:min-w-[200px] lg:min-w-[220px]">
|
||||||
|
<div className="pb-4 w-full flex justify-center lg:justify-start">
|
||||||
<Link to="/donate" className="block">
|
<Link to="/donate" className="block">
|
||||||
<Button className="bg-[#ff9e77] hover:bg-[#ff8c64] text-[#48286e] rounded-full px-6 py-3 text-base sm:text-lg font-medium w-full sm:w-[217px]">
|
<Button className="bg-[#ff9e77] hover:bg-[#ff8c64] text-[#48286e] rounded-full px-12 lg:px-16 py-6 text-lg sm:text-lg font-medium ">
|
||||||
Donate
|
Donate
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[#ddd8eb] text-sm sm:text-base font-medium text-center sm:text-left w-full" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<p className="text-[#ddd8eb] text-sm sm:text-base font-medium text-center md:text-left w-full" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
LOAF is supported by<br />the Hollyfield Foundation
|
LOAF is supported by<br />the Hollyfield Foundation
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,22 +49,22 @@ const PublicFooter = () => {
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
{/* Bottom Footer */}
|
{/* Bottom Footer */}
|
||||||
<footer className="bg-gradient-to-r from-[#48286e] to-[#644c9f] border-t border-[rgba(0,0,0,0.1)] px-4 sm:px-8 md:px-16 py-6 md:py-8">
|
<footer className="bg-gradient-to-r from-[#48286e] to-[#644c9f] border-t border-[rgba(0,0,0,0.1)] px-4 sm:px-8 md:px-20 py-5">
|
||||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6 justify-between items-center max-w-7xl mx-auto">
|
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6 justify-between items-center max-w-7xl mx-auto">
|
||||||
<nav className="flex flex-col sm:flex-row gap-4 sm:gap-8 items-center order-1 sm:order-none">
|
<nav className="flex flex-col sm:flex-row gap-4 sm:gap-8 items-center order-1 sm:order-none">
|
||||||
<Link to="/terms-of-service" className="text-[#c5b4e3] text-sm sm:text-base font-medium hover:text-white transition-colors whitespace-nowrap" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<Link to="/terms-of-service" className="text-[#c5b4e3] text-sm sm:text-base font-medium hover:text-white transition-colors whitespace-nowrap" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
Terms of Service
|
Terms of Service
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/privacy-policy" className="text-[#c5b4e3] text-sm sm:text-base font-medium hover:text-white transition-colors whitespace-nowrap" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<Link to="/privacy-policy" className="text-[#c5b4e3] text-sm sm:text-base font-medium hover:text-white transition-colors whitespace-nowrap" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
Privacy Policy
|
Privacy Policy
|
||||||
</Link>
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
<p className="text-[#c5b4e3] text-sm sm:text-base font-medium text-center order-2 sm:order-none" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<p className="text-[#c5b4e3] text-sm sm:text-base font-medium text-center order-2 sm:order-none" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
© 2025 LOAF. All Rights Reserved.
|
© 2025 LOAF. All Rights Reserved.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[#c5b4e3] text-sm sm:text-base font-medium text-center order-3 sm:order-none" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<p className="text-[#c5b4e3] text-sm sm:text-base font-medium text-center order-3 sm:order-none" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
Designed and Managed by{' '}
|
Designed and Managed by{' '}
|
||||||
<a href="https://konceptkit.com/" className="text-[#d1c3e9] underline hover:text-white transition-colors whitespace-nowrap">
|
<a href="https://konceptkit.com/" className=" text-white transition-colors whitespace-nowrap">
|
||||||
Koncept Kit
|
Koncept Kit
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { ChevronDown, Menu, X } from 'lucide-react';
|
import { ChevronDown, Menu, X } from 'lucide-react';
|
||||||
@@ -13,8 +13,23 @@ import {
|
|||||||
const PublicNavbar = () => {
|
const PublicNavbar = () => {
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
// Helper function to check if a path is active
|
||||||
|
const isActive = (path) => {
|
||||||
|
if (path.includes('#')) {
|
||||||
|
// For hash links like /#welcome
|
||||||
|
return location.pathname + location.hash === path || location.hash === path.replace('/', '');
|
||||||
|
}
|
||||||
|
return location.pathname === path || location.pathname.startsWith(path + '/');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if any About Us sub-page is active
|
||||||
|
const isAboutActive = () => {
|
||||||
|
return location.pathname.startsWith('/about');
|
||||||
|
};
|
||||||
|
|
||||||
// LOAF logo (local)
|
// LOAF logo (local)
|
||||||
const loafLogo = `${process.env.PUBLIC_URL}/loaf-logo.png`;
|
const loafLogo = `${process.env.PUBLIC_URL}/loaf-logo.png`;
|
||||||
|
|
||||||
@@ -27,14 +42,45 @@ const PublicNavbar = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Active and inactive link styles for desktop
|
||||||
|
const getDesktopLinkClasses = (path) => {
|
||||||
|
const baseClasses = "text-[17.5px] font-medium transition-all px-3 py-1 rounded-md";
|
||||||
|
if (isActive(path)) {
|
||||||
|
return `${baseClasses} text-[#ff9e77] hover:text-[#ff8c64] `;
|
||||||
|
}
|
||||||
|
return `${baseClasses} text-white hover:opacity-80`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Active and inactive link styles for mobile
|
||||||
|
const getMobileLinkClasses = (path) => {
|
||||||
|
const baseClasses = "text-base font-medium px-4 py-3 rounded-md transition-colors";
|
||||||
|
if (isActive(path)) {
|
||||||
|
return `${baseClasses} bg-[#ff9e77] hover:bg-[#ff8c64] text-[#48286e]`;
|
||||||
|
}
|
||||||
|
return `${baseClasses} text-white hover:bg-[#48286e]`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Active and inactive link styles for mobile sub-items (About Us)
|
||||||
|
const getMobileSubLinkClasses = (path) => {
|
||||||
|
const baseClasses = "text-sm font-medium px-6 py-2 rounded-md transition-colors block";
|
||||||
|
if (isActive(path)) {
|
||||||
|
return `${baseClasses} bg-[#ff9e77] hover:bg-[#ff8c64] text-[#48286e]`;
|
||||||
|
}
|
||||||
|
return `${baseClasses} text-[#ddd8eb] hover:bg-[#48286e] hover:text-white`;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Top Header - Auth Actions */}
|
{/* Top Header - Auth Actions */}
|
||||||
<header className="bg-gradient-to-r from-[#644c9f] to-[#48286e] px-4 sm:px-8 md:px-16 py-4 flex justify-end items-center gap-4 sm:gap-6">
|
<div className='sticky top-0 inset-x-0 z-50'>
|
||||||
|
|
||||||
|
<header className="bg-gradient-to-r flex-wrap from-[#644c9f] to-[#48286e] px-[20px] py-[10px] flex md:justify-end justify-between items-center gap-4 sm:gap-6">
|
||||||
|
<div className='flex gap-4 sm:gap-6'>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleAuthAction}
|
onClick={handleAuthAction}
|
||||||
className="text-white text-base font-medium hover:opacity-80 transition-opacity bg-transparent border-none cursor-pointer"
|
className="text-white text-base font-medium hover:opacity-80 transition-opacity bg-transparent border-none cursor-pointer"
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
>
|
>
|
||||||
{user ? 'Logout' : 'Login'}
|
{user ? 'Logout' : 'Login'}
|
||||||
</button>
|
</button>
|
||||||
@@ -42,14 +88,15 @@ const PublicNavbar = () => {
|
|||||||
<Link
|
<Link
|
||||||
to="/register"
|
to="/register"
|
||||||
className="text-white text-base font-medium hover:opacity-80 transition-opacity"
|
className="text-white text-base font-medium hover:opacity-80 transition-opacity"
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
>
|
>
|
||||||
Register
|
Register
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
<Link to="/donate">
|
<Link to="/donate">
|
||||||
<Button
|
<Button
|
||||||
className="bg-[#ff9e77] hover:bg-[#ff8c64] text-[#48286e] rounded-[25px] px-[54px] py-[10px] text-[16.5px] font-semibold h-[41px]"
|
className="bg-[#ff9e77] hover:bg-[#ff8c64] text-[#48286e] rounded-[25px] px-[50px] py-[5px] text-[16.5px] font-semibold h-[41px]"
|
||||||
style={{ fontFamily: "'Montserrat', sans-serif" }}
|
style={{ fontFamily: "'Montserrat', sans-serif" }}
|
||||||
>
|
>
|
||||||
Donate
|
Donate
|
||||||
@@ -58,7 +105,7 @@ const PublicNavbar = () => {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Main Header - Navigation */}
|
{/* Main Header - Navigation */}
|
||||||
<header className="bg-[#664fa3] px-4 sm:px-8 md:px-16 py-2 flex justify-between items-center">
|
<header className=" bg-[#664fa3] px-[20px] py-2 flex justify-between items-center">
|
||||||
<Link to="/">
|
<Link to="/">
|
||||||
<img src={loafLogo} alt="LOAF Logo" className="h-16 w-16 sm:h-20 sm:w-20 md:h-28 md:w-28 object-contain" />
|
<img src={loafLogo} alt="LOAF Logo" className="h-16 w-16 sm:h-20 sm:w-20 md:h-28 md:w-28 object-contain" />
|
||||||
</Link>
|
</Link>
|
||||||
@@ -69,22 +116,25 @@ const PublicNavbar = () => {
|
|||||||
className="lg:hidden p-2 text-white hover:bg-[#48286e] rounded-md transition-colors"
|
className="lg:hidden p-2 text-white hover:bg-[#48286e] rounded-md transition-colors"
|
||||||
aria-label="Open menu"
|
aria-label="Open menu"
|
||||||
>
|
>
|
||||||
<Menu className="h-6 w-6" />
|
<Menu className="size-14" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Desktop Navigation */}
|
{/* Desktop Navigation */}
|
||||||
<nav className="hidden lg:flex gap-10 items-center">
|
<nav className="hidden lg:flex gap-6 items-center">
|
||||||
<Link
|
<Link
|
||||||
to="/#welcome"
|
to="/#welcome"
|
||||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
className={getDesktopLinkClasses('/#welcome')}
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
>
|
>
|
||||||
Welcome
|
Welcome
|
||||||
</Link>
|
</Link>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity flex items-center gap-1 bg-transparent border-none cursor-pointer"
|
<button
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
className={`${isAboutActive()
|
||||||
|
? "text-[#ff9e77] hover:text-[#ff8c64]"
|
||||||
|
: "text-white hover:opacity-80"} text-[17.5px] font-medium transition-all flex items-center gap-1 bg-transparent border-none cursor-pointer px-3 py-1 rounded-md`}
|
||||||
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
About Us
|
About Us
|
||||||
<ChevronDown className="h-4 w-4" />
|
<ChevronDown className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -92,19 +142,19 @@ const PublicNavbar = () => {
|
|||||||
<DropdownMenuContent align="start" className="bg-white min-w-[220px]">
|
<DropdownMenuContent align="start" className="bg-white min-w-[220px]">
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link to="/about/history" className="w-full px-3 py-2 text-[#48286e] hover:bg-[#f1eef9] cursor-pointer"
|
<Link to="/about/history" className="w-full px-3 py-2 text-[#48286e] hover:bg-[#f1eef9] cursor-pointer"
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
History
|
History
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link to="/about/mission-values" className="w-full px-3 py-2 text-[#48286e] hover:bg-[#f1eef9] cursor-pointer"
|
<Link to="/about/mission-values" className="w-full px-3 py-2 text-[#48286e] hover:bg-[#f1eef9] cursor-pointer"
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
Mission and Values
|
Mission and Values
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link to="/about/board" className="w-full px-3 py-2 text-[#48286e] hover:bg-[#f1eef9] cursor-pointer"
|
<Link to="/about/board" className="w-full px-3 py-2 text-[#48286e] hover:bg-[#f1eef9] cursor-pointer"
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
Board of Directors
|
Board of Directors
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -112,37 +162,38 @@ const PublicNavbar = () => {
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<Link
|
<Link
|
||||||
to={user ? "/dashboard" : "/become-a-member"}
|
to={user ? "/dashboard" : "/become-a-member"}
|
||||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
className={getDesktopLinkClasses(user ? "/dashboard" : "/become-a-member")}
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
>
|
>
|
||||||
{user ? 'Dashboard' : 'Become a Member'}
|
{user ? 'Dashboard' : 'Become a Member'}
|
||||||
</Link>
|
</Link>
|
||||||
{!user && (
|
{!user && (
|
||||||
<Link
|
<Link
|
||||||
to="/login"
|
to="/login"
|
||||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
className={getDesktopLinkClasses('/login')}
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
>
|
>
|
||||||
Members Only
|
Members Only
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<Link
|
<Link
|
||||||
to="/resources"
|
to="/resources"
|
||||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
className={getDesktopLinkClasses('/resources')}
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
>
|
>
|
||||||
Resources
|
Resources
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/contact-us"
|
to="/contact-us"
|
||||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
className={getDesktopLinkClasses('/contact-us')}
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
>
|
>
|
||||||
Contact Us
|
Contact Us
|
||||||
</Link>
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
</div>
|
||||||
{/* Mobile Menu Drawer */}
|
{/* Mobile Menu Drawer */}
|
||||||
{isMobileMenuOpen && (
|
{isMobileMenuOpen && (
|
||||||
<div className="fixed inset-0 z-50 lg:hidden">
|
<div className="fixed inset-0 z-50 lg:hidden">
|
||||||
@@ -156,7 +207,7 @@ const PublicNavbar = () => {
|
|||||||
<div className="fixed right-0 top-0 h-full w-[280px] bg-[#664fa3] shadow-xl overflow-y-auto">
|
<div className="fixed right-0 top-0 h-full w-[280px] bg-[#664fa3] shadow-xl overflow-y-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex justify-between items-center p-6 border-b border-[#48286e]">
|
<div className="flex justify-between items-center p-6 border-b border-[#48286e]">
|
||||||
<span className="text-white text-lg font-semibold" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
<span className="text-white text-lg font-semibold" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
Menu
|
Menu
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
@@ -173,38 +224,41 @@ const PublicNavbar = () => {
|
|||||||
<Link
|
<Link
|
||||||
to="/#welcome"
|
to="/#welcome"
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
className="text-white text-base font-medium hover:bg-[#48286e] px-4 py-3 rounded-md transition-colors"
|
className={getMobileLinkClasses('/#welcome')}
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
>
|
>
|
||||||
Welcome
|
Welcome
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* About Us Section */}
|
{/* About Us Section */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-white text-base font-semibold px-4 py-2" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
<p
|
||||||
|
className={`text-base font-semibold px-4 py-2 rounded-md ${isAboutActive() ? 'text-[#ff9e77]' : 'text-white'}`}
|
||||||
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
|
>
|
||||||
About Us
|
About Us
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
to="/about/history"
|
to="/about/history"
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
className="text-[#ddd8eb] text-sm font-medium hover:bg-[#48286e] hover:text-white px-6 py-2 rounded-md transition-colors block"
|
className={getMobileSubLinkClasses('/about/history')}
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
>
|
>
|
||||||
History
|
History
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/about/mission-values"
|
to="/about/mission-values"
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
className="text-[#ddd8eb] text-sm font-medium hover:bg-[#48286e] hover:text-white px-6 py-2 rounded-md transition-colors block"
|
className={getMobileSubLinkClasses('/about/mission-values')}
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
>
|
>
|
||||||
Mission and Values
|
Mission and Values
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/about/board"
|
to="/about/board"
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
className="text-[#ddd8eb] text-sm font-medium hover:bg-[#48286e] hover:text-white px-6 py-2 rounded-md transition-colors block"
|
className={getMobileSubLinkClasses('/about/board')}
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
>
|
>
|
||||||
Board of Directors
|
Board of Directors
|
||||||
</Link>
|
</Link>
|
||||||
@@ -213,8 +267,8 @@ const PublicNavbar = () => {
|
|||||||
<Link
|
<Link
|
||||||
to={user ? "/dashboard" : "/become-a-member"}
|
to={user ? "/dashboard" : "/become-a-member"}
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
className="text-white text-base font-medium hover:bg-[#48286e] px-4 py-3 rounded-md transition-colors"
|
className={getMobileLinkClasses(user ? "/dashboard" : "/become-a-member")}
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
>
|
>
|
||||||
{user ? 'Dashboard' : 'Become a Member'}
|
{user ? 'Dashboard' : 'Become a Member'}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -223,8 +277,8 @@ const PublicNavbar = () => {
|
|||||||
<Link
|
<Link
|
||||||
to="/login"
|
to="/login"
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
className="text-white text-base font-medium hover:bg-[#48286e] px-4 py-3 rounded-md transition-colors"
|
className={getMobileLinkClasses('/login')}
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
>
|
>
|
||||||
Members Only
|
Members Only
|
||||||
</Link>
|
</Link>
|
||||||
@@ -233,8 +287,8 @@ const PublicNavbar = () => {
|
|||||||
<Link
|
<Link
|
||||||
to="/resources"
|
to="/resources"
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
className="text-white text-base font-medium hover:bg-[#48286e] px-4 py-3 rounded-md transition-colors"
|
className={getMobileLinkClasses('/resources')}
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
>
|
>
|
||||||
Resources
|
Resources
|
||||||
</Link>
|
</Link>
|
||||||
@@ -242,8 +296,8 @@ const PublicNavbar = () => {
|
|||||||
<Link
|
<Link
|
||||||
to="/contact-us"
|
to="/contact-us"
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
className="text-white text-base font-medium hover:bg-[#48286e] px-4 py-3 rounded-md transition-colors"
|
className={getMobileLinkClasses('/contact-us')}
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
>
|
>
|
||||||
Contact Us
|
Contact Us
|
||||||
</Link>
|
</Link>
|
||||||
@@ -256,7 +310,7 @@ const PublicNavbar = () => {
|
|||||||
setIsMobileMenuOpen(false);
|
setIsMobileMenuOpen(false);
|
||||||
}}
|
}}
|
||||||
className="w-full text-left text-white text-base font-medium hover:bg-[#48286e] px-4 py-3 rounded-md transition-colors"
|
className="w-full text-left text-white text-base font-medium hover:bg-[#48286e] px-4 py-3 rounded-md transition-colors"
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
>
|
>
|
||||||
{user ? 'Logout' : 'Login'}
|
{user ? 'Logout' : 'Login'}
|
||||||
</button>
|
</button>
|
||||||
@@ -265,7 +319,7 @@ const PublicNavbar = () => {
|
|||||||
to="/register"
|
to="/register"
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
className="block text-white text-base font-medium hover:bg-[#48286e] px-4 py-3 rounded-md transition-colors"
|
className="block text-white text-base font-medium hover:bg-[#48286e] px-4 py-3 rounded-md transition-colors"
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
>
|
>
|
||||||
Register
|
Register
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
987
src/components/WordPressImportWizard.js
Normal file
987
src/components/WordPressImportWizard.js
Normal file
@@ -0,0 +1,987 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from './ui/dialog';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { Card } from './ui/card';
|
||||||
|
import { Badge } from './ui/badge';
|
||||||
|
import { Checkbox } from './ui/checkbox';
|
||||||
|
import { Input } from './ui/input';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table';
|
||||||
|
import { Progress } from './ui/progress';
|
||||||
|
import { Alert, AlertDescription } from './ui/alert';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import api from '../utils/api';
|
||||||
|
import {
|
||||||
|
Upload,
|
||||||
|
FileCheck,
|
||||||
|
CheckCircle,
|
||||||
|
Eye,
|
||||||
|
Play,
|
||||||
|
CheckCircle2,
|
||||||
|
AlertCircle,
|
||||||
|
AlertTriangle,
|
||||||
|
Trash2,
|
||||||
|
FileDown,
|
||||||
|
Users,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
Loader2
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WordPress Import Wizard Component
|
||||||
|
*
|
||||||
|
* A comprehensive 6-step wizard for importing WordPress users to LOAF platform.
|
||||||
|
* Features:
|
||||||
|
* - CSV upload and analysis
|
||||||
|
* - Interactive status review and adjustment
|
||||||
|
* - Preview before import
|
||||||
|
* - Real-time import progress
|
||||||
|
* - Full rollback capability
|
||||||
|
* - Error reporting
|
||||||
|
*/
|
||||||
|
export default function WordPressImportWizard({ open, onOpenChange, onSuccess }) {
|
||||||
|
// Wizard state
|
||||||
|
const [currentStep, setCurrentStep] = useState(1);
|
||||||
|
const [importJobId, setImportJobId] = useState(null);
|
||||||
|
|
||||||
|
// Data state
|
||||||
|
const [uploadedFile, setUploadedFile] = useState(null);
|
||||||
|
const [analysisResult, setAnalysisResult] = useState(null);
|
||||||
|
const [previewData, setPreviewData] = useState([]);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
|
|
||||||
|
// Override state
|
||||||
|
const [statusOverrides, setStatusOverrides] = useState({});
|
||||||
|
const [selectedRows, setSelectedRows] = useState(new Set());
|
||||||
|
|
||||||
|
// Import execution state
|
||||||
|
const [importing, setImporting] = useState(false);
|
||||||
|
const [importProgress, setImportProgress] = useState(0);
|
||||||
|
const [importResults, setImportResults] = useState(null);
|
||||||
|
|
||||||
|
// UI state
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// Step definitions
|
||||||
|
const steps = [
|
||||||
|
{ number: 1, title: 'Upload CSV', icon: Upload },
|
||||||
|
{ number: 2, title: 'Field Mapping', icon: FileCheck },
|
||||||
|
{ number: 3, title: 'Review Status', icon: CheckCircle },
|
||||||
|
{ number: 4, title: 'Preview', icon: Eye },
|
||||||
|
{ number: 5, title: 'Execute', icon: Play },
|
||||||
|
{ number: 6, title: 'Results', icon: CheckCircle2 }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Reset wizard state when dialog opens/closes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setTimeout(() => {
|
||||||
|
setCurrentStep(1);
|
||||||
|
setImportJobId(null);
|
||||||
|
setUploadedFile(null);
|
||||||
|
setAnalysisResult(null);
|
||||||
|
setPreviewData([]);
|
||||||
|
setStatusOverrides({});
|
||||||
|
setSelectedRows(new Set());
|
||||||
|
setImporting(false);
|
||||||
|
setImportProgress(0);
|
||||||
|
setImportResults(null);
|
||||||
|
}, 300); // Wait for dialog close animation
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Step Navigation
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const canProceed = () => {
|
||||||
|
switch (currentStep) {
|
||||||
|
case 1:
|
||||||
|
return uploadedFile && analysisResult;
|
||||||
|
case 2:
|
||||||
|
return true; // Field mapping auto-detected
|
||||||
|
case 3:
|
||||||
|
return true; // Status review is optional
|
||||||
|
case 4:
|
||||||
|
return true; // Preview is informational
|
||||||
|
case 5:
|
||||||
|
return !importing;
|
||||||
|
case 6:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
if (currentStep < 6 && canProceed()) {
|
||||||
|
if (currentStep === 3) {
|
||||||
|
// Load preview data when moving from step 3 to 4
|
||||||
|
loadPreviewData(1);
|
||||||
|
}
|
||||||
|
setCurrentStep(currentStep + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
if (currentStep > 1) {
|
||||||
|
setCurrentStep(currentStep - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Step 1: Upload CSV
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const handleFileSelect = (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
if (!file.name.endsWith('.csv')) {
|
||||||
|
toast.error('Please upload a CSV file');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setUploadedFile(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpload = async () => {
|
||||||
|
if (!uploadedFile) return;
|
||||||
|
|
||||||
|
setUploading(true);
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', uploadedFile);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.post('/admin/import/upload-csv', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
});
|
||||||
|
|
||||||
|
setImportJobId(response.data.import_job_id);
|
||||||
|
setAnalysisResult(response.data);
|
||||||
|
toast.success(`CSV analyzed: ${response.data.valid_rows} valid rows, ${response.data.warnings} warnings`);
|
||||||
|
|
||||||
|
// Auto-advance to next step
|
||||||
|
setTimeout(() => setCurrentStep(2), 500);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error.response?.data?.detail || 'Failed to upload CSV');
|
||||||
|
} finally {
|
||||||
|
setUploading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Step 3: Review & Adjust Status
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const loadPreviewData = async (page = 1) => {
|
||||||
|
if (!importJobId) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await api.get(`/admin/import/${importJobId}/preview`, {
|
||||||
|
params: { page, page_size: 50 }
|
||||||
|
});
|
||||||
|
|
||||||
|
setPreviewData(response.data.rows);
|
||||||
|
setCurrentPage(response.data.page);
|
||||||
|
setTotalPages(response.data.total_pages);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to load preview data');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentStep === 3 && importJobId && previewData.length === 0) {
|
||||||
|
loadPreviewData(1);
|
||||||
|
}
|
||||||
|
}, [currentStep, importJobId]);
|
||||||
|
|
||||||
|
const handleStatusOverride = (rowNum, status) => {
|
||||||
|
setStatusOverrides(prev => ({
|
||||||
|
...prev,
|
||||||
|
[rowNum]: { status }
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBulkStatusChange = (status) => {
|
||||||
|
const newOverrides = { ...statusOverrides };
|
||||||
|
selectedRows.forEach(rowNum => {
|
||||||
|
newOverrides[rowNum] = { status };
|
||||||
|
});
|
||||||
|
setStatusOverrides(newOverrides);
|
||||||
|
toast.success(`Updated ${selectedRows.size} users to ${status}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleRowSelection = (rowNum) => {
|
||||||
|
setSelectedRows(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
if (newSet.has(rowNum)) {
|
||||||
|
newSet.delete(rowNum);
|
||||||
|
} else {
|
||||||
|
newSet.add(rowNum);
|
||||||
|
}
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSelectAll = () => {
|
||||||
|
if (selectedRows.size === previewData.length) {
|
||||||
|
setSelectedRows(new Set());
|
||||||
|
} else {
|
||||||
|
setSelectedRows(new Set(previewData.map(row => row.row_number)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Step 5: Execute Import
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const handleExecuteImport = async () => {
|
||||||
|
setImporting(true);
|
||||||
|
setCurrentStep(5);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Start import
|
||||||
|
const response = await api.post(`/admin/import/${importJobId}/execute`, {
|
||||||
|
overrides: statusOverrides,
|
||||||
|
options: {
|
||||||
|
send_password_emails: true,
|
||||||
|
skip_errors: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setImportResults(response.data);
|
||||||
|
toast.success(`Import completed: ${response.data.successful_rows} users imported`);
|
||||||
|
|
||||||
|
// Move to results step
|
||||||
|
setCurrentStep(6);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error.response?.data?.detail || 'Import failed');
|
||||||
|
} finally {
|
||||||
|
setImporting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Poll for import progress
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentStep === 5 && importing && importJobId) {
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const response = await api.get(`/admin/import/${importJobId}/status`);
|
||||||
|
setImportProgress(response.data.progress_percent);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch import status:', error);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, [currentStep, importing, importJobId]);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Step 6: Rollback
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const [rollbackConfirmOpen, setRollbackConfirmOpen] = useState(false);
|
||||||
|
const [confirmText, setConfirmText] = useState('');
|
||||||
|
|
||||||
|
const handleRollback = async () => {
|
||||||
|
try {
|
||||||
|
await api.post(`/admin/import/${importJobId}/rollback`, { confirm: true });
|
||||||
|
toast.success(`Rolled back ${importResults.successful_rows} users`);
|
||||||
|
onOpenChange(false);
|
||||||
|
if (onSuccess) onSuccess();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error.response?.data?.detail || 'Rollback failed');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadErrors = async () => {
|
||||||
|
try {
|
||||||
|
const response = await api.get(`/admin/import/${importJobId}/errors/download`, {
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.setAttribute('download', `import_errors_${importJobId}.csv`);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
|
||||||
|
toast.success('Error report downloaded');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to download error report');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Status Badge Component
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const StatusBadge = ({ status }) => {
|
||||||
|
const colors = {
|
||||||
|
active: 'bg-green-100 text-green-800 border-green-300',
|
||||||
|
pre_validated: 'bg-blue-100 text-blue-800 border-blue-300',
|
||||||
|
payment_pending: 'bg-yellow-100 text-yellow-800 border-yellow-300',
|
||||||
|
inactive: 'bg-gray-100 text-gray-800 border-gray-300'
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge variant="outline" className={colors[status] || 'bg-gray-100 text-gray-800'}>
|
||||||
|
{status.replace('_', ' ')}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Render Step Content
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const renderStepContent = () => {
|
||||||
|
switch (currentStep) {
|
||||||
|
case 1:
|
||||||
|
return <Step1Upload />;
|
||||||
|
case 2:
|
||||||
|
return <Step2FieldMapping />;
|
||||||
|
case 3:
|
||||||
|
return <Step3ReviewStatus />;
|
||||||
|
case 4:
|
||||||
|
return <Step4Preview />;
|
||||||
|
case 5:
|
||||||
|
return <Step5Execute />;
|
||||||
|
case 6:
|
||||||
|
return <Step6Results />;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Step 1: Upload CSV
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const Step1Upload = () => (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-[#422268] mb-2">Upload WordPress CSV Export</h3>
|
||||||
|
<p className="text-sm text-[#664fa3]">
|
||||||
|
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-[#ddd8eb] bg-[#f9f5ff]">
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<Upload className="h-12 w-12 text-[#664fa3]" />
|
||||||
|
<div className="text-center">
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
accept=".csv"
|
||||||
|
onChange={handleFileSelect}
|
||||||
|
className="max-w-xs"
|
||||||
|
/>
|
||||||
|
{uploadedFile && (
|
||||||
|
<p className="text-sm text-[#664fa3] mt-2">
|
||||||
|
Selected: {uploadedFile.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{uploadedFile && !analysisResult && (
|
||||||
|
<Button
|
||||||
|
onClick={handleUpload}
|
||||||
|
disabled={uploading}
|
||||||
|
className="w-full bg-[#664fa3] hover:bg-[#422268]"
|
||||||
|
>
|
||||||
|
{uploading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Analyzing CSV...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Upload className="mr-2 h-4 w-4" />
|
||||||
|
Upload and Analyze
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{analysisResult && (
|
||||||
|
<Card className="p-6 bg-green-50 border-green-200">
|
||||||
|
<h4 className="font-semibold text-green-900 mb-4">Analysis Complete</h4>
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-green-700">Total Rows</p>
|
||||||
|
<p className="text-2xl font-semibold text-green-900">{analysisResult.total_rows}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-green-700">Valid Rows</p>
|
||||||
|
<p className="text-2xl font-semibold text-green-900">{analysisResult.valid_rows}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-yellow-700">Warnings</p>
|
||||||
|
<p className="text-2xl font-semibold text-yellow-900">{analysisResult.warnings}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-red-700">Errors</p>
|
||||||
|
<p className="text-2xl font-semibold text-red-900">{analysisResult.errors}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{analysisResult.data_quality && (
|
||||||
|
<div className="mt-4 pt-4 border-t border-green-300">
|
||||||
|
<h5 className="text-sm font-semibold text-green-900 mb-2">Data Quality Issues:</h5>
|
||||||
|
<ul className="text-sm text-green-800 space-y-1">
|
||||||
|
{analysisResult.data_quality.invalid_dob > 0 && (
|
||||||
|
<li>• {analysisResult.data_quality.invalid_dob} invalid dates of birth</li>
|
||||||
|
)}
|
||||||
|
{analysisResult.data_quality.missing_phone > 0 && (
|
||||||
|
<li>• {analysisResult.data_quality.missing_phone} missing phone numbers</li>
|
||||||
|
)}
|
||||||
|
{analysisResult.data_quality.duplicate_email > 0 && (
|
||||||
|
<li>• {analysisResult.data_quality.duplicate_email} duplicate emails</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Step 2: Field Mapping
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const Step2FieldMapping = () => (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-[#422268] mb-2">Field Mapping</h3>
|
||||||
|
<p className="text-sm text-[#664fa3]">
|
||||||
|
WordPress fields have been automatically mapped to LOAF platform fields.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>WordPress Field</TableHead>
|
||||||
|
<TableHead>→</TableHead>
|
||||||
|
<TableHead>LOAF Field</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className="font-mono text-sm">user_email</TableCell>
|
||||||
|
<TableCell>→</TableCell>
|
||||||
|
<TableCell className="font-mono text-sm">email</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className="font-mono text-sm">first_name</TableCell>
|
||||||
|
<TableCell>→</TableCell>
|
||||||
|
<TableCell className="font-mono text-sm">first_name</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className="font-mono text-sm">last_name</TableCell>
|
||||||
|
<TableCell>→</TableCell>
|
||||||
|
<TableCell className="font-mono text-sm">last_name</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className="font-mono text-sm">cell_phone</TableCell>
|
||||||
|
<TableCell>→</TableCell>
|
||||||
|
<TableCell className="font-mono text-sm">phone</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className="font-mono text-sm">date_of_birth</TableCell>
|
||||||
|
<TableCell>→</TableCell>
|
||||||
|
<TableCell className="font-mono text-sm">date_of_birth</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className="font-mono text-sm">wp_capabilities</TableCell>
|
||||||
|
<TableCell>→</TableCell>
|
||||||
|
<TableCell className="font-mono text-sm">role + status</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Alert className="bg-blue-50 border-blue-200">
|
||||||
|
<AlertCircle className="h-4 w-4 text-blue-600" />
|
||||||
|
<AlertDescription className="text-blue-800">
|
||||||
|
WordPress roles will be automatically converted:
|
||||||
|
<ul className="mt-2 ml-4 space-y-1">
|
||||||
|
<li>• <code>loaf_admin</code> → admin (active)</li>
|
||||||
|
<li>• <code>loaf_treasure</code> → finance (active)</li>
|
||||||
|
<li>• <code>administrator</code> → superadmin (active)</li>
|
||||||
|
<li>• <code>pms_subscription_plan_63</code> → member</li>
|
||||||
|
</ul>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Step 3: Review & Adjust Status (KEY FEATURE)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const Step3ReviewStatus = () => (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-[#422268] mb-2">Review & Adjust User Status</h3>
|
||||||
|
<p className="text-sm text-[#664fa3]">
|
||||||
|
Review suggested status mappings and override as needed before import.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bulk edit toolbar */}
|
||||||
|
<Card className="p-4 bg-[#f9f5ff] border-[#ddd8eb]">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedRows.size === previewData.length && previewData.length > 0}
|
||||||
|
onCheckedChange={toggleSelectAll}
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-[#664fa3] font-medium">
|
||||||
|
{selectedRows.size > 0 ? `${selectedRows.size} selected` : 'Select all'}
|
||||||
|
</span>
|
||||||
|
{selectedRows.size > 0 && (
|
||||||
|
<Select onValueChange={handleBulkStatusChange}>
|
||||||
|
<SelectTrigger className="w-48">
|
||||||
|
<SelectValue placeholder="Change status to..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="active">Active</SelectItem>
|
||||||
|
<SelectItem value="pre_validated">Pre-Validated</SelectItem>
|
||||||
|
<SelectItem value="payment_pending">Payment Pending</SelectItem>
|
||||||
|
<SelectItem value="inactive">Inactive</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Data table */}
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-[#664fa3]" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="border rounded-lg overflow-hidden">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="bg-[#f9f5ff]">
|
||||||
|
<TableHead className="w-12">
|
||||||
|
<Checkbox checked={false} />
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>Row</TableHead>
|
||||||
|
<TableHead>Email</TableHead>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>WP Role</TableHead>
|
||||||
|
<TableHead>Suggested Status</TableHead>
|
||||||
|
<TableHead>Override Status</TableHead>
|
||||||
|
<TableHead>Issues</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{previewData.map((row) => (
|
||||||
|
<TableRow key={row.row_number}>
|
||||||
|
<TableCell>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedRows.has(row.row_number)}
|
||||||
|
onCheckedChange={() => toggleRowSelection(row.row_number)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-mono text-sm">{row.row_number}</TableCell>
|
||||||
|
<TableCell className="text-sm">{row.email}</TableCell>
|
||||||
|
<TableCell className="text-sm">
|
||||||
|
{row.first_name} {row.last_name}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge className="bg-[#ddd8eb] text-[#422268]">
|
||||||
|
{row.wordpress_roles?.join(', ') || 'N/A'}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<StatusBadge status={row.suggested_status} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Select
|
||||||
|
value={statusOverrides[row.row_number]?.status || row.suggested_status}
|
||||||
|
onValueChange={(value) => handleStatusOverride(row.row_number, value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-40">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="active">Active</SelectItem>
|
||||||
|
<SelectItem value="pre_validated">Pre-Validated</SelectItem>
|
||||||
|
<SelectItem value="payment_pending">Payment Pending</SelectItem>
|
||||||
|
<SelectItem value="inactive">Inactive</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{row.warnings?.map((warning, idx) => (
|
||||||
|
<Badge
|
||||||
|
key={idx}
|
||||||
|
variant="outline"
|
||||||
|
className="text-orange-600 border-orange-300 mr-1"
|
||||||
|
>
|
||||||
|
<AlertCircle className="h-3 w-3 mr-1" />
|
||||||
|
{warning}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-sm text-[#664fa3]">
|
||||||
|
Page {currentPage} of {totalPages}
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => loadPreviewData(currentPage - 1)}
|
||||||
|
disabled={currentPage === 1 || loading}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => loadPreviewData(currentPage + 1)}
|
||||||
|
disabled={currentPage === totalPages || loading}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Step 4: Preview
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const Step4Preview = () => {
|
||||||
|
const overrideCount = Object.keys(statusOverrides).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-[#422268] mb-2">Import Preview</h3>
|
||||||
|
<p className="text-sm text-[#664fa3]">
|
||||||
|
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-[#664fa3]">Total Users</p>
|
||||||
|
<p className="text-3xl font-semibold text-[#422268]">{analysisResult?.total_rows}</p>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-6">
|
||||||
|
<p className="text-sm text-[#664fa3]">Status Overrides</p>
|
||||||
|
<p className="text-3xl font-semibold text-[#422268]">{overrideCount}</p>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-6">
|
||||||
|
<p className="text-sm text-[#664fa3]">Expected Imports</p>
|
||||||
|
<p className="text-3xl font-semibold text-[#422268]">{analysisResult?.valid_rows}</p>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<h4 className="font-semibold text-[#422268] 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-[#664fa3]">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-[#664fa3]">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-[#664fa3]">Full rollback capability available after import</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{overrideCount > 0 && (
|
||||||
|
<Alert className="bg-yellow-50 border-yellow-200">
|
||||||
|
<AlertCircle className="h-4 w-4 text-yellow-600" />
|
||||||
|
<AlertDescription className="text-yellow-800">
|
||||||
|
You have overridden {overrideCount} user status{overrideCount > 1 ? 'es' : ''}.
|
||||||
|
These will be applied during import.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Step 5: Execute
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const Step5Execute = () => (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-[#422268] mb-2">
|
||||||
|
{importing ? 'Import in Progress...' : 'Ready to Import'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-[#664fa3]">
|
||||||
|
{importing
|
||||||
|
? 'Please wait while users are imported. This may take a few minutes.'
|
||||||
|
: 'Click "Start Import" to begin importing users.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{importing && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Progress value={importProgress} className="w-full" />
|
||||||
|
<p className="text-center text-sm text-[#664fa3]">
|
||||||
|
{importProgress.toFixed(1)}% complete
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!importing && !importResults && (
|
||||||
|
<Button
|
||||||
|
onClick={handleExecuteImport}
|
||||||
|
className="w-full bg-[#664fa3] hover:bg-[#422268] py-6 text-lg"
|
||||||
|
>
|
||||||
|
<Play className="mr-2 h-5 w-5" />
|
||||||
|
Start Import
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Step 6: Results & Rollback
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const Step6Results = () => (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-[#422268] mb-2">Import Complete</h3>
|
||||||
|
<p className="text-sm text-[#664fa3]">
|
||||||
|
Review the import results and download error reports if needed.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats cards */}
|
||||||
|
<div className="grid md:grid-cols-3 gap-4">
|
||||||
|
<Card className="p-6 bg-green-50 border-green-200">
|
||||||
|
<p className="text-sm text-green-700">Successful Imports</p>
|
||||||
|
<p className="text-4xl font-semibold text-green-900">{importResults?.successful_rows || 0}</p>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-6 bg-red-50 border-red-200">
|
||||||
|
<p className="text-sm text-red-700">Failed Imports</p>
|
||||||
|
<p className="text-4xl font-semibold text-red-900">{importResults?.failed_rows || 0}</p>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-6 bg-blue-50 border-blue-200">
|
||||||
|
<p className="text-sm text-blue-700">Password Emails Sent</p>
|
||||||
|
<p className="text-4xl font-semibold text-blue-900">{importResults?.password_emails_queued || 0}</p>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div className="flex gap-4 justify-between flex-wrap">
|
||||||
|
<div className="flex gap-4 flex-wrap">
|
||||||
|
{importResults?.failed_rows > 0 && (
|
||||||
|
<Button onClick={handleDownloadErrors} variant="outline">
|
||||||
|
<FileDown className="h-4 w-4 mr-2" />
|
||||||
|
Download Error Report
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
onOpenChange(false);
|
||||||
|
if (onSuccess) onSuccess();
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<Users className="h-4 w-4 mr-2" />
|
||||||
|
View Imported Members
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Rollback button (prominent, red) */}
|
||||||
|
{importResults?.successful_rows > 0 && (
|
||||||
|
<Button
|
||||||
|
onClick={() => setRollbackConfirmOpen(true)}
|
||||||
|
variant="destructive"
|
||||||
|
className="bg-red-600 hover:bg-red-700"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
Rollback Import ({importResults.successful_rows} users)
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Rollback confirmation dialog */}
|
||||||
|
<Dialog open={rollbackConfirmOpen} onOpenChange={setRollbackConfirmOpen}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<div className="p-3 bg-red-100 rounded-full">
|
||||||
|
<AlertTriangle className="h-6 w-6 text-red-600" />
|
||||||
|
</div>
|
||||||
|
<DialogTitle className="text-2xl font-semibold text-[#422268]">
|
||||||
|
Confirm Rollback
|
||||||
|
</DialogTitle>
|
||||||
|
</div>
|
||||||
|
<DialogDescription className="text-[#664fa3]">
|
||||||
|
This will permanently delete{' '}
|
||||||
|
<strong>{importResults?.successful_rows} users</strong> that were imported.
|
||||||
|
This action cannot be undone.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4 my-4">
|
||||||
|
<p className="text-sm text-red-800 font-medium mb-2">
|
||||||
|
Type "DELETE {importResults?.successful_rows} USERS" to confirm:
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
value={confirmText}
|
||||||
|
onChange={(e) => setConfirmText(e.target.value)}
|
||||||
|
className="mt-2"
|
||||||
|
placeholder={`DELETE ${importResults?.successful_rows} USERS`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button onClick={() => setRollbackConfirmOpen(false)} variant="outline">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleRollback}
|
||||||
|
disabled={confirmText !== `DELETE ${importResults?.successful_rows} USERS`}
|
||||||
|
className="bg-red-600 hover:bg-red-700"
|
||||||
|
>
|
||||||
|
Yes, Delete {importResults?.successful_rows} Users
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Main Render
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-2xl font-semibold text-[#422268]">
|
||||||
|
WordPress Import Wizard
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-[#664fa3]">
|
||||||
|
Import WordPress users with interactive status review and full rollback capability
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* Step indicator */}
|
||||||
|
<div className="flex items-center justify-between mb-6 px-4">
|
||||||
|
{steps.map((step, index) => {
|
||||||
|
const StepIcon = step.icon;
|
||||||
|
const isCompleted = currentStep > step.number;
|
||||||
|
const isCurrent = currentStep === step.number;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={step.number}>
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
w-10 h-10 rounded-full flex items-center justify-center
|
||||||
|
${isCurrent ? 'bg-[#664fa3] text-white' : ''}
|
||||||
|
${isCompleted ? 'bg-green-600 text-white' : ''}
|
||||||
|
${!isCurrent && !isCompleted ? 'bg-gray-200 text-gray-600' : ''}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{isCompleted ? (
|
||||||
|
<CheckCircle className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<StepIcon className="h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className={`text-xs mt-1 ${isCurrent ? 'font-semibold text-[#422268]' : 'text-gray-600'}`}>
|
||||||
|
{step.title}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{index < steps.length - 1 && (
|
||||||
|
<div className={`flex-1 h-0.5 mx-2 ${isCompleted ? 'bg-green-600' : 'bg-gray-300'}`} />
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step content */}
|
||||||
|
<div className="py-6">
|
||||||
|
{renderStepContent()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation footer */}
|
||||||
|
<DialogFooter className="flex justify-between items-center">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleBack}
|
||||||
|
disabled={currentStep === 1 || importing}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4 mr-2" />
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{currentStep < 5 && (
|
||||||
|
<Button
|
||||||
|
onClick={handleNext}
|
||||||
|
disabled={!canProceed()}
|
||||||
|
className="bg-[#664fa3] hover:bg-[#422268]"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
<ChevronRight className="h-4 w-4 ml-2" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentStep === 6 && (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
onOpenChange(false);
|
||||||
|
if (onSuccess) onSuccess();
|
||||||
|
}}
|
||||||
|
className="bg-[#664fa3] hover:bg-[#422268]"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -86,7 +86,7 @@ const RegistrationStep4 = ({ formData, handleInputChange }) => {
|
|||||||
<label htmlFor="accepts_tos" className="text-sm text-gray-700" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<label htmlFor="accepts_tos" className="text-sm text-gray-700" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
I agree to the{' '}
|
I agree to the{' '}
|
||||||
<a
|
<a
|
||||||
href="/membership/terms-of-service"
|
href="/become-a-member/terms-of-service"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-[#664fa3] hover:text-[#422268] font-semibold underline"
|
className="text-[#664fa3] hover:text-[#422268] font-semibold underline"
|
||||||
@@ -95,7 +95,7 @@ const RegistrationStep4 = ({ formData, handleInputChange }) => {
|
|||||||
</a>
|
</a>
|
||||||
{' '}and{' '}
|
{' '}and{' '}
|
||||||
<a
|
<a
|
||||||
href="/membership/privacy-policy"
|
href="become-a-member/privacy-policy"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-[#664fa3] hover:text-[#422268] font-semibold underline"
|
className="text-[#664fa3] hover:text-[#422268] font-semibold underline"
|
||||||
|
|||||||
@@ -1,31 +1,33 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Tabs = TabsPrimitive.Root
|
const Tabs = TabsPrimitive.Root;
|
||||||
|
|
||||||
const TabsList = React.forwardRef(({ className, ...props }, ref) => (
|
const TabsList = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
<TabsPrimitive.List
|
<TabsPrimitive.List
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
"inline-flex h-full items-center justify-center rounded-lg gap-6 p-1 text-muted-foreground",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props} />
|
{...props}
|
||||||
))
|
/>
|
||||||
TabsList.displayName = TabsPrimitive.List.displayName
|
));
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||||
|
|
||||||
const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => (
|
const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
<TabsPrimitive.Trigger
|
<TabsPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
"inline-flex items-center justify-center whitespace-nowrap hover:bg-[#f1eef9] border-2 border-[#664fa3] rounded-2xl px-3 py-1 text-[#664fa3] 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
|
className
|
||||||
)}
|
)}
|
||||||
{...props} />
|
{...props}
|
||||||
))
|
/>
|
||||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
));
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
const TabsContent = React.forwardRef(({ className, ...props }, ref) => (
|
const TabsContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
<TabsPrimitive.Content
|
<TabsPrimitive.Content
|
||||||
@@ -34,8 +36,9 @@ const TabsContent = React.forwardRef(({ className, ...props }, ref) => (
|
|||||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props} />
|
{...props}
|
||||||
))
|
/>
|
||||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
));
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||||
|
|
||||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||||
|
|||||||
@@ -3,7 +3,14 @@ import axios from 'axios';
|
|||||||
|
|
||||||
const AuthContext = createContext();
|
const AuthContext = createContext();
|
||||||
|
|
||||||
const API_URL = process.env.REACT_APP_BACKEND_URL;
|
const API_URL = process.env.REACT_APP_BACKEND_URL || window.location.origin;
|
||||||
|
|
||||||
|
// Log environment on module load for debugging
|
||||||
|
console.log('[AuthContext] Module initialized with:', {
|
||||||
|
REACT_APP_BACKEND_URL: process.env.REACT_APP_BACKEND_URL,
|
||||||
|
REACT_APP_BASENAME: process.env.REACT_APP_BASENAME,
|
||||||
|
API_URL: API_URL
|
||||||
|
});
|
||||||
|
|
||||||
export const AuthProvider = ({ children }) => {
|
export const AuthProvider = ({ children }) => {
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
@@ -54,16 +61,79 @@ export const AuthProvider = ({ children }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const login = async (email, password) => {
|
const login = async (email, password) => {
|
||||||
const response = await axios.post(`${API_URL}/api/auth/login`, { email, password });
|
try {
|
||||||
|
console.log('[AuthContext] Starting login request...', {
|
||||||
|
API_URL: API_URL,
|
||||||
|
envBackendUrl: process.env.REACT_APP_BACKEND_URL,
|
||||||
|
fullUrl: `${API_URL}/api/auth/login`
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
`${API_URL}/api/auth/login`,
|
||||||
|
{ email, password },
|
||||||
|
{
|
||||||
|
timeout: 30000, // 30 second timeout
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('[AuthContext] Login response received:', {
|
||||||
|
status: response.status,
|
||||||
|
hasToken: !!response.data?.access_token,
|
||||||
|
hasUser: !!response.data?.user
|
||||||
|
});
|
||||||
|
|
||||||
const { access_token, user: userData } = response.data;
|
const { access_token, user: userData } = response.data;
|
||||||
|
|
||||||
|
// Store token first
|
||||||
localStorage.setItem('token', access_token);
|
localStorage.setItem('token', access_token);
|
||||||
|
console.log('[AuthContext] Token stored in localStorage');
|
||||||
|
|
||||||
|
// Update state
|
||||||
setToken(access_token);
|
setToken(access_token);
|
||||||
setUser(userData);
|
setUser(userData);
|
||||||
|
console.log('[AuthContext] User state updated:', {
|
||||||
|
email: userData.email,
|
||||||
|
role: userData.role
|
||||||
|
});
|
||||||
|
|
||||||
// Fetch user permissions
|
// Fetch user permissions (don't let this fail the login)
|
||||||
|
// Use setTimeout to defer permission fetching slightly
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
console.log('[AuthContext] Fetching permissions...');
|
||||||
await fetchPermissions(access_token);
|
await fetchPermissions(access_token);
|
||||||
|
console.log('[AuthContext] Permissions fetched successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[AuthContext] Failed to fetch permissions (non-critical):', {
|
||||||
|
message: error.message,
|
||||||
|
response: error.response?.data,
|
||||||
|
status: error.response?.status
|
||||||
|
});
|
||||||
|
// Don't throw - permissions can be fetched later if needed
|
||||||
|
}
|
||||||
|
}, 100); // Small delay to ensure state is settled
|
||||||
|
|
||||||
return userData;
|
return userData;
|
||||||
|
} catch (error) {
|
||||||
|
// Enhanced error logging
|
||||||
|
console.error('[AuthContext] Login failed:', {
|
||||||
|
message: error.message,
|
||||||
|
response: error.response?.data,
|
||||||
|
status: error.response?.status,
|
||||||
|
code: error.code,
|
||||||
|
config: {
|
||||||
|
url: error.config?.url,
|
||||||
|
method: error.config?.method,
|
||||||
|
timeout: error.config?.timeout
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-throw to let Login component handle the error
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
|
@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 base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family:
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||||
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
|
||||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||||
sans-serif;
|
sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
@@ -13,8 +16,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family:
|
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||||
source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
|
monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
@@ -113,3 +116,27 @@ code {
|
|||||||
border-bottom-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ const AcceptInvitation = () => {
|
|||||||
const [invitation, setInvitation] = useState(null);
|
const [invitation, setInvitation] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [success, setSuccess] = useState(false);
|
||||||
|
const [successUser, setSuccessUser] = useState(null);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
password: '',
|
password: '',
|
||||||
@@ -134,19 +136,23 @@ const AcceptInvitation = () => {
|
|||||||
const { access_token, user } = response.data;
|
const { access_token, user } = response.data;
|
||||||
localStorage.setItem('token', access_token);
|
localStorage.setItem('token', access_token);
|
||||||
|
|
||||||
toast.success('Welcome to LOAF! Your account has been created successfully.');
|
|
||||||
|
|
||||||
// Call login to update auth context
|
// Call login to update auth context
|
||||||
if (login) {
|
if (login) {
|
||||||
await login(invitation.email, formData.password);
|
await login(invitation.email, formData.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect based on role
|
// Show success state
|
||||||
|
setSuccessUser(user);
|
||||||
|
setSuccess(true);
|
||||||
|
|
||||||
|
// Auto-redirect after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
if (user.role === 'admin' || user.role === 'superadmin') {
|
if (user.role === 'admin' || user.role === 'superadmin') {
|
||||||
navigate('/admin/dashboard');
|
navigate('/admin');
|
||||||
} else {
|
} else {
|
||||||
navigate('/dashboard');
|
navigate('/dashboard');
|
||||||
}
|
}
|
||||||
|
}, 3000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error.response?.data?.detail || 'Failed to accept invitation';
|
const errorMessage = error.response?.data?.detail || 'Failed to accept invitation';
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
@@ -206,6 +212,83 @@ const AcceptInvitation = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
const redirectPath = successUser?.role === 'admin' || successUser?.role === 'superadmin' ? '/admin' : '/dashboard';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-[#F9F8FB] to-white flex items-center justify-center p-4">
|
||||||
|
<Card className="w-full max-w-2xl p-12 bg-white rounded-2xl border border-[#ddd8eb] text-center">
|
||||||
|
{/* Success Animation */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="h-24 w-24 mx-auto rounded-full bg-gradient-to-br from-[#81B29A] to-[#6DA085] 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-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Welcome to LOAF! 🎉
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-[#664fa3] 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-[#DDD8EB] to-[#F9F8FB] rounded-xl">
|
||||||
|
<div className="grid md:grid-cols-2 gap-4 text-left">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-[#664fa3] mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Name
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
{successUser?.first_name} {successUser?.last_name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-[#664fa3] mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Email
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
{successUser?.email}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-[#664fa3] mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Role
|
||||||
|
</p>
|
||||||
|
<div>{getRoleBadge(successUser?.role)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-[#664fa3] mb-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Status
|
||||||
|
</p>
|
||||||
|
<Badge className="bg-[#81B29A] text-white px-4 py-2 rounded-full text-sm">
|
||||||
|
{successUser?.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Redirect Info */}
|
||||||
|
<div className="mb-8 p-4 bg-blue-50 border border-blue-200 rounded-xl">
|
||||||
|
<p className="text-sm text-blue-800" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
<Loader2 className="h-4 w-4 inline mr-2 animate-spin" />
|
||||||
|
Redirecting you to your dashboard in 3 seconds...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Manual Continue Button */}
|
||||||
|
<Button
|
||||||
|
onClick={() => navigate(redirectPath)}
|
||||||
|
className="w-full h-14 rounded-xl bg-gradient-to-r from-[#81B29A] to-[#6DA085] hover:from-[#6DA085] hover:to-[#5A8F72] text-white text-lg font-semibold"
|
||||||
|
>
|
||||||
|
Continue to Dashboard
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-[#F9F8FB] to-white flex items-center justify-center p-4">
|
<div className="min-h-screen bg-gradient-to-br from-[#F9F8FB] to-white flex items-center justify-center p-4">
|
||||||
<Card className="w-full max-w-3xl p-8 md:p-12 bg-white rounded-2xl border border-[#ddd8eb]">
|
<Card className="w-full max-w-3xl p-8 md:p-12 bg-white rounded-2xl border border-[#ddd8eb]">
|
||||||
|
|||||||
@@ -14,22 +14,20 @@ const BecomeMember = () => {
|
|||||||
const imgIconAdminFee5 = `${process.env.PUBLIC_URL}/imgIconAdminFee5.png`;
|
const imgIconAdminFee5 = `${process.env.PUBLIC_URL}/imgIconAdminFee5.png`;
|
||||||
const imgShootingStar = `${process.env.PUBLIC_URL}/imgShootingStar.png`;
|
const imgShootingStar = `${process.env.PUBLIC_URL}/imgShootingStar.png`;
|
||||||
|
|
||||||
|
const Arrow = ({ ...props }) => (
|
||||||
|
<div className="flex justify-center mb-2">
|
||||||
|
<ArrowDown className="size-8 text-[#4f378a] font-bold" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 relative">
|
<div className="min-h-screen bg-gray-50 relative">
|
||||||
<PublicNavbar />
|
<PublicNavbar />
|
||||||
|
|
||||||
{/* Decorative shooting star element */}
|
|
||||||
<div className="hidden lg:block absolute left-[88px] top-[974px] w-[195px] h-[1135px] pointer-events-none opacity-50">
|
|
||||||
<img
|
|
||||||
src={imgShootingStar}
|
|
||||||
alt=""
|
|
||||||
className="w-full h-full object-contain"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<div className="relative bg-gray-50 pt-20 pb-24">
|
<div className="relative bg-gray-50 pt-20 px-6 pb-16">
|
||||||
<div className="max-w-7xl mx-auto px-6 text-center">
|
<div className="max-w-7xl mx-auto text-center">
|
||||||
<h1
|
<h1
|
||||||
className="text-3xl sm:text-4xl md:text-5xl font-bold text-[#48286e] mb-6 sm:mb-8 leading-[1.2] tracking-[-0.96px]"
|
className="text-3xl sm:text-4xl md:text-5xl font-bold text-[#48286e] mb-6 sm:mb-8 leading-[1.2] tracking-[-0.96px]"
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||||
@@ -46,7 +44,7 @@ const BecomeMember = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Annual Administrative Fees Section */}
|
{/* Annual Administrative Fees Section */}
|
||||||
<div className="max-w-[1340px] mx-auto px-6 mb-12 sm:mb-16">
|
<div className="max-w-[1340px] z-10 mx-auto px-6 mb-12 sm:mb-16">
|
||||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-5 items-center">
|
<div className="flex flex-col sm:flex-row gap-4 sm:gap-5 items-center">
|
||||||
<div className="w-24 h-24 sm:w-32 sm:h-32 md:w-[153px] md:h-[138px] flex-shrink-0">
|
<div className="w-24 h-24 sm:w-32 sm:h-32 md:w-[153px] md:h-[138px] flex-shrink-0">
|
||||||
<img
|
<img
|
||||||
@@ -73,27 +71,35 @@ const BecomeMember = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Membership Process Section */}
|
{/* Membership Process Section */}
|
||||||
<div className="relative bg-gray-50 py-16">
|
<div className="relative bg-gray-50 py-32 bg-gradient-to-bl from-[#F9FAFB] to-[#DDD8EB] ">
|
||||||
<div className="max-w-7xl mx-auto px-6 text-center">
|
{/* Decorative shooting star element */}
|
||||||
|
<div className="hidden lg:block absolute left-0 top-64 w-[195px] h-[1130px] pointer-events-none opacity-50">
|
||||||
|
<img
|
||||||
|
src={imgShootingStar}
|
||||||
|
alt=""
|
||||||
|
className="w-full h-full z-20 object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="max-w-7xl mx-auto px-6 mb-24 text-center">
|
||||||
<h2
|
<h2
|
||||||
className="text-2xl sm:text-3xl md:text-4xl font-bold text-[#48286e] mb-6 sm:mb-8 leading-[1.2] tracking-[-0.8px]"
|
className="text-2xl sm:text-3xl md:text-[40px] font-bold text-[#48286e] mb-6 sm:mb-8 leading-[1.2] tracking-[-0.8px]"
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||||
>
|
>
|
||||||
Membership Process
|
Membership Process
|
||||||
</h2>
|
</h2>
|
||||||
<p
|
<p
|
||||||
className="text-base sm:text-lg font-medium text-[#48286e] max-w-2xl mx-auto leading-[1.6]"
|
className="text-base sm:text-lg md:text-xl lg:text-2xl lg:font-semibold font-medium text-[#48286e] max-w-2xl mx-auto leading-[1.6]"
|
||||||
style={{ fontFamily: "'Nunito Sans', sans-serif", fontVariationSettings: "'YTLC' 500, 'wdth' 100" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif", fontVariationSettings: "'YTLC' 500, 'wdth' 100" }}
|
||||||
>
|
>
|
||||||
Becoming a member is easy, but for the safety and privacy of our membership, there are a few steps:
|
Becoming a member is easy, but for the safety and privacy of our membership, there are a few steps:
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Step 1 */}
|
{/* Step 1 */}
|
||||||
<div className="max-w-[1340px] mx-auto px-6 mb-6 sm:mb-8">
|
<div className="max-w-[1340px] mx-auto px-6 mb-2 ">
|
||||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-5 items-center">
|
<div className="flex flex-col sm:flex-row gap-4 sm:gap-5 items-center">
|
||||||
<div className="w-24 h-24 sm:w-32 sm:h-32 md:w-[153px] md:h-[138px] flex-shrink-0">
|
<div className="w-24 h-24 sm:w-32 z-40 sm:h-32 md:w-[153px] md:h-[138px] flex-shrink-0">
|
||||||
<img
|
<img
|
||||||
src={imgIconAdminFee2}
|
src={imgIconAdminFee2}
|
||||||
alt="Step 1 Icon"
|
alt="Step 1 Icon"
|
||||||
@@ -118,14 +124,12 @@ const BecomeMember = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Arrow Down Icon */}
|
{/* Arrow Down Icon */}
|
||||||
<div className="flex justify-center mb-8">
|
<Arrow />
|
||||||
<ArrowDown className="w-8 h-8 text-[#4f378a]" strokeWidth={2} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Step 2 */}
|
{/* Step 2 */}
|
||||||
<div className="max-w-[1340px] mx-auto px-6 mb-6 sm:mb-8">
|
<div className="max-w-[1340px] mx-auto px-6 mb-2">
|
||||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-5 items-center">
|
<div className="flex flex-col sm:flex-row gap-4 sm:gap-5 items-center">
|
||||||
<div className="w-24 h-24 sm:w-32 sm:h-32 md:w-[153px] md:h-[138px] flex-shrink-0">
|
<div className="w-24 h-24 z-40 sm:w-32 sm:h-32 md:w-[153px] md:h-[138px] flex-shrink-0">
|
||||||
<img
|
<img
|
||||||
src={imgIconAdminFee3}
|
src={imgIconAdminFee3}
|
||||||
alt="Step 2 Icon"
|
alt="Step 2 Icon"
|
||||||
@@ -150,14 +154,11 @@ const BecomeMember = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Arrow Down Icon */}
|
{/* Arrow Down Icon */}
|
||||||
<div className="flex justify-center mb-8">
|
<Arrow />
|
||||||
<ArrowDown className="w-8 h-8 text-[#4f378a]" strokeWidth={2} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Step 3 */}
|
{/* Step 3 */}
|
||||||
<div className="max-w-[1340px] mx-auto px-6 mb-6 sm:mb-8">
|
<div className="max-w-[1340px] mx-auto px-6 mb-2">
|
||||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-5 items-center">
|
<div className="flex flex-col sm:flex-row gap-4 sm:gap-5 items-center">
|
||||||
<div className="w-24 h-24 sm:w-32 sm:h-32 md:w-[153px] md:h-[138px] flex-shrink-0">
|
<div className="w-24 h-24 z-40 sm:w-32 sm:h-32 md:w-[153px] md:h-[138px] flex-shrink-0">
|
||||||
<img
|
<img
|
||||||
src={imgIconAdminFee4}
|
src={imgIconAdminFee4}
|
||||||
alt="Step 3 Icon"
|
alt="Step 3 Icon"
|
||||||
@@ -182,14 +183,12 @@ const BecomeMember = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Arrow Down Icon */}
|
{/* Arrow Down Icon */}
|
||||||
<div className="flex justify-center mb-8">
|
<Arrow />
|
||||||
<ArrowDown className="w-8 h-8 text-[#4f378a]" strokeWidth={2} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Step 4 - With Gradient Background */}
|
{/* Step 4 - With Gradient Background */}
|
||||||
<div className="max-w-[1340px] mx-auto px-6 mb-12 sm:mb-16">
|
<div className="max-w-[1340px] mx-auto px-6 mb-2">
|
||||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-5 items-center">
|
<div className="flex flex-col sm:flex-row gap-4 sm:gap-5 items-center">
|
||||||
<div className="w-24 h-24 sm:w-32 sm:h-32 md:w-[153px] md:h-[138px] flex-shrink-0">
|
<div className="w-24 h-24 z-40 sm:w-32 sm:h-32 md:w-[153px] md:h-[138px] flex-shrink-0">
|
||||||
<img
|
<img
|
||||||
src={imgIconAdminFee5}
|
src={imgIconAdminFee5}
|
||||||
alt="Step 4 Icon"
|
alt="Step 4 Icon"
|
||||||
@@ -212,19 +211,19 @@ const BecomeMember = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* CTA Section */}
|
{/* CTA Section */}
|
||||||
<div className="relative bg-gray-50 py-16 ">
|
<div className="relative bg-gray-50 py-16 ">
|
||||||
<div className="max-w-7xl mx-auto px-6 text-center">
|
<div className="max-w-7xl mx-auto px-6 flex flex-col md:flex-row items-center justify-center gap-12 text-center">
|
||||||
<h2
|
<h2
|
||||||
className="text-2xl sm:text-3xl md:text-4xl font-bold text-[#48286e] mb-6 sm:mb-8 leading-[1.2] tracking-[-0.8px]"
|
className="text-2xl sm:text-3xl md:text-[40px] content-center font-bold text-[#48286e] leading-[1.2] tracking-[-0.8px]"
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||||
>
|
>
|
||||||
Ready to Join Us?
|
Ready to Join Us?
|
||||||
</h2>
|
</h2>
|
||||||
<Link to="/register">
|
<Link to="/register">
|
||||||
<Button
|
<Button
|
||||||
className="bg-[#664fa3] text-white hover:bg-[#48286e] rounded-[35px] px-6 py-3 sm:px-12 sm:py-5 md:px-16 md:py-6 text-base sm:text-lg font-medium tracking-[-0.09px] h-auto"
|
className="bg-[#664fa3] text-white hover:bg-[#48286e] rounded-[35px] px-6 py-3 sm:px-12 sm:py-5 md:px-16 md:py-6 text-[19px] sm:text-lg font-medium tracking-[-0.09px] leading-5 h-auto"
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||||
>
|
>
|
||||||
Register Now!
|
Register Now!
|
||||||
|
|||||||
@@ -12,24 +12,78 @@ const BoardOfDirectors = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const boardMembers = [
|
const boardMembers = [
|
||||||
'Danita Cole',
|
{ name: 'Danita Cole', title: 'Director' },
|
||||||
'Roxanne Cherico',
|
{ name: 'Roxanne Cherico', title: 'Director' },
|
||||||
'Lucretia Copeland',
|
{ name: 'Lucretia Copeland', title: 'Director' },
|
||||||
'Julie Fischer'
|
{ name: 'Julie Fischer', title: 'Director' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
const DirectorCards = ({ title, members }) => {
|
||||||
|
return (
|
||||||
|
<section className=" w-full">
|
||||||
|
<div className="mx-auto bg-white rounded-3xl p-10 shadow-lg h-full">
|
||||||
|
{title && (
|
||||||
|
<h2
|
||||||
|
className="text-2xl sm:text-4xl font-bold text-[#48286e] text-center mb-8"
|
||||||
|
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-col-span-1 lg:grid-cols-2 gap-5 w-full">
|
||||||
|
{members.map((member, index) => {
|
||||||
|
const { name, title } =
|
||||||
|
typeof member === "string"
|
||||||
|
? { name: member, title: "" }
|
||||||
|
: member;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={`${name}-${index}`}
|
||||||
|
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||||
|
className="bg-[#eeebf4] text-[#48286e] 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-[#48286e]">
|
||||||
|
{name}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{title && (
|
||||||
|
<p className="text-xl mt-2 font-semibold">
|
||||||
|
{title}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-white">
|
||||||
<PublicNavbar />
|
<PublicNavbar />
|
||||||
|
|
||||||
<main className="bg-gradient-to-b from-white to-[#f1eef9] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-8 sm:py-10 md:py-12">
|
<main className="bg-gradient-to-b from-[#f9fafb] to-[#ddd8eb] pt-8 sm:pt-10 md:pt-12">
|
||||||
{/* Hero Section with Contact */}
|
{/* Hero Section */}
|
||||||
<section className="bg-gradient-to-r from-[#664fa3] to-[#48286e] py-8 rounded-2xl mb-12">
|
<section className=" pt-16 pb-4 px-4 sm:px-6 md:px-8 lg:px-12 xl:px-20">
|
||||||
<div className="max-w-5xl mx-auto text-center px-8">
|
<div className="max-w-5xl mx-auto text-center px-8">
|
||||||
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold text-white mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h1 className="text-2xl sm:text-3xl md:text-[40px] leading-[1.2] text-[#48286e] font-bold mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
LOAF Board of Directors 2025
|
LOAF Board of Directors 2025
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-white text-lg mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/* Contact Info */}
|
||||||
|
<section className="flex justify-center mt-4 mb-8">
|
||||||
|
<div className=" w-full text-center px-8 justify-center bg-gradient-to-r from-[#664fa3] to-[#48286e] max-w-5xl mx-6 flex lg:mx-12 py-5 rounded-3xl sm:px-6 md:px-8 lg:px-12 xl:px-20">
|
||||||
|
|
||||||
|
<p className="text-white text-xl font-bold " style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
For any questions or inquiries please email us at{' '}
|
For any questions or inquiries please email us at{' '}
|
||||||
<a href="mailto:info@loaftx.org" className="text-[#c5b4e3] underline font-bold hover:text-white transition-colors">
|
<a href="mailto:info@loaftx.org" className="text-[#c5b4e3] underline font-bold hover:text-white transition-colors">
|
||||||
info@loaftx.org
|
info@loaftx.org
|
||||||
@@ -37,57 +91,28 @@ const BoardOfDirectors = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
{/* Board Members Section */}
|
||||||
|
<section className=' flex lg:flex-row flex-col gap-10 items-stretch mt-8 px-4 sm:px-6 md:px-8 lg:px-12 xl:px-20 pb-12'>
|
||||||
{/* Officers Grid */}
|
{/* Officers Grid */}
|
||||||
<section className="py-12">
|
<DirectorCards title="Officers" members={officers} />
|
||||||
<div className="max-w-6xl mx-auto">
|
|
||||||
<h2 className="text-xl sm:text-2xl md:text-3xl font-bold text-[#48286e] text-center mb-8" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
Officers
|
|
||||||
</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
||||||
{officers.map((officer, index) => (
|
|
||||||
<Card key={index} className="bg-[#eeebf4] p-6 text-center rounded-xl shadow-md hover:shadow-lg transition-shadow">
|
|
||||||
<h3 className="text-xl font-bold text-[#48286e] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
{officer.name}
|
|
||||||
</h3>
|
|
||||||
<p className="text-lg text-[#48286e]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
{officer.title}
|
|
||||||
</p>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Board Members Grid */}
|
{/* Board Members Grid */}
|
||||||
<section className="py-12 bg-gray-50 rounded-2xl">
|
<DirectorCards title="Board Of Directors" members={boardMembers} />
|
||||||
<div className="max-w-6xl mx-auto px-8">
|
|
||||||
<h2 className="text-xl sm:text-2xl md:text-3xl font-bold text-[#48286e] text-center mb-8" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
Board of Directors
|
|
||||||
</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
||||||
{boardMembers.map((member, index) => (
|
|
||||||
<Card key={index} className="bg-[#eeebf4] p-6 text-center rounded-xl shadow-md hover:shadow-lg transition-shadow">
|
|
||||||
<h3 className="text-xl font-bold text-[#48286e]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
{member}
|
|
||||||
</h3>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Join the Board Section */}
|
{/* Join the Board Section */}
|
||||||
<section className="py-12">
|
<section className="py-12 bg-white mt-12 ">
|
||||||
<div className="max-w-4xl mx-auto">
|
{/* content containter */}
|
||||||
<h2 className="text-xl sm:text-2xl md:text-3xl font-bold text-[#48286e] text-center mb-8" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<div className="w-full mx-auto flex flex-col px-4 sm:px-6 md:px-8 lg:px-12 xl:px-20">
|
||||||
|
<h2 className="text-xl mx-auto sm:text-2xl md:text-4xl font-bold text-[#48286e] text-center mb-8" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
Join the Board of Directors
|
Join the Board of Directors
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xl text-[#48286e] text-center mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="lg:text-2xl text-md md:text-lg max-w-4xl mx-auto justify-center font-bold text-[#48286e] text-center mb-8" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
Our elections take place at our December holiday social. Here are some things
|
Our elections take place at our December holiday social. Here are some things to know if you are thinking about serving on the Board of Directors.
|
||||||
to know if you are thinking about serving on the Board of Directors.
|
|
||||||
</p>
|
</p>
|
||||||
<Card className="bg-[#eeebf4] p-8 rounded-2xl shadow-lg">
|
{/* card */}
|
||||||
|
<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-[#48286e]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<ol className="list-decimal list-inside space-y-4 text-lg text-[#48286e]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
<li>
|
<li>
|
||||||
Nominations are due by November 1. Nomination Form:{' '}
|
Nominations are due by November 1. Nomination Form:{' '}
|
||||||
@@ -96,13 +121,39 @@ const BoardOfDirectors = () => {
|
|||||||
Click Here
|
Click Here
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>Nominees must have been a member for at least 1 year and current with their dues.</li>
|
<li>Nominees must have been a member for at least 1 year, however it is possible to be elected prior to 1 year, but start the term on the 1 year anniversary.</li>
|
||||||
<li>Officer positions are only available to current directors.</li>
|
<li>Officer positions are only available to current directors.</li>
|
||||||
<li>Each director shall serve a 2 year term.</li>
|
|
||||||
<li>The time commitment is 1-2 hours per week.</li>
|
<li>Each director shall serve a 2-year term.</li>
|
||||||
<li>The tasks that directors perform depend on individual interests, skills, and time available.</li>
|
|
||||||
<li>Directors must attend Board meetings which are held the second Thursday of each month at 6:30pm via Zoom.</li>
|
<li>The time commitment is approximately 1–2 hours per week.</li>
|
||||||
<li>We are a fun group, and we would love for you to join us in providing this service for our community.</li>
|
|
||||||
|
<li>
|
||||||
|
The tasks that directors perform depend on individual interests. Recent
|
||||||
|
tasks include researching how to obtain an extra PO Box key, ordering
|
||||||
|
Welcome Team name tags, taking pictures at events, researching new venues
|
||||||
|
for holiday socials, and monitoring Facebook posts. For more information
|
||||||
|
about director duties, see Article 2 of the bylaws in the Members Only
|
||||||
|
section of the website:
|
||||||
|
<a
|
||||||
|
href="https://loaftx.org/bylaws/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-[#48286e] underline"
|
||||||
|
>
|
||||||
|
https://loaftx.org/bylaws/
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
Directors must attend Board meetings held on the second Thursday of each
|
||||||
|
month at 6:30pm via Zoom.
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
We are a fun group, and we would love for you to join us in providing this
|
||||||
|
service for our community.
|
||||||
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Checkbox } from '../components/ui/checkbox';
|
|||||||
import { Mail, MapPin, Loader2 } from 'lucide-react';
|
import { Mail, MapPin, Loader2 } from 'lucide-react';
|
||||||
import api from '../utils/api';
|
import api from '../utils/api';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { PiMapTrifoldBold } from "react-icons/pi";
|
||||||
const ContactUs = () => {
|
const ContactUs = () => {
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
firstName: '',
|
firstName: '',
|
||||||
@@ -103,8 +103,8 @@ const ContactUs = () => {
|
|||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8">
|
||||||
{/* Contact Form */}
|
{/* Contact Form */}
|
||||||
<Card className="p-8 bg-white rounded-2xl border-2 border-[#ddd8eb] shadow-lg">
|
<Card className="p-8 bg-white rounded-2xl">
|
||||||
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold text-[#48286e] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h1 className="text-2xl sm:text-[28px] leading-5 font-bold text-[#48286e] mb-12" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
Contact Form
|
Contact Form
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ const ContactUs = () => {
|
|||||||
name="firstName"
|
name="firstName"
|
||||||
value={formData.firstName}
|
value={formData.firstName}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="border-2 border-[#ddd8eb] focus:border-[#664fa3] rounded-full h-12 px-4"
|
className="border-2 border-[#ddd8eb] bg-[#eaedf4] focus:border-[#664fa3] rounded-full h-12 px-4"
|
||||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -134,7 +134,7 @@ const ContactUs = () => {
|
|||||||
name="lastName"
|
name="lastName"
|
||||||
value={formData.lastName}
|
value={formData.lastName}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="border-2 border-[#ddd8eb] focus:border-[#664fa3] rounded-full h-12 px-4"
|
className="border-2 bg-[#eaedf4] border-[#ddd8eb] focus:border-[#664fa3] rounded-full h-12 px-4"
|
||||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -152,7 +152,7 @@ const ContactUs = () => {
|
|||||||
type="email"
|
type="email"
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="border-2 border-[#ddd8eb] focus:border-[#664fa3] rounded-full h-12 px-4"
|
className="border-2 bg-[#eaedf4] border-[#ddd8eb] focus:border-[#664fa3] rounded-full h-12 px-4"
|
||||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -168,7 +168,7 @@ const ContactUs = () => {
|
|||||||
name="subject"
|
name="subject"
|
||||||
value={formData.subject}
|
value={formData.subject}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="border-2 border-[#ddd8eb] focus:border-[#664fa3] rounded-full h-12 px-4"
|
className="border-2 bg-[#eaedf4] border-[#ddd8eb] focus:border-[#664fa3] rounded-full h-12 px-4"
|
||||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -184,7 +184,7 @@ const ContactUs = () => {
|
|||||||
name="message"
|
name="message"
|
||||||
value={formData.message}
|
value={formData.message}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="border-2 border-[#ddd8eb] focus:border-[#664fa3] rounded-2xl min-h-[150px] px-4 py-3 resize-none"
|
className="border-2 bg-[#eaedf4] border-[#ddd8eb] focus:border-[#664fa3] rounded-2xl min-h-[150px] px-4 py-3 resize-none"
|
||||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -226,21 +226,21 @@ const ContactUs = () => {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Message Card */}
|
{/* Message Card */}
|
||||||
<Card className="p-8 bg-gradient-to-r from-[#664fa3] to-[#48286e] rounded-2xl shadow-lg text-white">
|
<Card className="p-8 bg-gradient-to-r from-[#664fa3] to-[#48286e] rounded-2xl shadow-lg text-white">
|
||||||
<p className="text-xl leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-[28px] font-semibold leading-relaxed" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
If you have questions, or are interested in joining, we would love hearing from you.
|
If you have questions, or are interested in joining, we would love hearing from you.
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Email Card */}
|
{/* Email Card */}
|
||||||
<Card className="p-6 bg-white rounded-2xl border-2 border-[#ddd8eb] shadow-lg">
|
<Card className="p-6 bg-white rounded-2xl">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="w-12 h-12 bg-[#e8e0f5] rounded-full flex items-center justify-center flex-shrink-0">
|
<div className="flex items-center justify-center flex-shrink-0">
|
||||||
<Mail className="h-6 w-6 text-[#664fa3]" />
|
<Mail className="size-12 text-[#664fa3]" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a
|
<a
|
||||||
href="mailto:info@loaftx.org"
|
href="mailto:info@loaftx.org"
|
||||||
className="text-[#664fa3] text-xl font-semibold hover:text-[#48286e] transition-colors"
|
className="text-[#865edf] text-xl font-semibold hover:text-[#48286e] transition-colors"
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||||
>
|
>
|
||||||
info@loaftx.org
|
info@loaftx.org
|
||||||
@@ -250,16 +250,16 @@ const ContactUs = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Address Card */}
|
{/* Address Card */}
|
||||||
<Card className="p-6 bg-white rounded-2xl border-2 border-[#ddd8eb] shadow-lg">
|
<Card className="p-6 bg-white rounded-2xl ">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<div className="w-12 h-12 bg-[#e8e0f5] rounded-full flex items-center justify-center flex-shrink-0">
|
<div className="flex items-center justify-center flex-shrink-0">
|
||||||
<MapPin className="h-6 w-6 text-[#664fa3]" />
|
<PiMapTrifoldBold className="size-12 text-[#664fa3]" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[#48286e] text-lg font-semibold mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<p className="text-[#48286e] text-[28px] font-semibold mb-1" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
LOAF
|
LOAF
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[#664fa3] text-base leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-[#48286e] text-[28px] font-semibold leading-relaxed" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
P.O. Box 7207<br />
|
P.O. Box 7207<br />
|
||||||
Houston, Texas 77248-7207
|
Houston, Texas 77248-7207
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -15,9 +15,12 @@ const Dashboard = () => {
|
|||||||
const [events, setEvents] = useState([]);
|
const [events, setEvents] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [resendLoading, setResendLoading] = useState(false);
|
const [resendLoading, setResendLoading] = useState(false);
|
||||||
|
const [eventActivity, setEventActivity] = useState(null);
|
||||||
|
const [activityLoading, setActivityLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUpcomingEvents();
|
fetchUpcomingEvents();
|
||||||
|
fetchEventActivity();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchUpcomingEvents = async () => {
|
const fetchUpcomingEvents = async () => {
|
||||||
@@ -32,6 +35,17 @@ const Dashboard = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchEventActivity = async () => {
|
||||||
|
try {
|
||||||
|
const response = await api.get('/members/event-activity');
|
||||||
|
setEventActivity(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch event activity:', error);
|
||||||
|
} finally {
|
||||||
|
setActivityLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleResendVerification = async () => {
|
const handleResendVerification = async () => {
|
||||||
setResendLoading(true);
|
setResendLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -298,6 +312,156 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Event Activity Section */}
|
||||||
|
<div className="mt-12">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h2 className="text-3xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
My Event Activity
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activityLoading ? (
|
||||||
|
<p className="text-[#664fa3]" 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-white rounded-2xl border border-[#ddd8eb]">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="bg-[#DDD8EB]/20 p-4 rounded-lg">
|
||||||
|
<Calendar className="h-8 w-8 text-[#664fa3]" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total RSVPs</p>
|
||||||
|
<p className="text-3xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
{eventActivity.total_rsvps}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-6 bg-white rounded-2xl border border-[#ddd8eb]">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="bg-[#81B29A]/20 p-4 rounded-lg">
|
||||||
|
<CheckCircle className="h-8 w-8 text-[#81B29A]" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Events Attended</p>
|
||||||
|
<p className="text-3xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
{eventActivity.total_attended}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Upcoming RSVP'd Events */}
|
||||||
|
{eventActivity.upcoming_events && eventActivity.upcoming_events.length > 0 && (
|
||||||
|
<Card className="p-6 bg-white rounded-2xl border border-[#ddd8eb]">
|
||||||
|
<h3 className="text-xl font-semibold text-[#422268] 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-[#ddd8eb] rounded-xl hover:border-[#664fa3] hover:shadow-md transition-all">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-semibold text-[#422268] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>{event.title}</h4>
|
||||||
|
<p className="text-sm text-[#664fa3] 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-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>{event.location}</p>
|
||||||
|
</div>
|
||||||
|
<Badge className={
|
||||||
|
event.rsvp_status === 'yes' ? 'bg-[#81B29A] text-white' :
|
||||||
|
event.rsvp_status === 'maybe' ? 'bg-orange-100 text-orange-700' :
|
||||||
|
'bg-gray-200 text-gray-700'
|
||||||
|
}>
|
||||||
|
{event.rsvp_status === 'yes' ? 'Going' :
|
||||||
|
event.rsvp_status === 'maybe' ? 'Maybe' : 'Not Going'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Past Events & Attendance */}
|
||||||
|
{eventActivity.past_events && eventActivity.past_events.length > 0 && (
|
||||||
|
<Card className="p-6 bg-white rounded-2xl border border-[#ddd8eb]">
|
||||||
|
<h3 className="text-xl font-semibold text-[#422268] 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-[#ddd8eb] rounded-xl">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-semibold text-[#422268] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>{event.title}</h4>
|
||||||
|
<p className="text-sm text-[#664fa3] 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-[#81B29A] 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-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Checked in: {new Date(event.attended_at).toLocaleDateString()}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{eventActivity.past_events.length > 5 && (
|
||||||
|
<p className="text-sm text-center text-[#664fa3] mt-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Showing 5 of {eventActivity.past_events.length} past events
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* No Events Message */}
|
||||||
|
{(!eventActivity.upcoming_events || eventActivity.upcoming_events.length === 0) &&
|
||||||
|
(!eventActivity.past_events || eventActivity.past_events.length === 0) && (
|
||||||
|
<Card className="p-12 bg-white rounded-2xl border border-[#ddd8eb]">
|
||||||
|
<div className="text-center">
|
||||||
|
<Calendar className="h-16 w-16 text-[#ddd8eb] mx-auto mb-4" />
|
||||||
|
<h3 className="text-xl font-semibold text-[#422268] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
No Event Activity Yet
|
||||||
|
</h3>
|
||||||
|
<p className="text-[#664fa3] 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-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-6">
|
||||||
|
<Calendar className="h-4 w-4 mr-2" />
|
||||||
|
Browse Events
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Card className="p-12 bg-white rounded-2xl border border-[#ddd8eb]">
|
||||||
|
<div className="text-center">
|
||||||
|
<AlertCircle className="h-16 w-16 text-[#ddd8eb] mx-auto mb-4" />
|
||||||
|
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Failed to load event activity. Please try refreshing the page.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MemberFooter />
|
<MemberFooter />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -55,32 +55,36 @@ const Donate = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen">
|
||||||
<PublicNavbar />
|
<PublicNavbar />
|
||||||
|
|
||||||
<main className="bg-gradient-to-b from-white to-[#f1eef9] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-8 sm:py-10 md:py-12">
|
<main className="bg-gradient-to-bl from-[#F9FAFB] to-[#DDD8EB] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-8 sm:py-10 md:py-12">
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section className="py-12">
|
<section className="py-12">
|
||||||
<div className="max-w-4xl mx-auto text-center">
|
<div className="max-w-4xl mx-auto text-center h-full">
|
||||||
<div className="flex justify-center mb-4">
|
<div className="flex justify-center mb-4">
|
||||||
<img src={loafHearts} alt="Hearts" className="w-32 h-auto" onError={(e) => e.target.style.display = 'none'} />
|
<img src={loafHearts} alt="Hearts" className="w-32 h-auto" onError={(e) => e.target.style.display = 'none'} />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl sm:text-4xl md:text-5xl font-bold text-[#48286e] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h1 className="text-3xl sm:text-4xl md:text-5xl font-bold text-[#48286e] mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
Donate
|
Donate
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xl text-[#48286e]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-xl text-[#48286e] font-medium" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
We really appreciate your donations. You can make your donation online
|
We really appreciate your donations. You can make your donation online
|
||||||
or send a check by mail.
|
or send a check by mail.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Columns */}
|
||||||
|
<div className="py-12">
|
||||||
|
<div className='grid grid-cols-1 items-stretch lg:grid-cols-[2fr_1fr] gap-8 lg:max-h-[450px]'>
|
||||||
|
|
||||||
{/* Donation Amount Buttons */}
|
{/* Donation Amount Buttons */}
|
||||||
<section className="py-12">
|
<section className="flex flex-col h-full">
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="mx-auto flex-1 w-full h-full">
|
||||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
<Card className="p-8 bg-white rounded-3xl w-full h-full content-center">
|
||||||
<div className="flex items-center gap-4 mb-6">
|
<div className="flex items-center gap-4 mb-6">
|
||||||
<CreditCard className="w-12 h-12 text-[#664fa3]" />
|
<CreditCard className="size-24 text-[#664fa3]" />
|
||||||
<h2 className="text-3xl font-bold text-[#48286e]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h2 className="text-3xl font-bold text-[#48286e]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
Select Your Donation Amount
|
Select Your Donation Amount
|
||||||
</h2>
|
</h2>
|
||||||
@@ -122,12 +126,14 @@ const Donate = () => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Alternative Payment Methods */}
|
{/* Alternative Payment Methods */}
|
||||||
<section className="py-12">
|
<section className="flex flex-col">
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-6xl mx-auto w-full">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
<div className="flex flex-col gap-8 w-full">
|
||||||
{/* Mail Check */}
|
{/* Mail Check */}
|
||||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
<Card className="p-8 bg-white rounded-3xl flex gap-4 items-center flex-1">
|
||||||
<Mail className="w-12 h-12 text-[#664fa3] mb-4" />
|
<Mail className="size-24 text-[#664fa3]" />
|
||||||
|
|
||||||
|
<div>
|
||||||
<h3 className="text-2xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h3 className="text-2xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
Mail a Check
|
Mail a Check
|
||||||
</h3>
|
</h3>
|
||||||
@@ -137,13 +143,16 @@ const Donate = () => {
|
|||||||
P.O. Box 7207<br />
|
P.O. Box 7207<br />
|
||||||
Houston, Texas 77248-7207
|
Houston, Texas 77248-7207
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Zelle */}
|
{/* Zelle */}
|
||||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
<Card className="p-8 bg-white rounded-3xl flex gap-4 items-center flex-1">
|
||||||
<div className="mb-4">
|
<div className="w-44">
|
||||||
<img src={zelleLogo} alt="Zelle" className="h-32" onError={(e) => e.target.style.display = 'none'} />
|
<img src={zelleLogo} alt="Zelle" className=" w-32" onError={(e) => e.target.style.display = 'none'} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
<h3 className="text-2xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h3 className="text-2xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
Pay with Zelle
|
Pay with Zelle
|
||||||
</h3>
|
</h3>
|
||||||
@@ -155,17 +164,22 @@ const Donate = () => {
|
|||||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
LOAFHoustonTX@gmail.com
|
LOAFHoustonTX@gmail.com
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Columns end */}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<PublicFooter />
|
<PublicFooter />
|
||||||
|
|
||||||
{/* Custom Amount Dialog */}
|
{/* Custom Amount Dialog */}
|
||||||
<Dialog open={customAmountDialogOpen} onOpenChange={setCustomAmountDialogOpen}>
|
<Dialog open={customAmountDialogOpen} onOpenChange={setCustomAmountDialogOpen}>
|
||||||
<DialogContent className="sm:max-w-[450px] bg-white rounded-2xl">
|
<DialogContent className="sm:max-w-[450px] bg-white rounded-3xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-2xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<DialogTitle className="text-2xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
Enter Donation Amount
|
Enter Donation Amount
|
||||||
|
|||||||
@@ -4,6 +4,35 @@ import PublicFooter from '../components/PublicFooter';
|
|||||||
import { Button } from '../components/ui/button';
|
import { Button } from '../components/ui/button';
|
||||||
import { Card } from '../components/ui/card';
|
import { Card } from '../components/ui/card';
|
||||||
import { Pen } from 'lucide-react';
|
import { Pen } from 'lucide-react';
|
||||||
|
import { LuArrowDown } from "react-icons/lu";
|
||||||
|
|
||||||
|
const CardSection = ({ children, className = '', arrow = true }) => (
|
||||||
|
<section className={` ${className}`}>
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<Card className="p-14 bg-white rounded-3xl">
|
||||||
|
{children}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
{arrow && (<div className="flex text-2xl my-5 justify-center">
|
||||||
|
<LuArrowDown />
|
||||||
|
</div>)}
|
||||||
|
{!arrow && (
|
||||||
|
<div className="mb-12"></div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Title = ({ children }) => (
|
||||||
|
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
|
{children}
|
||||||
|
</h2>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Paragragh = ({ children }) => (
|
||||||
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
|
||||||
const History = () => {
|
const History = () => {
|
||||||
const ardenCharlotteImg = `${process.env.PUBLIC_URL}/history-arden-charlotte.png`;
|
const ardenCharlotteImg = `${process.env.PUBLIC_URL}/history-arden-charlotte.png`;
|
||||||
@@ -12,20 +41,21 @@ const History = () => {
|
|||||||
const part3Img = `${process.env.PUBLIC_URL}/history-part3.png`;
|
const part3Img = `${process.env.PUBLIC_URL}/history-part3.png`;
|
||||||
const part7Img = `${process.env.PUBLIC_URL}/history-part7.png`;
|
const part7Img = `${process.env.PUBLIC_URL}/history-part7.png`;
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-white">
|
||||||
<PublicNavbar />
|
<PublicNavbar />
|
||||||
|
|
||||||
<main className="bg-gradient-to-b from-white to-[#f1eef9] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-8 sm:py-10 md:py-12">
|
<main className="bg-gradient-to-br from-[#F9FAFB] to-[#DCD7EA] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-8 sm:py-10 md:py-12">
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section className="py-12">
|
<section className="py-12">
|
||||||
<div className="max-w-5xl mx-auto text-center">
|
<div className="max-w-3xl mx-auto flex justify-around mb-12 flex-col gap-6 items-center lg:flex-row">
|
||||||
<h1 className="text-3xl sm:text-4xl md:text-5xl font-bold text-[#48286e] mb-4"
|
<h1 className="text-3xl sm:text-4xl md:text-5xl font-extrabold text-[#48286e] "
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
History of LOAF
|
History of LOAF
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex items-center justify-center gap-2 text-[#48286e]">
|
<div className="flex items-center justify-center gap-6 text-[#48286e]">
|
||||||
<Pen className="h-5 w-5" />
|
<Pen className="size-7" />
|
||||||
<p className="text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
By Arden Eversmeyer
|
By Arden Eversmeyer
|
||||||
</p>
|
</p>
|
||||||
@@ -34,10 +64,8 @@ const History = () => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Part 1 - With Image */}
|
{/* Part 1 - With Image */}
|
||||||
<section className="py-12">
|
<CardSection>
|
||||||
<div className="max-w-5xl mx-auto">
|
<div className="flex flex-col md:flex-row gap-14 items-center">
|
||||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
|
||||||
<div className="flex flex-col md:flex-row gap-8 items-start">
|
|
||||||
<div className="md:w-1/3 ">
|
<div className="md:w-1/3 ">
|
||||||
<img src={ardenCharlotteImg} alt="Arden Eversmeyer and Charlotte Avery"
|
<img src={ardenCharlotteImg} alt="Arden Eversmeyer and Charlotte Avery"
|
||||||
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
||||||
@@ -46,190 +74,180 @@ const History = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:w-2/3">
|
<div className="md:w-2/3">
|
||||||
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<Title>Part 1</Title>
|
||||||
Part 1
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
</h2>
|
In 1985 my life partner of 33 years died. For many years we had been part of a large “friendship group”, many of whom had been together longer than we had. But I was the first to lose a partner. After a few months I began to feel the need to explore community. Already retired, the necessity of being closeted was gone. I soon discovered there was no group for mid-life an old lesbians in Houston, and began the search for such groups around the U.S.
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
In 1985 my life partner of 33 years died. For many years we had been part of a large "friendship group" that got together for meals and games. After her death, I found myself on the edge of the group. I felt invisible. The group, composed primarily of couples, didn't know what to do with the single person they had suddenly become.
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-md mb-4 text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
When I moved to Houston in 1992, I again found myself isolated. I had friends, but not being "coupled" in a "couples world" left me on the outside. I was aware of my advancing age – I was 63 at the time - and I was sure that I was the only "old lesbian" in Houston. I checked out the Montrose bars, but to my dismay, found that older lesbians were non-existent; at least they didn't hang out in bars.
|
In April of 1987 I loaded my camper and headed for California. I started in San Diego, and worked my way up the coast to San Francisco finding and visiting senior LGBT groups. I came home with much information, and some suggestions about organizing. One fact that was consistent with all the groups was that if the group was for both men and women - the women dropped out. The recommendation was to start a group for women only.
|
||||||
</p>
|
</p>
|
||||||
|
<p className="text-md text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>I had become friends with some young lesbians here in Houston who knew the Gay community. We started meeting and brainstorming, and the group now known as LOAF was born.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</CardSection>
|
||||||
</div>
|
{/* Arrow */}
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Part 2 */}
|
{/* Part 2 */}
|
||||||
<section className="py-12">
|
<CardSection >
|
||||||
<div className="max-w-5xl mx-auto">
|
<Title>Part 2</Title>
|
||||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
Part 2
|
|
||||||
</h2>
|
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
The Founding Mothers of LOAF are Ruth Sathre (nurse), JoAnn Beene (psychologist), Delores Nason (business woman), JoAnn Loulan (psychologist and writer, now living in Guerneville, CA), and Judy Peyton (social worker). We decided to form a group for Lesbians Over Fifty and began the search for others "like us."
|
The Founding Mothers of LOAF are Ruth Sathre (nurse), JoAnn Beene (psychologist), Delores Nason (business woman), JoAnn Loulan (psychologist and writer, now living in Guerneville, CA), and Judy Peyton (social worker). We decided to form a group for Lesbians Over Fifty and began the search for others "like us."
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
In 1993, we advertised in OutSmart Magazine, the Houston gay magazine, and invited interested women to join us at a local restaurant. Founding mothers and 19 other women came to that first meeting. Since then the group has gone through many evolutions.
|
In 1993, we advertised in OutSmart Magazine, the Houston gay magazine, and invited interested women to join us at a local restaurant. Founding mothers and 19 other women came to that first meeting. Since then the group has gone through many evolutions.
|
||||||
</p>
|
</p>
|
||||||
<ul className="list-disc ml-6 mt-4 space-y-2 text-lg text-[#48286e]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<ul className="list-disc ml-6 mt-4 space-y-2 text-md text-[#48286e]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
<li><strong>AGE OF PARTICIPANTS</strong> - we started off as LOAFF - Lesbians over Age Fifty-Five. The extra F stood for 55, which didn't work very well, so we changed to LOAF and lowered the age to 50.</li>
|
<li>AGE OF PARTICIPANTS - We launched as LOAFF (Lesbians Over Age Fifty-Five) and quickly lowered the entry age to fifty so more women could join.</li>
|
||||||
<li><strong>NAME FOR THE GROUP</strong> - LOAFF and then LOAF</li>
|
<li>NAME FOR THE GROUP - The acronym began as LOAFF, then we streamlined it to the now-familiar LOAF.</li>
|
||||||
<li><strong>NUMBER OF EVENTS</strong> - Some of the early years we had events every Saturday afternoon, but as we aged, we cut back to one event each month, then we went to the current format of one event during the week, either afternoon or evening, and a weekend activity.</li>
|
<li>AMOUNT OF STRUCTURING - Too many rules can smother a grassroots group, so we kept things loose and let participating women guide direction as needs evolved.</li>
|
||||||
<li><strong>TYPES OF EVENTS</strong> - We've had LOTS of different events. Some of the events we have had include: going to a museum, going to the symphony, seeing a play or movie together, going out to dinner, pot luck dinners, game nights, campfires, hiking, kayaking, and more.</li>
|
<li>SAFE HAVEN FOR MEETINGS - Gatherings had to be in discreet, transit-accessible locations, scheduled during daylight (often Sundays) so closeted or non-driving members could attend comfortably.</li>
|
||||||
|
<li>NEWSLETTER - A monthly mailing went out before each month's end, highlighting news plus upcoming activities tailored to the community.</li>
|
||||||
|
<li>DUES - Contributions were set at $2 per month per woman, with a standing policy that anyone unable to pay was still welcome—unchanged since inception.</li>
|
||||||
|
<li>CREATIVE PUBLICITY - We produced flyers and placed them with LGBTQ+ organizations, counselors, and other allies, recognizing the women we hoped to reach wouldn't necessarily be found in bars and would arrive mostly via word of mouth.</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</Card>
|
|
||||||
</div>
|
</CardSection>
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Part 3 - With Image */}
|
{/* Part 3 - With Image */}
|
||||||
<section className="py-12">
|
<CardSection >
|
||||||
<div className="max-w-5xl mx-auto">
|
<div className="flex gap-14 flex-col min-w-2xl md:flex-row justify-center items-center">
|
||||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
<div className="">
|
||||||
<div className="flex flex-col md:flex-row gap-8 items-start">
|
|
||||||
<div className="md:w-2/3">
|
|
||||||
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
Part 3
|
|
||||||
</h2>
|
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
We have never had a formal organization with by-laws and officers. We have operated on a consensus basis with the founders making most of the decisions. One of the early decisions we made was that we would not have any kind of formal membership. We wanted to be as inclusive as possible and not create any barriers to participation.
|
|
||||||
</p>
|
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
We have always been self-supporting. We have never charged dues or asked for donations. Each person pays for their own meal or activity. We have never had a budget or a bank account. We have been able to operate this way because we have always kept our activities simple and inexpensive.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="md:w-1/3">
|
|
||||||
<img src={part3Img} alt="LOAF Community"
|
<img src={part3Img} alt="LOAF Community"
|
||||||
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
||||||
<p className="text-sm text-[#48286e] mt-2 text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
LOAF Community
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Part 4 - With Image */}
|
</div>
|
||||||
<section className="py-12">
|
<div className="">
|
||||||
<div className="max-w-5xl mx-auto">
|
<Title>Part 3</Title>
|
||||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
<div className="flex flex-col md:flex-row gap-8 items-start">
|
The first meeting of LOAF was the third Sunday of October 1987 at Womynspace. There were six women besides myself at that first meeting. attending were Betty Rudnick, Billie Carter, Josephine Jones, Sylvia Porter, Marjorie Fulp, and Charlotte Avery. Of those six women, only Sylvia Porter and Charlotte Avery are still alive.
|
||||||
<div className="md:w-2/3">
|
|
||||||
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
Part 4
|
|
||||||
</h2>
|
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
Over the years, LOAF has been a place where women can be themselves, where they can talk about their lives and their experiences without fear of judgment. We have created a safe space for women to explore their sexuality and their identity as lesbians.
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
Many women have told us that LOAF has been a lifeline for them, especially as they age and find themselves increasingly isolated. LOAF has provided a community and a sense of belonging that has been invaluable.
|
Much that we discussed that day is still the heart of the group today - decisions by consensus at monthly meetings and activities governed by needs and wishes of participating women. It was soon decided to make the age requirement for membership age 50, and we became LOAF.
|
||||||
|
</p>
|
||||||
|
<p className="text-md text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
In 1989 member Jo Stewart, social worker at Methodist Hospital, started urging LOAF to incorporate as a non-profit. The work began in 1990 with Moore & Hunt (Debbie Hunt) as our Corporate Attorneys. Jo died of cancer in 1990. The work for application of our 501(c)3 was done by Floi Ewing, Arden's sister, and our non-profit status was granted in January 1991. Loaf incorporated as a social networking and support group without a membership roll to protect the anonymity of the women in LOAF.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:w-1/3">
|
</div>
|
||||||
|
|
||||||
|
</CardSection>
|
||||||
|
|
||||||
|
{/* Part 4 */}
|
||||||
|
<CardSection>
|
||||||
|
<div className=" ">
|
||||||
|
<Title>Part 4</Title>
|
||||||
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Third Sunday meeting places have changed over the years. We moved from Womynspace to Autrey House near Rice University. We were there from November 1987 until May 1990 when the new Bishop dis-invited all GLBT groups because of homophobia. We spent a couple months at Montrose Counseling Center (on Lovett), and then moved to the Metropolitan Multi- Service Center on W. Gray. We met there from August 1990 until January 1993. We left because the city started closing the centers on Sunday, and we were not willing to change our meeting day. From February through June we met at Inklings Book Store . In July we started our long occupancy with Houston Mission Church, and met there until the church dissolved in April 2001. We then met at the Hollyfield Center for seven months. From there we went to the GLBT Community Center on Hawthorne where we stayed until July 2003. Attendance was dropping off, and some of the women were not comfortable in a gay identified place. So Third Sunday Meetings moved to Charlotte and Arden's home - and we met there from August 2003 until April 2011. Membership had grown until the meetings had reached critical mass and parking was a problem. So a team of board members started researching for a new home. And on the third Sunday of May 2011 LOAF started meeting at the Montrose Counseling Center. A new era had started.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardSection>
|
||||||
|
|
||||||
|
{/* Part 5 - With Image */}
|
||||||
|
<CardSection >
|
||||||
|
<div className="flex gap-8 flex-col lg:flex-row justify-center items-center md:items-start">
|
||||||
|
<div className="flex flex-col gap-8 w-full lg:w-1/2">
|
||||||
<img src={pride1Img} alt="Pride Parade"
|
<img src={pride1Img} alt="Pride Parade"
|
||||||
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
||||||
<img src={pride2Img} alt="Pride Parade"
|
<img src={pride2Img} alt="Pride Parade"
|
||||||
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
||||||
<p className="text-sm text-[#48286e] mt-2 text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
LOAF at Pride
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Part 5 */}
|
<div className="w-full lg:w-1/2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
<section className="py-12">
|
<Title>Part 5</Title>
|
||||||
<div className="max-w-5xl mx-auto">
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" >
|
||||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
The activities of the monthly meetings were decided by the participating women. Rules were very limited, and decisions were made monthly by the women attending. We soon decided to set up a quarterly meeting schedule of pot-luck, speaker, and games. We followed this schedule for at least ten years. New ideas/programs were added as time allowed. Our first speaker was Pokey Anderson, an icon in the GLBT community. She provided much information about our history in Houston. We also decided to ask Deb Hunt to talk to us about documents, and she has done this periodically over the years. She spoke to us well before we decided to incorporate, and she then became our Corporate Attorney.
|
||||||
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
Part 5
|
|
||||||
</h2>
|
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
LOAF has also been a place where women can give back to the community. Many of our members have been active in various LGBTQ+ organizations and causes. We have marched in Pride parades, volunteered at LGBTQ+ events, and supported various LGBTQ+ initiatives.
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
As we look to the future, we are committed to continuing to provide a welcoming and inclusive space for lesbians over 50. We know that there are many women out there who are looking for a community like ours, and we want to make sure that they know that LOAF is here for them.
|
In November 1988 we instituted our annual Benevolent Project. We collected items to contribute to Stone Soup Kitchen - a GLBT food pantry. We have done this every year since, and have contributed to groups such as Omega House, The Rose, Battered Women, local lesbian organizations, and some individuals.
|
||||||
|
</p>
|
||||||
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
In June 1989 we entered our first Pride Parade. That year we had a convertible and a walking group. Several of the LOAF women participate with other groups in the parade, but we have participated every year since 1989. In 2010 we entered our first float and won a trophy for best representation of the theme.
|
||||||
|
</p>
|
||||||
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
All of these decisions were made by the women at the Third Sunday meetings. There have never been rules instigated by the Board of Directors. Because many women don't want to attend meetings, we changed Third Sunday Meeting to Meet 'N Greet several years ago. And that is what we do - take care of any necessary business. But greet newcomers and socialize with our friends.
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
|
</CardSection>
|
||||||
|
|
||||||
|
|
||||||
{/* Part 6 */}
|
{/* Part 6 */}
|
||||||
<section className="py-12">
|
<CardSection >
|
||||||
<div className="max-w-5xl mx-auto">
|
|
||||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
Part 6
|
Part 6
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
One of the things that has made LOAF special is the diversity of our members. We have women from all walks of life, all backgrounds, all races, all religions, and all political persuasions. What we have in common is our age and our sexual orientation.
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Socials have always been a big part of the activities for LOAF, and having a social as well as the monthly “Third Sunday” meeting was always on the calendar. Pot lucks topped the list for many years, and they were hosted by women in their homes. That is more difficult now because of the size of the membership. Bev and Sandy have hosted a potluck since 1995, and Eva Geer for several years.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
We have learned so much from each other over the years. We have shared our stories, our wisdom, and our experiences. We have laughed together, cried together, and supported each other through good times and bad.
|
<p className="text-md text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
The ice cream socials started in 1989. There are still women who have never cranked or eaten home made ice cream.The “picnic in the park” started in 2000. We have held picnics in a couple State Parks as well as Tom Bass Park in recent years.In 1988 we started attending the TUTS Broadway Musical at Miller Theater in July. We bring a snack supper and a chair and sit on the hill.In 2000 we started eating at Sudie's Catfish House in January. A breather from a busy party season, but a good way to connect.From 1987 to 1994 we had “Second Tuesday Dancing”. First at The Ranch, and then at Ms B's, it was our way to celebrate birthdays of the month. It was well attended.
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
|
||||||
</div>
|
<p className="text-md text-[#48286e] leading-relaxed mt-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
</section>
|
From 1989 to 1993 we had a five day Thanksgiving camp-out at a State Park. We roasted turkey and women came for potluck on Thanksgiving Day.We had from 10 to 20 campers, and maybe 25 for dinner.We have had a Christmas party every year since 1987.We have had several Port of Houston tours on the Sam Houston; several Houston Zoo tours; museum tours; and out-of town tours for bluebonnets, miniature horses, and Blue Bell ice cream. And occasionally, for lack of inspiration for an event social, we simply met for lunch at a local restaurant.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="text-md text-[#48286e] leading-relaxed mt-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
LOAF has always been a social group. We have never had support groups or counseling. We are a place to meet other lesbians over fifty, make friends, and have fun.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</CardSection>
|
||||||
|
|
||||||
{/* Part 7 - With Image */}
|
{/* Part 7 - With Image */}
|
||||||
<section className="py-12">
|
<CardSection>
|
||||||
<div className="max-w-5xl mx-auto">
|
|
||||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
<div className="flex flex-col md:flex-row gap-8 items-center">
|
||||||
<div className="flex flex-col md:flex-row gap-8 items-start">
|
|
||||||
<div className="md:w-1/3">
|
<div className="md:w-1/3">
|
||||||
<img src={part7Img} alt="LOAF Members"
|
<img src={part7Img} alt="LOAF Members"
|
||||||
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
className="rounded-lg w-full" onError={(e) => e.target.style.display = 'none'} />
|
||||||
<p className="text-sm text-[#48286e] mt-2 text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
LOAF Members
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="md:w-2/3">
|
<div className="md:w-2/3">
|
||||||
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
Part 7
|
Part 7
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
LOAF has evolved over the years, but our core mission has remained the same: to provide a welcoming and inclusive community for lesbians over 50. We have adapted to the changing times and the changing needs of our members, but we have never lost sight of what makes LOAF special.
|
The LOAF Library has been an important part of the offering to the women. It started about 1987 when Arden discovered there were books - both fiction and non-fiction - about lesbians. We had one bookstore then -” Wilde 'N Stein” - that had a limited selection of lesbian books. Then Arden discovered Womencraft Books, a mail order book company. This began the collection now in the library. Over the years women have donated books. At one time we took duplicate titles to our book stores (Inkilngs and Book Woman) and traded them for titles we didn't have on the shelf. When the last book store closed we started donating duplicate copies to HATCH, and they are building their library. We have a collection that includes feminist, fantasy/sci-fi, poetry, non-fiction, as well as fiction books. We have a collection of out-of-print periodicals, women's music, and a video library. We have some beautiful “coffee table” books. We have copies of many of the “pulp” books.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-md text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
We are proud of what we have accomplished over the years, and we are excited about the future. We know that there will always be a need for a community like LOAF, and we are committed to being here for as long as we are needed.
|
LOAF also was gifted with a beautiful pianola player piano and about 150 piano rolls. Marie Mariano donated it several years ago. It was in the “Allison” flood, and when it was restored Arden was told it was a beautiful instrument, and quite valuable.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
|
||||||
</div>
|
</CardSection>
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Part 8 */}
|
{/* Part 8 */}
|
||||||
<section className="py-12">
|
<CardSection arrow={false} >
|
||||||
<div className="max-w-5xl mx-auto">
|
|
||||||
<Card className="p-8 bg-white rounded-2xl shadow-lg">
|
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
<h2 className="text-3xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
Part 8
|
Part 8
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
As I reflect on the history of LOAF, I am filled with gratitude for all of the women who have been part of this community over the years. Each one of you has made LOAF what it is today, and I am so proud of what we have created together.
|
LOAF has become a unique organization in that it is the oldest lesbian organization in Houston, and the only one of its kind in Texas. Over the years there has been quite a bit of exposure and promotion for LOAF.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-md text-[#48286e] leading-relaxed mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
LOAF has been a place where we can be ourselves, where we can celebrate who we are, and where we can support each other through all of life's challenges. It has been a place of joy, laughter, friendship, and love.
|
For 17 consecutive years, from 1987 to 2004, we had a Texas Lesbian Conference that rotated between Houston, San Antonio, Austin, and Dallas. LOAF presented workshops at five of these conferences. LOAF did a workshop at the National Lesbian Conference in Atlanta in 1991. We did a workshop at at the PFLAG “Healing the Hurt” conference in 1994. We did a program at the Silver Threads conference in St Petersburg, FL. We have done programs at three OLOC conferences. Charlotte and Arden participated in a live TV show about senior GLBT persons in Dallas. We participated in a documentary on GLBT seniors produced in Canada. And another documentary for Golden Threads at Cape Cod. We participated on a panel for the Women’s Studies Department at the University of Houston for their “Living archive” series. We have done several programs for the Women’s Group in Houston, and appeared on the After Hours radio show on KPFT several times.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-md text-[#48286e] leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
Thank you for being part of LOAF. Thank you for making this community what it is. And thank you for continuing to support LOAF into the future.
|
All of these appearances are documented and part of the LOAF Archives located at the University of Houston Special Collections Library. One result of these workshops and programs has been the formation of similar groups for mid-life and old lesbians throughout the country. But most important is the connection with other lesbians of our generation and avoiding isolation..
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
|
||||||
</div>
|
</CardSection>
|
||||||
</section>
|
|
||||||
|
</main>
|
||||||
|
|
||||||
{/* CTA Section */}
|
{/* CTA Section */}
|
||||||
<section className="py-12 bg-[#48286e] rounded-2xl">
|
<section className="py-20 bg-[#48286e] mx-0">
|
||||||
<div className="max-w-5xl mx-auto px-8">
|
<div className="max-w-7xl mx-auto px-8">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
<div className="flex gap-8 md:flex-row flex-col">
|
||||||
<Card className="p-8 text-center bg-white rounded-2xl shadow-lg hover:shadow-xl transition-shadow">
|
<Card className="p-8 text-center bg-white rounded-2xl shadow-lg hover:shadow-xl transition-shadow">
|
||||||
<h3 className="text-2xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h3 className="text-2xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
A Life Remembered
|
A Life Remembered
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-[#48286e] mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-[#48286e] mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
@@ -243,7 +261,7 @@ const History = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="p-8 text-center bg-white rounded-2xl shadow-lg hover:shadow-xl transition-shadow">
|
<Card className="p-8 text-center bg-white rounded-2xl shadow-lg hover:shadow-xl transition-shadow">
|
||||||
<h3 className="text-2xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h3 className="text-2xl font-bold text-[#48286e] mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
The Old Lesbian Oral Herstory Project
|
The Old Lesbian Oral Herstory Project
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-[#48286e] mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-[#48286e] mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
@@ -258,7 +276,6 @@ const History = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
|
||||||
|
|
||||||
<PublicFooter />
|
<PublicFooter />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,26 +7,89 @@ import PublicFooter from '../components/PublicFooter';
|
|||||||
|
|
||||||
const Landing = () => {
|
const Landing = () => {
|
||||||
// LOAF brand assets (local)
|
// LOAF brand assets (local)
|
||||||
const taglineImage = `${process.env.PUBLIC_URL}/tagline-image.png`;
|
const taglineImage = `${process.env.PUBLIC_URL}/web_elements_tagline.png`;
|
||||||
const shootingStar = `${process.env.PUBLIC_URL}/shooting-star.png`;
|
const shootingStar = `${process.env.PUBLIC_URL}/shooting_star_2.png`;
|
||||||
const iconMeetGreet = `${process.env.PUBLIC_URL}/icon-meet-greet.png`;
|
const iconMeetGreet = `${process.env.PUBLIC_URL}/icon-meet-greet.png`;
|
||||||
const iconSocials = `${process.env.PUBLIC_URL}/icon-socials.png`;
|
const iconSocials = `${process.env.PUBLIC_URL}/icon-socials.png`;
|
||||||
const iconActive = `${process.env.PUBLIC_URL}/icon-active.png`;
|
const iconActive = `${process.env.PUBLIC_URL}/icon-active.png`;
|
||||||
const heroLoaf = `${process.env.PUBLIC_URL}/hero-loaf.png`;
|
const heroLoaf = `${process.env.PUBLIC_URL}/hero-loaf.png`;
|
||||||
|
const friendships = `${process.env.PUBLIC_URL}/friendships.png`;
|
||||||
|
const InfoCard = ({ iconSrc, infoTitle, description }) => (
|
||||||
|
<Card className="relative bg-white rounded-2xl overflow-visible flex flex-col gap-3.5 items-center pt-16 pb-0 w-full max-w-none lg:max-w-[363px]">
|
||||||
|
<div className="absolute -top-20 md:-top-40 flex justify-center w-full">
|
||||||
|
<img
|
||||||
|
src={iconSrc}
|
||||||
|
alt={infoTitle}
|
||||||
|
className=" w-40 md:w-64 lg:max-w-[330px] h-auto aspect-[10/9] object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 flex flex-col pt-10 gap-4.5 w-full">
|
||||||
|
<h5 className="text-[#48286e] text-[28px] leading-10 pb-10 font-semibold text-center" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
|
{infoTitle}
|
||||||
|
</h5>
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
const infoCardData = [
|
||||||
|
{
|
||||||
|
iconSrc: iconMeetGreet,
|
||||||
|
infoTitle: 'Meet and Greet',
|
||||||
|
description: (
|
||||||
|
<p className="text-[#48286e] text-lg text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
The MEET and GREETs provide opportunities for prospective members to get acquainted with LOAF, have conversations
|
||||||
|
with members, and ask the board of directors questions. They are held the 3rd Sunday of the month and usually
|
||||||
|
take place at a restaurant or other fun places conducive to its purpose. Please email{' '}
|
||||||
|
<a href="mailto:info@loaftx.org" className="underline">info@loaftx.org</a> for upcoming times and locations.
|
||||||
|
</p>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconSrc: iconSocials,
|
||||||
|
infoTitle: 'Socials',
|
||||||
|
description: (
|
||||||
|
<p className="text-[#48286e] text-lg text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Our social events provide opportunities for members to explore Houston and connect with other lesbians. Past
|
||||||
|
social events include bowling, museums, painting lessons, sporting events, Miller Outdoor Theater, bingo and board
|
||||||
|
games, pool parties, putt putt golf, camping and holiday get togethers. No matter your age or ability, there is
|
||||||
|
something for everyone.
|
||||||
|
</p>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconSrc: iconActive,
|
||||||
|
infoTitle: 'Active LOAFers',
|
||||||
|
description: (
|
||||||
|
<p className="text-[#48286e] text-lg text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
ActiveLOAFers events provide members with opportunities to be physically active. Past activities have included
|
||||||
|
hiking/walking in the park, swimming (or floating), pickleball, kayaking, bike riding, axe throwing, and strolling
|
||||||
|
through the botanic gardens or the Arboretum.
|
||||||
|
</p>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-white">
|
||||||
<PublicNavbar />
|
<PublicNavbar />
|
||||||
|
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section className="bg-gradient-to-b from-[#48286e] to-[#664fa3] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-8 md:py-12 lg:py-0 flex flex-col lg:flex-row gap-8 md:gap-12 lg:gap-16 items-center justify-center">
|
<section className="relative bg-gradient-to-b from-[#48286e] to-[#664fa3] py-20 sm:py-8 md:py-12 lg:py-16 flex flex-col lg:flex-row gap-8 md:gap-12 lg:gap-16 items-center justify-center w-full">
|
||||||
<div className="py-8 md:py-10 flex flex-col gap-6 sm:gap-8 items-center justify-center w-full lg:w-[420px] lg:flex-shrink-0">
|
{/* Friendships background image */}
|
||||||
|
<div className="absolute inset-0 z-0 flex overflow-hidden top-[-32rem] lg:-top-32">
|
||||||
|
<img src={friendships} alt="Friendships" className="lg:max-w-screen opacity-15 max-w-full max-h-full object-contain" />
|
||||||
|
</div>
|
||||||
|
{/* Blur Overlay */}
|
||||||
|
<div className="absolute inset-0 z-[1] bg-white/5 backdrop-blur-xs"></div>
|
||||||
|
{/* Left column Loaf Image */}
|
||||||
|
<div className="relative z-10 lg:py-20 py-7 flex flex-col gap-6 sm:gap-8 items-center justify-center w-full lg:w-[530px] lg:flex-shrink-0">
|
||||||
<div className="flex flex-col gap-6 items-center">
|
<div className="flex flex-col gap-6 items-center">
|
||||||
<img src={heroLoaf} alt="LOAF" className="w-full max-w-[334px] h-auto object-contain" />
|
<img src={heroLoaf} alt="LOAF" className="w-full max-w-xs md:max-w-[370px] h-auto object-contain" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 items-center justify-center w-full max-w-[339px]">
|
<div className="flex flex-col gap-4 items-center justify-center w-full max-w-[339px]">
|
||||||
<Link to="/register" className="w-full">
|
<Link to="/become-a-member" className="w-full">
|
||||||
<Button className="bg-[#DDD8EB] hover:bg-white text-[#422268] rounded-full px-6 py-6 sm:py-[32px] text-base sm:text-lg font-medium w-full transition-colors">
|
<Button style={{ fontFamily: "'Nunito sans', sans-serif" }} className="bg-[#DDD8EB] hover:bg-white text-[#422268] rounded-full px-6 py-6 sm:py-[32px] text-base sm:text-lg font-medium w-full transition-colors">
|
||||||
Become a Member
|
Become a Member
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -35,74 +98,43 @@ const Landing = () => {
|
|||||||
LOAF is supported by the Hollyfield Foundation
|
LOAF is supported by the Hollyfield Foundation
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="py-8 md:py-12 lg:py-16 flex items-center justify-center w-full lg:w-[594px] h-auto">
|
{/* Right Column Loaf Tagline */}
|
||||||
<img src={taglineImage} alt="LOAF Tagline" className="w-full max-w-[483px] h-auto object-contain" />
|
<div className="relative z-10 py-8 md:py-12 lg:py-16 flex items-center justify-center w-full lg:max-w-[815px] h-auto">
|
||||||
|
<img src={taglineImage} alt="LOAF Tagline" className="relative z-10 w-full h-auto object-cover" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* About Section */}
|
{/* About Section */}
|
||||||
<section id="about" className="bg-gradient-to-b from-white to-[#f1eef9] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 pt-12 sm:pt-16 md:pt-20 lg:pt-30 pb-0 flex flex-col gap-6 sm:gap-8">
|
<section id="about" className="bg-gradient-to-b pb-10 lg:pb-44 from-white to-[#f1eef9] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 pt-4 sm:pt-16 md:pt-20 lg:pt-30 flex flex-col">
|
||||||
<div className="flex flex-col items-center pt-12">
|
<div className="flex flex-col items-center pt-4">
|
||||||
<h3 className="text-[#48286e] text-3xl sm:text-4xl md:text-5xl font-extrabold text-center" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h3 className="text-[#48286e] px-4 pb-6 md:py-8 text-4xl leading-[60px] md:text-5xl lg:text-6xl font-extrabold text-center" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
Welcome to LOAF
|
Welcome to LOAF
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[rgba(0,0,0,0.55)] text-lg text-center font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<p className="text-[rgba(0,0,0,0.55)] text-lg lg:text-2xl text-center font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
LOAF is Houston's social networking group for lesbians who are 50 years of age and older. LOAF hosts three main activities each month, Meet and Greets, Socials, and ActiveLOAFers. TheaterLOAFers coordinate events throughout the year.
|
LOAF is Houston's social networking group for lesbians who are 50 years of age and older. LOAF hosts three main activities each month, Meet and Greets, Socials, and ActiveLOAFers. TheaterLOAFers coordinate events throughout the year.
|
||||||
</p>
|
</p>
|
||||||
<img src={shootingStar} alt="Decorative element" className="w-full h-[197px] object-contain" />
|
<img src={shootingStar} alt="Decorative element" className="w-full h-[197px] object-contain" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Feature Cards Section */}
|
{/* Feature Cards Section */}
|
||||||
<section className="bg-gradient-to-b from-[#f1eef9] to-[#ddd8eb] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-12 sm:py-16 md:py-20 lg:py-30 flex flex-col md:flex-row gap-6 sm:gap-8 items-start justify-center">
|
<section className="bg-gradient-to-b pb-20 from-[#f1eef9] to-[#ddd8eb] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-4 md:py-20 lg:py-30 flex flex-col sm:w-full lg:flex-row gap-40 md:gap-64 lg:gap-8 items-stretch justify-center">
|
||||||
<Card className="bg-white rounded-2xl overflow-hidden flex flex-col gap-3.5 items-center pt-5 pb-0 w-full max-w-[363px]">
|
{infoCardData.map((card) => (
|
||||||
<img src={iconMeetGreet} alt="Meet and Greet" className="w-full max-w-[300px] h-auto aspect-[10/9] object-contain" />
|
<InfoCard key={card.infoTitle} {...card} />
|
||||||
<div className="p-6 flex flex-col gap-4.5 w-full">
|
))}
|
||||||
<h5 className="text-[#48286e] text-2xl font-semibold text-center" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
Meet and Greet
|
|
||||||
</h5>
|
|
||||||
<p className="text-[#48286e] text-lg text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
The MEET and GREETs provide opportunities for prospective members to get acquainted with LOAF, have conversations with members, and ask the board of directors questions. They are held the 3rd Sunday of the month and usually take place at a restaurant or other fun places conducive to its purpose. Please email{' '}
|
|
||||||
<a href="mailto:info@loaftx.org" className="underline">info@loaftx.org</a> for upcoming times and locations.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="bg-white rounded-2xl overflow-hidden flex flex-col gap-3.5 items-center pt-5 pb-0 w-full max-w-[363px]">
|
|
||||||
<img src={iconSocials} alt="Socials" className="w-full max-w-[300px] h-auto aspect-[10/9] object-contain" />
|
|
||||||
<div className="p-6 flex flex-col gap-4.5 w-full">
|
|
||||||
<h5 className="text-[#48286e] text-2xl font-semibold text-center" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
Socials
|
|
||||||
</h5>
|
|
||||||
<p className="text-[#48286e] text-lg text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
Our social events provide opportunities for members to explore Houston and connect with other lesbians. Past social events include, bowling, museums, painting lessons, sporting events, Miller Outdoor Theater, bingo and board games, pool parties, putt putt golf, camping and holiday get togethers. No matter your age or ability, there is something for everyone.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="bg-white rounded-2xl overflow-hidden flex flex-col gap-3.5 items-center pt-5 pb-0 w-full max-w-[363px]">
|
|
||||||
<img src={iconActive} alt="Active LOAFers" className="w-full max-w-[300px] h-auto aspect-[10/9] object-contain" />
|
|
||||||
<div className="p-6 flex flex-col gap-4.5 w-full">
|
|
||||||
<h5 className="text-[#48286e] text-2xl font-semibold text-center" style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
Active LOAFers
|
|
||||||
</h5>
|
|
||||||
<p className="text-[#48286e] text-lg text-center" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
ActiveLOAFers events provide members with opportunities to be physically active. Past activities have included, hiking/walking in the park, swimming (or floating), pickleball, kayaking, bike riding, axe throwing, and strolling through the botanic gardens or the Arboretum.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* CTA Section */}
|
{/* CTA Section */}
|
||||||
<section className="bg-gradient-to-b from-[#644c9f] to-[#48286e] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-12 sm:py-16 md:py-20 lg:py-30 flex items-center justify-center">
|
<section className="bg-gradient-to-b from-[#644c9f] to-[#48286e] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-12 sm:py-16 md:py-20 lg:py-30 flex items-center justify-center">
|
||||||
<div className="flex flex-col lg:flex-row gap-8 sm:gap-10 md:gap-12 items-center justify-center w-full max-w-6xl">
|
<div className="flex flex-col-reverse md:flex-col lg:flex-row gap-8 sm:gap-10 md:gap-12 items-center justify-center w-full max-w-6xl">
|
||||||
<Link to="/register" className="w-full sm:w-auto">
|
<Link to="/register" className="w-full sm:w-auto flex items-center justify-center">
|
||||||
<Button className="bg-[#DDD8EB] hover:bg-white text-[#422268] rounded-full px-6 py-6 sm:py-[32px] text-base sm:text-lg font-medium w-full sm:w-[392px] transition-colors">
|
<Button className="bg-[#DDD8EB] hover:bg-white text-[#422268] rounded-full
|
||||||
|
py-8 text-xl font-normal px-12 sm:w-[392px] transition-colors ">
|
||||||
Become a Member
|
Become a Member
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex-1 flex items-center justify-center">
|
<div className="flex-1 flex items-center justify-center">
|
||||||
<h4 className="text-white text-2xl sm:text-3xl md:text-4xl font-bold text-center lg:text-left max-w-[718px]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h4 className="text-white text-3xl px-4 font-bold text-center lg:text-left leading-normal max-w-[718px]" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
No matter your age or ability, there is something for everyone.
|
No matter your age or ability, there is something for everyone.
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,33 +10,31 @@ const MissionValues = () => {
|
|||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-white">
|
||||||
<PublicNavbar />
|
<PublicNavbar />
|
||||||
|
|
||||||
<main className="bg-gradient-to-b from-[#f9fafb] to-[#ddd8eb] px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-8 sm:py-12 md:py-16">
|
<main className="bg-gradient-to-b from-[#f9fafb] to-[#ddd8eb] px-4 sm:px-6 py-8 sm:py-12 md:py-20">
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-[1400px] mx-auto">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
<div className="flex md:flex-row flex-col gap-10 items-stretch">
|
||||||
{/* Left Card - Mission (Purple Gradient) */}
|
{/* Left Card - Mission (Purple Gradient) */}
|
||||||
<Card className="bg-gradient-to-br from-[#664fa3] to-[#48286e] p-8 rounded-2xl shadow-lg">
|
<Card className=" bg-gradient-to-br from-[#664fa3] to-[#48286e] p-16 rounded-2xl shadow-lg flex flex-col items-center justify-between flex-1 w-full md:w-1/2 ">
|
||||||
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold text-white text-center mb-6"
|
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold text-white text-center mb-6"
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
LOAF Mission
|
LOAF Mission
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-white text-lg text-center leading-relaxed"
|
<p className="text-white text-xl text-center leading-relaxed"
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
LOAF's mission is to alleviate isolation and enrich the lives of lesbians
|
LOAF’s mission is to alleviate isolation and enrich the lives of lesbians over the age of 50 by providing several social networking events every month in Houston and the surrounding areas.
|
||||||
over the age of 50 by providing several social networking events every month
|
|
||||||
in Houston and the surrounding areas.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="flex justify-center mb-6">
|
<div className="flex justify-center mb-6">
|
||||||
<img src={loafLogo} alt="LOAF Logo" className="w-32 h-32 sm:w-40 sm:h-40 md:w-48 md:h-48 lg:w-64 lg:h-64 object-contain" />
|
<img src={loafLogo} alt="LOAF Logo" className="size-32 sm:size-40 md:size-64 lg:size-96 object-contain" />
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Right Card - Values */}
|
{/* Right Card - Values */}
|
||||||
<Card className="bg-white p-8 rounded-2xl shadow-lg">
|
<Card className="bg-white p-16 rounded-2xl shadow-lg flex-1 w-full md:w-1/2 ">
|
||||||
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold text-[#48286e] text-center mb-6"
|
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold text-[#48286e] text-center mb-6"
|
||||||
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
LOAF Values
|
LOAF Values
|
||||||
</h2>
|
</h2>
|
||||||
<ol className="list-decimal list-inside space-y-3 text-lg text-[#48286e]"
|
<ol className="list-decimal list-inside space-y-8 text-lg text-[#48286e]"
|
||||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
<li>Safe environments for lesbians to gather for a variety of social activities and interaction.</li>
|
<li>Safe environments for lesbians to gather for a variety of social activities and interaction.</li>
|
||||||
<li>Social support for lesbians.</li>
|
<li>Social support for lesbians.</li>
|
||||||
|
|||||||
81
src/pages/NotFound.js
Normal file
81
src/pages/NotFound.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Button } from '../components/ui/button';
|
||||||
|
import { Card } from '../components/ui/card';
|
||||||
|
import { Home, ArrowLeft, Search } from 'lucide-react';
|
||||||
|
|
||||||
|
const NotFound = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-[#F9F8FB] to-white flex items-center justify-center p-4">
|
||||||
|
<Card className="w-full max-w-2xl p-12 bg-white rounded-2xl border border-[#ddd8eb] 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-[#ddd8eb] to-[#f9f8fb] 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-[#664fa3] opacity-30" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Message */}
|
||||||
|
<h2
|
||||||
|
className="text-3xl font-semibold text-[#422268] mb-4"
|
||||||
|
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||||
|
>
|
||||||
|
Page Not Found
|
||||||
|
</h2>
|
||||||
|
<p
|
||||||
|
className="text-lg text-[#664fa3] mb-8 max-w-md mx-auto"
|
||||||
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
|
>
|
||||||
|
Oops! The page you're looking for doesn't exist. It might have been moved or deleted.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<Button
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
variant="outline"
|
||||||
|
className="rounded-xl border-2 border-[#664fa3] text-[#664fa3] hover:bg-[#f9f8fb] 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-[#664fa3] to-[#422268] hover:from-[#422268] hover:to-[#664fa3] text-white px-6 py-6"
|
||||||
|
>
|
||||||
|
<Home className="h-5 w-5 mr-2" />
|
||||||
|
Back to Home
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Help Text */}
|
||||||
|
<div className="mt-8 pt-8 border-t border-[#ddd8eb]">
|
||||||
|
<p
|
||||||
|
className="text-sm text-[#664fa3]"
|
||||||
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
|
>
|
||||||
|
Need help? Contact us at{' '}
|
||||||
|
<a
|
||||||
|
href="mailto:support@loaftx.org"
|
||||||
|
className="text-[#664fa3] hover:text-[#422268] font-semibold underline"
|
||||||
|
>
|
||||||
|
support@loaftx.org
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotFound;
|
||||||
@@ -253,7 +253,13 @@ const Plans = () => {
|
|||||||
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading plans...</p>
|
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Loading plans...</p>
|
||||||
</div>
|
</div>
|
||||||
) : plans.length > 0 ? (
|
) : plans.length > 0 ? (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8 max-w-5xl mx-auto">
|
<div className={`grid gap-6 sm:gap-8 mx-auto ${
|
||||||
|
plans.length === 1
|
||||||
|
? 'grid-cols-1 max-w-md'
|
||||||
|
: plans.length === 2
|
||||||
|
? 'grid-cols-1 sm:grid-cols-2 max-w-3xl'
|
||||||
|
: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 max-w-5xl'
|
||||||
|
}`}>
|
||||||
{plans.map((plan) => {
|
{plans.map((plan) => {
|
||||||
const minimumPrice = plan.minimum_price_cents || plan.price_cents || 3000;
|
const minimumPrice = plan.minimum_price_cents || plan.price_cents || 3000;
|
||||||
const suggestedPrice = plan.suggested_price_cents || minimumPrice;
|
const suggestedPrice = plan.suggested_price_cents || minimumPrice;
|
||||||
|
|||||||
@@ -7,242 +7,190 @@ export default function PrivacyPolicy() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PublicNavbar />
|
<PublicNavbar />
|
||||||
<div className="min-h-screen bg-[#F8F7FB]">
|
<main className="bg-gradient-to-bl from-[#F9FAFB] to-[#DDD8EB] text-[#48286E]">
|
||||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-12">
|
<div className="mx-auto w-full max-w-5xl px-4 sm:px-6 lg:px-8 py-10">
|
||||||
{/* Header */}
|
<header className="border-b pb-6">
|
||||||
<div className="mb-8">
|
<h1 className="text-3xl sm:text-4xl font-bold tracking-tight" style={{fontFamily: 'Poppins'}}>
|
||||||
<h1 className="text-3xl sm:text-4xl md:text-5xl font-bold text-[#422268] mb-4"
|
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
Privacy Policy
|
|
||||||
</h1>
|
|
||||||
<p className="text-base sm:text-lg text-gray-600" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
LOAFers, Inc. Website Privacy Policy
|
LOAFers, Inc. Website Privacy Policy
|
||||||
</p>
|
</h1>
|
||||||
</div>
|
</header>
|
||||||
|
|
||||||
{/* Content */}
|
<div className="prose text-[#48286E] max-w-none prose-h2:mt-10 prose-h2:scroll-mt-24 prose-h3:mt-6">
|
||||||
<div className="bg-white rounded-lg border border-[#ddd8eb] p-6 sm:p-8 md:p-10 space-y-8">
|
<section className="mt-8">
|
||||||
|
<p>
|
||||||
{/* Introduction */}
|
This Privacy Policy ("Policy") applies to Membership Applications, and LOAFers, Inc. ("Company") and
|
||||||
<section>
|
governs data collection and usage. The Company's application is a Membership request, Membership online
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
profile, and Consent to receive eNewsletters. By using the Company application, you consent to the data
|
||||||
<p className="text-gray-700 leading-relaxed">
|
practices described in the statement.
|
||||||
This Privacy Policy ("Policy") applies to Membership Applications, and LOAFers, Inc. ("Company") and governs data collection and usage. The Company's application is a Membership request, Membership online profile, and Consent to receive eNewsletters. By using the Company application, you consent to the data practices described in the statement.
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-700 leading-relaxed">
|
<p>
|
||||||
We reserve the right to change this policy at any given time, of which you will be promptly updated. If you want to make sure that you are up to date with the latest changes, we advise you to frequently visit this page.
|
We reserve the right to change this policy at any given time, of which you will be promptly updated. If
|
||||||
|
you want to make sure that you are up to date with the latest changes, we advise you to frequently visit
|
||||||
|
this page.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Section 1: What User Data We Collect */}
|
<section id="user-data" className="scroll-mt-24">
|
||||||
<section>
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">What User Data We Collect</h2>
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4 flex items-center gap-2"
|
<p>When you visit the Site, we may collect the following data:</p>
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
<ul className="list-disc pl-6 space-y-1">
|
||||||
<span>💻</span> What User Data We Collect
|
<li>Your IP address.</li>
|
||||||
</h2>
|
<li>Your contact information and email address.</li>
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
When you visit the Site, we may collect the following data:
|
|
||||||
</p>
|
|
||||||
<ul className="list-disc list-inside space-y-2 text-gray-700 ml-4">
|
|
||||||
<li>Your <strong>IP address</strong></li>
|
|
||||||
<li>Your <strong>contact information and email address</strong></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p className="text-gray-700 leading-relaxed mt-4">
|
<p>When you apply for membership, we collect the following data:</p>
|
||||||
When you apply for membership, we collect the following data:
|
<ul className="list-disc pl-6 space-y-1">
|
||||||
</p>
|
<li>First and last name</li>
|
||||||
<ul className="list-disc list-inside space-y-2 text-gray-700 ml-4">
|
<li>Mailing address</li>
|
||||||
<li><strong>First and last name</strong></li>
|
<li>Email</li>
|
||||||
<li><strong>Mailing address</strong></li>
|
<li>Phone number</li>
|
||||||
<li><strong>Email</strong></li>
|
<li>Birthday</li>
|
||||||
<li><strong>Phone number</strong></li>
|
|
||||||
<li><strong>Birthday</strong></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p className="text-gray-700 leading-relaxed mt-4">
|
<p>If you choose to pay your membership administrative fee online, we have access to:</p>
|
||||||
If you choose to pay your membership administrative fee online, we have access to:
|
<ul className="list-disc pl-6 space-y-1">
|
||||||
</p>
|
<li>Partial credit card information</li>
|
||||||
<ul className="list-disc list-inside space-y-2 text-gray-700 ml-4">
|
|
||||||
<li><strong>Partial credit card information</strong></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p className="text-gray-700 leading-relaxed mt-4">
|
<p>You may also choose to provide the following:</p>
|
||||||
You may also choose to provide the following:
|
<ul className="list-disc pl-6 space-y-1">
|
||||||
</p>
|
<li>Partners name</li>
|
||||||
<ul className="list-disc list-inside space-y-2 text-gray-700 ml-4">
|
<li>Photo</li>
|
||||||
<li><strong>Partner's name</strong></li>
|
<li>Self-bio</li>
|
||||||
<li><strong>Photo</strong></li>
|
<li>Consent to receive our eNewsletter</li>
|
||||||
<li><strong>Self-bio</strong></li>
|
<li>Consent to display an online profile visible only to membership</li>
|
||||||
<li><strong>Consent to receive our eNewsletter</strong></li>
|
|
||||||
<li><strong>Consent to display an online profile visible only to membership</strong></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Section 2: Why We Collect Your Data */}
|
<section id="why-collect" className="scroll-mt-24">
|
||||||
<section>
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">Why We Collect Your Data</h2>
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4 flex items-center gap-2"
|
<ul className="list-disc pl-6 space-y-1">
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
<li>
|
||||||
<span>🎯</span> Why We Collect Your Data
|
To send you announcement emails containing the information about our events and information we think you
|
||||||
</h2>
|
will find interesting.
|
||||||
<div className="prose max-w-none" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
</li>
|
||||||
<ul className="list-disc list-inside space-y-2 text-gray-700 ml-4">
|
<li>To contact you to fill out surveys about our membership</li>
|
||||||
<li>To send you announcement emails containing the information about our events and information we think you will find interesting.</li>
|
|
||||||
<li>To contact you to fill out surveys about our membership.</li>
|
|
||||||
<li>To customize our blog according to your online behavior and personal preferences.</li>
|
<li>To customize our blog according to your online behavior and personal preferences.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Section 3: Sharing Information with Third Parties */}
|
<section id="third-parties" className="scroll-mt-24">
|
||||||
<section>
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">Sharing Information with Third Parties</h2>
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4 flex items-center gap-2"
|
<p>The Company does not sell, rent, or lease personal data to third parties.</p>
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
<p>
|
||||||
<span>🤝</span> Sharing Information with Third Parties
|
The Company may share data with trusted partners to help perform statistical analysis, provide customer
|
||||||
</h2>
|
support.
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
The Company does not sell, rent, or lease personal data to third parties.
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-700 leading-relaxed">
|
<p>
|
||||||
The Company may share data with trusted partners to help perform statistical analysis, provide customer support.
|
The Company uses Stripe to process online payments at which time users would no longer be governed by the
|
||||||
|
Company's Privacy Policy.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-700 leading-relaxed">
|
<p>The Company may disclose your personal information, without notice, if required to do so by law.</p>
|
||||||
The Company uses <strong>Stripe</strong> to process online payments at which time users would no longer be governed by the Company's Privacy Policy.
|
|
||||||
</p>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
The Company may disclose your personal information, without notice, if required to do so by law.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Section 4: Safeguarding and Securing the Data */}
|
<section id="safeguarding" className="scroll-mt-24">
|
||||||
<section>
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">Safeguarding and Securing the Data</h2>
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4 flex items-center gap-2"
|
<p>
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
LOAFers, Inc. is committed to securing your data and keeping it confidential. LOAFers, Inc. has done all
|
||||||
<span>🔒</span> Safeguarding and Securing the Data
|
in its power to prevent data theft, unauthorized access, and disclosure by implementing the latest
|
||||||
</h2>
|
technologies and software, which help us safeguard all the information we collect online.
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
LOAFers, Inc. is committed to securing your data and keeping it confidential. LOAFers, Inc. has done all in its power to prevent data theft, unauthorized access, and disclosure by implementing the latest technologies and software, which help us safeguard all the information we collect online.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Section 5: Our Cookie Policy */}
|
<section id="cookies" className="scroll-mt-24">
|
||||||
<section>
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">Our Cookie Policy</h2>
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4 flex items-center gap-2"
|
<p>
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
Once you agree to allow our blog to use cookies, you also agree to use the data it collects regarding your
|
||||||
<span>🍪</span> Our Cookie Policy
|
online behavior (analyze web traffic, web pages you visit and spend the most time on).
|
||||||
</h2>
|
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
Once you agree to allow our blog to use cookies, you also agree to use the data it collects regarding your online behavior (analyze web traffic, web pages you visit and spend the most time on).
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-700 leading-relaxed">
|
<p>
|
||||||
The data we collect by using cookies is used to customize our blog to your needs. After we use the data for statistical analysis, the data is completely removed from our systems.
|
The data we collect by using cookies is used to customize our blog to your needs. After we use the data
|
||||||
|
for statistical analysis, the data is completely removed from our systems.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-700 leading-relaxed">
|
<p>
|
||||||
Please note that cookies don't allow us to gain control of your computer in any way. They are strictly used to monitor which pages you find useful and which you do not so that we can provide a better experience for you.
|
Please note that cookies don't allow us to gain control of your computer in any way. They are strictly
|
||||||
|
used to monitor which pages you find useful and which you do not so that we can provide a better
|
||||||
|
experience for you.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-700 leading-relaxed">
|
<p>
|
||||||
If you want to disable cookies, you can do it by accessing the settings of your internet browser. You can visit{' '}
|
If you want to disable cookies, you can do it by accessing the settings of your internet browser. You can
|
||||||
<a href="https://www.internetcookies.com" target="_blank" rel="noopener noreferrer" className="text-[#664fa3] hover:text-[#422268] font-semibold">
|
visit{" "}
|
||||||
|
<a className="" href="https://www.internetcookies.com">
|
||||||
https://www.internetcookies.com
|
https://www.internetcookies.com
|
||||||
</a>, which contains comprehensive information on how to do this on a wide variety of browsers and devices.
|
</a>
|
||||||
|
, which contains comprehensive information on how to do this on a wide variety of browsers and devices.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Section 6: Links to Other Websites */}
|
<section id="other-sites" className="scroll-mt-24">
|
||||||
<section>
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">Links to Other Websites</h2>
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4 flex items-center gap-2"
|
<p>
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
Our blog contains links that lead to other websites. If you click on these links LOAFers, Inc. is not held
|
||||||
<span>🔗</span> Links to Other Websites
|
responsible for your data and privacy protection. Visiting those websites is not governed by this privacy
|
||||||
</h2>
|
policy agreement. Make sure to read the privacy policy documentation of the website you go to from our
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
website.
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
Our blog contains links that lead to other websites. If you click on these links LOAFers, Inc. is not held responsible for your data and privacy protection. Visiting those websites is not governed by this privacy policy agreement. Make sure to read the privacy policy documentation of the website you go to from our website.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Section 7: Restricting the Collection of your Personal Data */}
|
<section id="restricting" className="scroll-mt-24">
|
||||||
<section>
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4 flex items-center gap-2"
|
Restricting the Collection of your Personal Data
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
<span>🚫</span> Restricting the Collection of your Personal Data
|
|
||||||
</h2>
|
</h2>
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p>
|
||||||
<p className="text-gray-700 leading-relaxed">
|
At some point, you might wish to restrict the use and collection of your personal data. You can achieve
|
||||||
At some point, you might wish to restrict the use and collection of your personal data. You can achieve this by doing the following:
|
this by doing the following:
|
||||||
</p>
|
</p>
|
||||||
<ul className="list-disc list-inside space-y-2 text-gray-700 ml-4">
|
<ul className="list-disc pl-6 space-y-1">
|
||||||
<li>Log in to your online profile and make any changes you wish to your profile information.</li>
|
<li>Log in to your online profile and make any changes you wish to your profile information.</li>
|
||||||
<li>If you have already agreed to share your information with us, feel free to contact us via email and we will be more than happy to change this for you.</li>
|
<li>
|
||||||
|
If you have already agreed to share your information with us, feel free to contact us via email and we
|
||||||
|
will be more than happy to change this for you.
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Section 8: Children Under Thirteen */}
|
<section id="children" className="scroll-mt-24">
|
||||||
<section>
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">Children Under Thirteen</h2>
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4 flex items-center gap-2"
|
<p>The Company does not knowingly collect information from children under the age of 13.</p>
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
<span>👶</span> Children Under Thirteen
|
|
||||||
</h2>
|
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
The Company does not knowingly collect information from children under the age of 13.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Section 9: Changes to this Statement */}
|
<section id="changes" className="scroll-mt-24">
|
||||||
<section>
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">Changes to this Statement</h2>
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4 flex items-center gap-2"
|
<p>
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
The Company may make changes to this Policy. When this occurs the effective date of this policy will be
|
||||||
<span>🗓️</span> Changes to this Statement
|
updated.
|
||||||
</h2>
|
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
The Company may make changes to this Policy. When this occurs the effective date of this policy will be updated.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Section 10: Contact Information */}
|
<section id="contact" className="scroll-mt-24">
|
||||||
<section>
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">Contact Information</h2>
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4 flex items-center gap-2"
|
<p>If you have any question, please contact LOAFers, Inc. at:</p>
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
<div className="not-prose mt-4">
|
||||||
<span>📧</span> Contact Information
|
<p className="font-semibold mb-2">LOAFers, Inc.</p>
|
||||||
</h2>
|
<p className="">PO BOX 7207</p>
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="">Houston, TX 77248-7207</p>
|
||||||
<p className="text-gray-700 leading-relaxed">
|
<p className="mt-3">Or</p>
|
||||||
If you have any questions, please contact LOAFers, Inc. at:
|
<p className="mt-3">
|
||||||
|
<a className="" href="mailto:info@loaftx.org">
|
||||||
|
info@loaftx.org
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<div className="bg-[#F8F7FB] p-4 rounded-lg border border-[#ddd8eb]">
|
|
||||||
<p className="font-semibold text-gray-800">LOAFers, Inc.</p>
|
|
||||||
<p className="text-gray-700">PO Box 7207</p>
|
|
||||||
<p className="text-gray-700">Houston, TX 77248-7207</p>
|
|
||||||
<p className="text-gray-700 mt-2">
|
|
||||||
Email: <a href="mailto:info@loaftx.org" className="text-[#664fa3] hover:text-[#422268] font-semibold">info@loaftx.org</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Back to Home Link */}
|
{/* Back to Home Link */}
|
||||||
<div className="mt-8 text-center">
|
<div className="mt-8 text-center">
|
||||||
<Link to="/" className="text-[#664fa3] hover:text-[#422268] font-semibold transition-colors inline-flex items-center gap-2"
|
<Link
|
||||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
to="/"
|
||||||
|
className="text-[#664fa3] hover:text-[#422268] font-semibold transition-colors inline-flex items-center gap-2"
|
||||||
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
|
>
|
||||||
<span>←</span> Back to Home
|
<span>←</span> Back to Home
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
</div>
|
|
||||||
<PublicFooter />
|
<PublicFooter />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import PublicFooter from '../components/PublicFooter';
|
|||||||
import { Card } from '../components/ui/card';
|
import { Card } from '../components/ui/card';
|
||||||
import { ChevronDown, ExternalLink, Phone, Mail, MapPin } from 'lucide-react';
|
import { ChevronDown, ExternalLink, Phone, Mail, MapPin } from 'lucide-react';
|
||||||
|
|
||||||
|
import { FaFlag, FaHeartbeat, FaUtensils } from "react-icons/fa";
|
||||||
const Resources = () => {
|
const Resources = () => {
|
||||||
const [openAccordions, setOpenAccordions] = useState({});
|
const [openAccordions, setOpenAccordions] = useState({});
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ const Resources = () => {
|
|||||||
const categories = [
|
const categories = [
|
||||||
{
|
{
|
||||||
title: 'General LGBTQ+',
|
title: 'General LGBTQ+',
|
||||||
|
icon: <FaFlag />,
|
||||||
resources: [
|
resources: [
|
||||||
{
|
{
|
||||||
name: 'SPRY (Seniors Preparing for Rainbow Years)',
|
name: 'SPRY (Seniors Preparing for Rainbow Years)',
|
||||||
@@ -52,6 +54,7 @@ const Resources = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Healthcare',
|
title: 'Healthcare',
|
||||||
|
icon: <FaHeartbeat />,
|
||||||
resources: [
|
resources: [
|
||||||
{
|
{
|
||||||
name: 'LHI (Lesbian Health Initiative)',
|
name: 'LHI (Lesbian Health Initiative)',
|
||||||
@@ -67,6 +70,7 @@ const Resources = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Food Assistance',
|
title: 'Food Assistance',
|
||||||
|
icon: <FaUtensils />,
|
||||||
resources: [
|
resources: [
|
||||||
{
|
{
|
||||||
name: 'Meals on Wheels',
|
name: 'Meals on Wheels',
|
||||||
@@ -99,12 +103,10 @@ const Resources = () => {
|
|||||||
<main className="bg-gradient-to-b from-white via-[#f1eef9] to-[#e8e0f5] px-6 py-16">
|
<main className="bg-gradient-to-b from-white via-[#f1eef9] to-[#e8e0f5] px-6 py-16">
|
||||||
{/* Header Section */}
|
{/* Header Section */}
|
||||||
<section className="max-w-7xl mx-auto mb-12">
|
<section className="max-w-7xl mx-auto mb-12">
|
||||||
<h1 className="text-4xl md:text-5xl font-bold text-[#48286e] text-center mb-6" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h1 className="text-[28px] font-bold text-[#48286e] text-center mb-12" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
Resources
|
|
||||||
</h1>
|
|
||||||
<p className="text-xl text-[#48286e] text-center max-w-3xl mx-auto" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
Tap or click on each purple tab below to open and read its contents
|
Tap or click on each purple tab below to open and read its contents
|
||||||
</p>
|
</h1>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Resources Grid */}
|
{/* Resources Grid */}
|
||||||
@@ -113,7 +115,8 @@ const Resources = () => {
|
|||||||
{categories.map((category, categoryIndex) => (
|
{categories.map((category, categoryIndex) => (
|
||||||
<div key={categoryIndex} className="space-y-6">
|
<div key={categoryIndex} className="space-y-6">
|
||||||
{/* Category Title */}
|
{/* Category Title */}
|
||||||
<h2 className="text-3xl font-bold text-[#48286e] text-center mb-8" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<div className="flex justify-center text-4xl text-[#664ea2]">{category.icon}</div>
|
||||||
|
<h2 className="text-[32px] leading-6 font-bold text-[#48286e] text-center mb-8" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
{category.title}
|
{category.title}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@@ -127,25 +130,25 @@ const Resources = () => {
|
|||||||
{/* Accordion Button */}
|
{/* Accordion Button */}
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleAccordion(categoryIndex, resourceIndex)}
|
onClick={() => toggleAccordion(categoryIndex, resourceIndex)}
|
||||||
className="w-full bg-[#664fa3] hover:bg-[#5a4290] text-white px-6 py-4 rounded-full flex items-center justify-between transition-all shadow-lg hover:shadow-xl"
|
className={`w-full bg-gradient-to-tr from-[#48286E] to-[#664FA3] 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' : ''}`
|
||||||
|
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span className="text-lg font-semibold text-left" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<span className="text-lg font-semibold text-left" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
{resource.name}
|
{resource.name}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className={`h-6 w-6 flex-shrink-0 ml-3 transition-transform duration-300 ${
|
className={`h-6 w-6 flex-shrink-0 ml-3 transition-transform duration-300
|
||||||
isExpanded ? 'rotate-180' : ''
|
${isExpanded ? 'rotate-180' : ''}`}
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Accordion Content */}
|
{/* Accordion Content */}
|
||||||
<div
|
<div
|
||||||
className={`transition-all duration-300 ease-in-out ${
|
className={`transition-all duration-300 ease-in-out ${isExpanded ? 'max-h-[1000px] opacity-100' : 'max-h-0 opacity-0'
|
||||||
isExpanded ? 'max-h-[1000px] opacity-100 mt-3' : 'max-h-0 opacity-0'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Card className="p-6 bg-white rounded-2xl border-2 border-[#ddd8eb] shadow-lg">
|
<Card className="p-6 bg-white rounded-b-2xl rounded-t-none border-none ">
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<p className="text-[#48286e] mb-4 leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-[#48286e] mb-4 leading-relaxed" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
{resource.description}
|
{resource.description}
|
||||||
@@ -172,7 +175,7 @@ const Resources = () => {
|
|||||||
<div className="flex flex-col gap-1 ml-0">
|
<div className="flex flex-col gap-1 ml-0">
|
||||||
{resource.phone && (
|
{resource.phone && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Phone className="h-4 w-4" />
|
<Phone className="size-4" />
|
||||||
<a
|
<a
|
||||||
href={`tel:${resource.phone.replace(/[^0-9]/g, '')}`}
|
href={`tel:${resource.phone.replace(/[^0-9]/g, '')}`}
|
||||||
className="text-sm hover:text-[#48286e] transition-colors"
|
className="text-sm hover:text-[#48286e] transition-colors"
|
||||||
|
|||||||
@@ -3,305 +3,590 @@ import { Link } from 'react-router-dom';
|
|||||||
import PublicNavbar from '../components/PublicNavbar';
|
import PublicNavbar from '../components/PublicNavbar';
|
||||||
import PublicFooter from '../components/PublicFooter';
|
import PublicFooter from '../components/PublicFooter';
|
||||||
|
|
||||||
|
|
||||||
export default function TermsOfService() {
|
export default function TermsOfService() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PublicNavbar />
|
<PublicNavbar />
|
||||||
<div className="min-h-screen bg-[#F8F7FB]">
|
<main className="bg-gradient-to-bl from-[#F9FAFB] to-[#DDD8EB] text-[#48286E]">
|
||||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 py-12">
|
<div className="mx-auto w-full max-w-5xl px-4 sm:px-6 lg:px-8 py-10">
|
||||||
{/* Header */}
|
{/* Title */}
|
||||||
<div className="mb-8">
|
<header className="border-b pb-6">
|
||||||
<h1 className="text-3xl sm:text-4xl md:text-5xl font-bold text-[#422268] mb-4"
|
<h1 className="text-3xl sm:text-4xl font-bold tracking-tight ">
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
Terms of Service
|
Terms of Service
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-base sm:text-lg text-gray-600" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="mt-3 text-sm">
|
||||||
Last Updated: January 2025
|
<span className="font-medium">Last updated March 23, 2025</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</header>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Body */}
|
||||||
<div className="bg-white rounded-lg border border-[#ddd8eb] p-6 sm:p-8 md:p-10 space-y-8">
|
<div className="prose text-[#48286E] max-w-none prose-h2:mt-10 prose-h2:scroll-mt-24 prose-h3:mt-6">
|
||||||
|
{/* AGREEMENT */}
|
||||||
{/* Section 1: Agreement to Terms */}
|
<section aria-labelledby="agreement" className="mt-8">
|
||||||
<section>
|
<h2 id="agreement" className="text-xl sm:text-2xl text-[#48286E] font-bold ">
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
|
AGREEMENT TO OUR LEGAL TERMS
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
1. Agreement to Terms
|
|
||||||
</h2>
|
</h2>
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
<p>
|
||||||
These Terms of Service constitute a legally binding agreement made between you, whether personally or on behalf of an entity ("you") and LOAFers, Inc. ("Company", "we", "us", or "our"), concerning your access to and use of the https://loaftx.org website as well as any other media form, media channel, mobile website or mobile application related, linked, or otherwise connected thereto (collectively, the "Site").
|
We are LOAFers, Inc. ("Company," "we," "us," "our").
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
You agree that by accessing the Site, you have read, understood, and agree to be bound by all of these Terms of Service. If you do not agree with all of these Terms of Service, then you are expressly prohibited from using the Site and you must discontinue use immediately.
|
<p>
|
||||||
|
We operate, as well as any other related products and services that refer or link to these legal terms
|
||||||
|
(the "Legal Terms") (collectively, the "Services").
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
You can contact us by email at{" "}
|
||||||
|
<a className="" href="mailto:info@loaftx.com">
|
||||||
|
info@loaftx.com
|
||||||
|
</a>{" "}
|
||||||
|
or by mail to PO Box 7207, Houston, TX 77249, United States.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
These Legal Terms constitute a legally binding agreement made between you, whether personally or on behalf
|
||||||
|
of an entity ("you"), and LOAFers, Inc., concerning your access to and use of the Services. You agree that
|
||||||
|
by accessing the Services, you have read, understood, and agreed to be bound by all of these Legal Terms.{" "}
|
||||||
|
<strong>
|
||||||
|
IF YOU DO NOT AGREE WITH ALL OF THESE LEGAL TERMS, THEN YOU ARE EXPRESSLY PROHIBITED FROM USING THE
|
||||||
|
SERVICES AND YOU MUST DISCONTINUE USE IMMEDIATELY.
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Supplemental terms and conditions or documents that may be posted on the Services from time to time are
|
||||||
|
hereby expressly incorporated herein by reference. We reserve the right, in our sole discretion, to make
|
||||||
|
changes or modifications to these Legal Terms at any time and for any reason. We will alert you about any
|
||||||
|
changes by updating the "Last updated" date of these Legal Terms, and you waive any right to receive
|
||||||
|
specific notice of each such change. It is your responsibility to periodically review these Legal Terms to
|
||||||
|
stay informed of updates. You will be subject to, and will be deemed to have been made aware of and to have
|
||||||
|
accepted, the changes in any revised Legal Terms by your continued use of the Services after the date such
|
||||||
|
revised Legal Terms are posted. We recommend that you print a copy of these Legal Terms for your records.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Section 2: Intellectual Property Rights */}
|
{/* TABLE OF CONTENTS */}
|
||||||
<section>
|
<section aria-labelledby="toc" className="text-[#48286E]">
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
|
<h2 id="toc" className="text-lg sm:text-xl font-bold text-[#48286E] m-0">
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
TABLE OF CONTENTS
|
||||||
2. Intellectual Property Rights
|
|
||||||
</h2>
|
</h2>
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
<ol className="mt-4 list-decimal no-prose text-[#48286E] pl-5 space-y-1">
|
||||||
Unless otherwise indicated, the Site is our proprietary property and all source code, databases, functionality, software, website designs, audio, video, text, photographs, and graphics on the Site (collectively, the "Content") and the trademarks, service marks, and logos contained therein (the "Marks") are owned or controlled by us or licensed to us, and are protected by copyright and trademark laws and various other intellectual property rights and unfair competition laws of the United States, foreign jurisdictions, and international conventions.
|
<li><a className="text-[#48286E]" href="#our-services">OUR SERVICES</a></li>
|
||||||
</p>
|
<li><a className="text-[#48286E]" href="#ipr">INTELLECTUAL PROPERTY RIGHTS</a></li>
|
||||||
</div>
|
<li><a className="text-[#48286E]" href="#user-representations">USER REPRESENTATIONS</a></li>
|
||||||
|
<li><a className="text-[#48286E]" href="#prohibited-activities">PROHIBITED ACTIVITIES</a></li>
|
||||||
|
<li><a className="text-[#48286E]" href="#ugc">USER GENERATED CONTRIBUTIONS</a></li>
|
||||||
|
<li><a className="text-[#48286E]" href="#contribution-license">CONTRIBUTION LICENSE</a></li>
|
||||||
|
<li><a className="text-[#48286E]" href="#services-management">SERVICES MANAGEMENT</a></li>
|
||||||
|
<li><a className="text-[#48286E]" href="#term-termination">TERM AND TERMINATION</a></li>
|
||||||
|
<li><a className="text-[#48286E]" href="#modifications">MODIFICATIONS AND INTERRUPTIONS</a></li>
|
||||||
|
<li><a className="text-[#48286E]" href="#governing-law">GOVERNING LAW</a></li>
|
||||||
|
<li><a className="text-[#48286E]" href="#dispute-resolution">DISPUTE RESOLUTION</a></li>
|
||||||
|
<li><a className="text-[#48286E]" href="#corrections">CORRECTIONS</a></li>
|
||||||
|
<li><a className="text-[#48286E]" href="#disclaimer">DISCLAIMER</a></li>
|
||||||
|
<li><a className="text-[#48286E]" href="#limitations-liability">LIMITATIONS OF LIABILITY</a></li>
|
||||||
|
<li><a className="text-[#48286E]" href="#indemnification">INDEMNIFICATION</a></li>
|
||||||
|
<li><a className="text-[#48286E]" href="#user-data">USER DATA</a></li>
|
||||||
|
<li><a className="text-[#48286E]" href="#electronic-comms">ELECTRONIC COMMUNICATIONS, TRANSACTIONS, AND SIGNATURES</a></li>
|
||||||
|
<li><a className="text-[#48286E]" href="#miscellaneous">MISCELLANEOUS</a></li>
|
||||||
|
<li><a className="text-[#48286E]" href="#contact-us">CONTACT US</a></li>
|
||||||
|
</ol>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Section 3: User Representations */}
|
{/* 1. OUR SERVICES */}
|
||||||
<section>
|
<section id="our-services" className="scroll-mt-24">
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
|
<h2 className="text-xl text-[#48286E] sm:text-2xl font-bold ">1. OUR SERVICES</h2>
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
<p>
|
||||||
3. User Representations
|
The information provided when using the Services is not intended for distribution to or use by any person
|
||||||
</h2>
|
or entity in any jurisdiction or country where such distribution or use would be contrary to law or
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
regulation or which would subject us to any registration requirement within such jurisdiction or country.
|
||||||
<p className="text-gray-700 leading-relaxed">
|
Accordingly, those persons who choose to access the Services from other locations do so on their own
|
||||||
By using the Site, you represent and warrant that:
|
initiative and are solely responsible for compliance with local laws, if and to the extent local laws are
|
||||||
|
applicable.
|
||||||
</p>
|
</p>
|
||||||
<ul className="list-disc list-inside space-y-2 text-gray-700 ml-4">
|
</section>
|
||||||
<li>All registration information you submit will be true, accurate, current, and complete</li>
|
|
||||||
<li>You will maintain the accuracy of such information and promptly update such registration information as necessary</li>
|
{/* 2. INTELLECTUAL PROPERTY RIGHTS */}
|
||||||
<li>You have the legal capacity and you agree to comply with these Terms of Service</li>
|
<section id="ipr" className="scroll-mt-24">
|
||||||
<li>You are not under the age of 13</li>
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E] ">2. INTELLECTUAL PROPERTY RIGHTS</h2>
|
||||||
<li>Not a minor in the jurisdiction in which you reside, or if a minor, you have received parental permission to use the Site</li>
|
|
||||||
<li>You will not access the Site through automated or non-human means</li>
|
<h3 className="text-lg font-semibold ">Our intellectual property</h3>
|
||||||
<li>You will not use the Site for any illegal or unauthorized purpose</li>
|
<p>
|
||||||
<li>Your use of the Site will not violate any applicable law or regulation</li>
|
We are the owner or the licensee of all intellectual property rights in our Services, including all source
|
||||||
|
code, databases, functionality, software, website designs, audio, video, text, photographs, and graphics
|
||||||
|
in the Services (collectively, the "Content"), as well as the trademarks, service marks, and logos
|
||||||
|
contained therein (the "Marks"). Our Content and Marks are protected by copyright and trademark laws (and
|
||||||
|
various other intellectual property rights and unfair competition laws) and treaties around the world. The
|
||||||
|
Content and Marks are provided in or through the Services "AS IS" for your personal, non-commercial use or
|
||||||
|
internal business purpose only.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 className="text-lg font-semibold text-[#48286E]">Your use of our Services</h3>
|
||||||
|
<p>
|
||||||
|
Subject to your compliance with these Legal Terms, including the "PROHIBITED ACTIVITIES" section below, we
|
||||||
|
grant you a non-exclusive, non-transferable, revocable license to:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className="list-disc pl-6 space-y-1">
|
||||||
|
<li>access the Services; and</li>
|
||||||
|
<li>
|
||||||
|
download or print a copy of any portion of the Content to which you have properly gained access, solely
|
||||||
|
for your personal, non-commercial use or internal business purpose.
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Section 4: Prohibited Activities */}
|
<p>
|
||||||
<section>
|
Except as set out in this section or elsewhere in our Legal Terms, no part of the Services and no Content
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
|
or Marks may be copied, reproduced, aggregated, republished, uploaded, posted, publicly displayed, encoded,
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
translated, transmitted, distributed, sold, licensed, or otherwise exploited for any commercial purpose
|
||||||
4. Prohibited Activities
|
whatsoever, without our express prior written permission. If you wish to make any use of the Services,
|
||||||
</h2>
|
Content, or Marks other than as set out in this section or elsewhere in our Legal Terms, please address
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
your request to:{" "}
|
||||||
<p className="text-gray-700 leading-relaxed">
|
<a className="" href="mailto:loafhoustontx@gmail.com">
|
||||||
You may not access or use the Site for any purpose other than that for which we make the Site available. The Site may not be used in connection with any commercial endeavors except those that are specifically endorsed or approved by us. As a user of the Site, you agree not to:
|
loafhoustontx@gmail.com
|
||||||
|
</a>
|
||||||
|
.
|
||||||
</p>
|
</p>
|
||||||
<ul className="list-disc list-inside space-y-2 text-gray-700 ml-4">
|
|
||||||
<li>Systematically retrieve data or other content from the Site to create or compile, directly or indirectly, a collection, compilation, database, or directory without written permission from us</li>
|
<p>
|
||||||
<li>Make any unauthorized use of the Site, including collecting usernames and/or email addresses of users by electronic or other means for the purpose of sending unsolicited email, or creating user accounts by automated means or under false pretenses</li>
|
If we ever grant you the permission to post, reproduce, or publicly display any part of our Services or
|
||||||
<li>Circumvent, disable, or otherwise interfere with security-related features of the Site</li>
|
Content, you must identify us as the owners or licensors of the Services, Content, or Marks and ensure that
|
||||||
<li>Engage in unauthorized framing of or linking to the Site</li>
|
any copyright or proprietary notice appears or is visible on posting, reproducing, or displaying our
|
||||||
<li>Trick, defraud, or mislead us and other users, especially in any attempt to learn sensitive account information such as user passwords</li>
|
Content. We reserve all rights not expressly granted to you in and to the Services, Content, and Marks. Any
|
||||||
<li>Make improper use of our support services or submit false reports of abuse or misconduct</li>
|
breach of these Intellectual Property Rights will constitute a material breach of our Legal Terms and your
|
||||||
<li>Engage in any automated use of the system, such as using scripts to send comments or messages</li>
|
right to use our Services will terminate immediately.
|
||||||
<li>Interfere with, disrupt, or create an undue burden on the Site or the networks or services connected to the Site</li>
|
</p>
|
||||||
|
|
||||||
|
<h3 className="text-lg font-semibold text-[#48286E]">Your submissions</h3>
|
||||||
|
<p>
|
||||||
|
Please review this section and the "PROHIBITED ACTIVITIES" section carefully prior to using our Services to
|
||||||
|
understand the (a) rights you give us and (b) obligations you have when you post or upload any content
|
||||||
|
through the Services.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>Submissions:</strong> By directly sending us any question, comment, suggestion, idea, feedback, or
|
||||||
|
other information about the Services ("Submissions"), you agree to assign to us all intellectual property
|
||||||
|
rights in such Submission. You agree that we shall own this Submission and be entitled to its unrestricted
|
||||||
|
use and dissemination for any lawful purpose, commercial or otherwise, without acknowledgment or
|
||||||
|
compensation to you.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>
|
||||||
|
You are responsible for what you post or upload: By sending us Submissions through any part of the
|
||||||
|
Services you:
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className="list-disc pl-6 space-y-2">
|
||||||
|
<li>
|
||||||
|
confirm that you have read and agree with our "PROHIBITED ACTIVITIES" and will not post, send, publish,
|
||||||
|
upload, or transmit through the Services any Submission that is illegal, harassing, hateful, harmful,
|
||||||
|
defamatory, obscene, bullying, abusive, discriminatory, threatening to any person or group, sexually
|
||||||
|
explicit, false, inaccurate, deceitful, or misleading;
|
||||||
|
</li>
|
||||||
|
<li>to the extent permissible by applicable law, waive any and all moral rights to any such Submission;</li>
|
||||||
|
<li>
|
||||||
|
warrant that any such Submission are original to you or that you have the necessary rights and licenses
|
||||||
|
to submit such Submissions and that you have full authority to grant us the above-mentioned rights in
|
||||||
|
relation to your Submissions; and
|
||||||
|
</li>
|
||||||
|
<li>warrant and represent that your Submissions do not constitute confidential information.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
|
||||||
|
<p>
|
||||||
|
You are solely responsible for your Submissions and you expressly agree to reimburse us for any and all
|
||||||
|
losses that we may suffer because of your breach of (a) this section, (b) any third party’s intellectual
|
||||||
|
property rights, or (c) applicable law.
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Section 5: User Generated Contributions */}
|
{/* 3. USER REPRESENTATIONS */}
|
||||||
<section>
|
<section id="user-representations" className="scroll-mt-24">
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
|
<h2 className="text-xl sm:text-2xl font-bold ">3. USER REPRESENTATIONS</h2>
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
<p>By using the Services, you represent and warrant that:</p>
|
||||||
5. User Generated Contributions
|
<ol className="list-decimal pl-6 space-y-1">
|
||||||
|
<li>you have the legal capacity and you agree to comply with these Legal Terms;</li>
|
||||||
|
<li>you are not a minor in the jurisdiction in which you reside;</li>
|
||||||
|
<li>you will not access the Services through automated or non-human means, whether through abot, script or otherwise;</li>
|
||||||
|
<li>you will not use the Services for any illegal or unauthorized purpose; and</li>
|
||||||
|
<li>your use of the Services will not violate any applicable law or regulation.</li>
|
||||||
|
</ol>
|
||||||
|
<p>
|
||||||
|
If you provide any information that is untrue, inaccurate, not current, or incomplete, we have the right to
|
||||||
|
suspend or terminate your account and refuse any and all current or future use of the Services (or any
|
||||||
|
portion thereof).
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 4. PROHIBITED ACTIVITIES */}
|
||||||
|
<section id="prohibited-activities" className="scroll-mt-24">
|
||||||
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">4. PROHIBITED ACTIVITIES</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
You may not access or use the Services for any purpose other than that for which we make the Services
|
||||||
|
available. The Services may not be used in connection with any commercial endeavors except those that are
|
||||||
|
specifically endorsed or approved by us. As a user of the Services, you agree not to:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className="list-disc pl-6 space-y-2">
|
||||||
|
<li>Systematically retrieve data or other content from the Services to create or compile, directly or indirectly, a collection, compilation, database, or directory without written permission from us.</li>
|
||||||
|
<li>Trick, defraud, or mislead us and other users, especially in any attempt to learn sensitive account information such as user passwords.</li>
|
||||||
|
<li>Circumvent, disable, or otherwise interfere with security-related features of the Services, including features that prevent or restrict the use or copying of any Content or enforce limitations on the use of the Services and/or the Content contained therein.</li>
|
||||||
|
<li>Disparage, tarnish, or otherwise harm, in our opinion, us and/or the Services.</li>
|
||||||
|
<li>Use any information obtained from the Services to harass abuse or harm another person.</li>
|
||||||
|
<li>Make improper use of our support services or submit false reports ofabuse or misconduct.</li>
|
||||||
|
<li>Use the Services in a manner inconsistent with any applicable laws or regulations.</li>
|
||||||
|
<li>Engage in unauthorized framing of or linking to the Services.</li>
|
||||||
|
<li>Upload or transmit (or attempt to upload or to transmit) viruses, Trojan horses, or other material, including excessive use of capital letters and spamming (continuous posting of repetitive text), that interferes with any party’s uninterrupted use and enjoyment of the Services or modifies, impairs, disrupts, alters, or interferes with the use, features, functions, operation, or maintenance of the Services.</li>
|
||||||
|
<li>Engage in any automated use of the system, such as using scripts to send comments or messages, or using any data mining, robots, or similar data gathering and extraction tools.</li>
|
||||||
|
<li>Delete the copyright or other proprietary rights notice from any Content.</li>
|
||||||
|
<li>Attempt to impersonate another user or person or use the username of another user.</li>
|
||||||
|
<li>Upload or transmit (or attempt to upload or to transmit) any material that acts as a passive or active information collection or transmission mechanism, including without limitation, clear graphics interchange formats ("gifs"), 1×1 pixels, web bugs, cookies, or other similar devices (sometimes referred to as "spyware" or "passive collection mechanisms" or "pcms").</li>
|
||||||
|
<li>Interfere with, disrupt, or create an undue burden on the Services or the networks or services connected to the Services.</li>
|
||||||
|
<li>Harass, annoy, intimidate, or threaten any of our employees or agents engaged in providing any portion of the Services to you.</li>
|
||||||
|
<li>Attempt to bypass any measures of the Services designed to prevent or restrict access to the Services, or any portion of the Services.</li>
|
||||||
|
<li>Copy or adapt the Services' software, including but not limited to Flash, PHP, HTML, JavaScript, or other code.</li>
|
||||||
|
<li>Except as permitted by applicable law, decipher, decompile, disassemble, or reverse engineer any of the software comprising or in any way making up a part of the Services.</li>
|
||||||
|
<li>Except as may be the result of standard search engine or Internet browser usage, use, launch, develop, or distribute any automated system, including without limitation, any spider, robot, cheat utility, scraper, or offline reader that accesses the Services, or use or launch any unauthorized script or other software.</li>
|
||||||
|
<li>Use a buying agent or purchasing agent to make purchases on the Services.</li>
|
||||||
|
<li>Make any unauthorized use of the Services, including collecting usernames and/or email addresses of users by electronic or other means for the purpose of sending unsolicited email, or creating user accounts by automated means or under false pretenses.</li>
|
||||||
|
<li>Use the Services as part of any effort to compete with us or otherwise use the Services and/or the Content for any revenue-generating endeavor or commercial enterprise.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 5. USER GENERATED CONTRIBUTIONS */}
|
||||||
|
<section id="ugc" className="scroll-mt-24">
|
||||||
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">5. USER GENERATED CONTRIBUTIONS</h2>
|
||||||
|
<p>
|
||||||
|
The Services does not offer users to submit or post content. We may provide you with the opportunity to
|
||||||
|
create, submit, post, display, transmit, perform, publish, distribute, or broadcast content and materials
|
||||||
|
to us or on the Services, including but not limited to text, writings, video, audio, photographs, graphics,
|
||||||
|
comments, suggestions, or personal information or other material (collectively, "Contributions").
|
||||||
|
Contributions may be viewable by other users of the Services and through third-party websites. When you
|
||||||
|
create or make available any Contributions, you thereby represent and warrant that:
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 6. CONTRIBUTION LICENSE */}
|
||||||
|
<section id="contribution-license" className="scroll-mt-24">
|
||||||
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">6. CONTRIBUTION LICENSE</h2>
|
||||||
|
<p>
|
||||||
|
You and Services agree that we may access, store, process, and use any information and personal data that
|
||||||
|
you provide and your choices (including settings).
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
By submitting suggestions or other feedback regarding the Services, you agree that we can use and share
|
||||||
|
such feedback for any purpose without compensation to you.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We do not assert any ownership over your Contributions. You retain full ownership of all of your
|
||||||
|
Contributions and any intellectual property rights, or other proprietary rights associated with your
|
||||||
|
Contributions. We are not liable for any statements or representations in your Contributions provided by
|
||||||
|
you in any area on the Services. You are solely responsible for your Contributions to the Services and you
|
||||||
|
expressly agree to exonerate us from any and all responsibility and to refrain from any legal action
|
||||||
|
against us regarding your Contributions.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 7. SERVICES MANAGEMENT */}
|
||||||
|
<section id="services-management" className="scroll-mt-24">
|
||||||
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">7. SERVICES MANAGEMENT</h2>
|
||||||
|
<p>
|
||||||
|
We reserve the right, but not the obligation, to: (1) monitor the Services for violations of these Legal
|
||||||
|
Terms; (2) take appropriate legal action against anyone who, in our sole discretion, violates the law or
|
||||||
|
these Legal Terms, including without limitation, reporting such user to law enforcement authorities; (3) in
|
||||||
|
our sole discretion and without limitation, refuse, restrict access to, limit the availability of, or
|
||||||
|
disable (to the extent technologically feasible) any of your Contributions or any portion thereof; (4) in
|
||||||
|
our sole discretion and without limitation, notice, or liability, to remove from the Services or otherwise
|
||||||
|
disable all files and content that are excessive in size or are in any way burdensome to our systems; and
|
||||||
|
(5) otherwise manage the Services in a manner designed to protect our rights and property and to
|
||||||
|
facilitate the proper functioning of the Services.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 8. TERM AND TERMINATION */}
|
||||||
|
<section id="term-termination" className="scroll-mt-24">
|
||||||
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">8. TERM AND TERMINATION</h2>
|
||||||
|
<p>
|
||||||
|
These Legal Terms shall remain in full force and effect while you use the Services.{" "}
|
||||||
|
<strong>
|
||||||
|
WITHOUT LIMITING ANY OTHER PROVISION OF THESE LEGAL TERMS, WE RESERVE THE RIGHT TO, IN OUR SOLE
|
||||||
|
DISCRETION AND WITHOUT NOTICE OR LIABILITY, DENY ACCESS TO AND USE OF THE SERVICES (INCLUDING BLOCKING
|
||||||
|
CERTAIN IP ADDRESSES), TO ANY PERSON FOR ANY REASON OR FOR NO REASON, INCLUDING WITHOUT LIMITATION FOR
|
||||||
|
BREACH OF ANY REPRESENTATION, WARRANTY, OR COVENANT CONTAINED IN THESE LEGAL TERMS OR OF ANY APPLICABLE
|
||||||
|
LAW OR REGULATION. WE MAY TERMINATE YOUR USE OR PARTICIPATION IN THE SERVICES OR DELETE ANY CONTENT OR
|
||||||
|
INFORMATION THAT YOU POSTED AT ANY TIME, WITHOUT WARNING, IN OUR SOLE DISCRETION.
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If we terminate or suspend your account for any reason, you are prohibited from registering and creating a
|
||||||
|
new account under your name, a fake or borrowed name, or the name of any third party, even if you may be
|
||||||
|
acting on behalf of the third party. In addition to terminating or suspending your account, we reserve the
|
||||||
|
right to take appropriate legal action, including without limitation pursuing civil, criminal, and
|
||||||
|
injunctive redress.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 9. MODIFICATIONS AND INTERRUPTIONS */}
|
||||||
|
<section id="modifications" className="scroll-mt-24">
|
||||||
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">9. MODIFICATIONS AND INTERRUPTIONS</h2>
|
||||||
|
<p>
|
||||||
|
We reserve the right to change, modify, or remove the contents of the Services at any time or for any
|
||||||
|
reason at our sole discretion without notice. However, we have no obligation to update any information on
|
||||||
|
our Services. We will not be liable to you or any third party for any modification, price change,
|
||||||
|
suspension, or discontinuance of the Services.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We cannot guarantee the Services will be available at all times. We may experience hardware, software, or
|
||||||
|
other problems or need to perform maintenance related to the Services, resulting in interruptions, delays,
|
||||||
|
or errors. We reserve the right to change, revise, update, suspend, discontinue, or otherwise modify the
|
||||||
|
Services at any time or for any reason without notice to you.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You agree that we have no liability whatsoever for any loss, damage, or inconvenience caused by your
|
||||||
|
inability to access or use the Services during any downtime or discontinuance of the Services. Nothing in
|
||||||
|
these Legal Terms will be construed to obligate us to maintain and support the Services or to supply any
|
||||||
|
corrections, updates, or releases in connection therewith.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 10. GOVERNING LAW */}
|
||||||
|
<section id="governing-law" className="scroll-mt-24">
|
||||||
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">10. GOVERNING LAW</h2>
|
||||||
|
<p>
|
||||||
|
These Legal Terms shall be governed by and defined following the laws of Texas. LOAFers, Inc. and yourself
|
||||||
|
irrevocably consent that the courts of Houston shall have exclusive jurisdiction to resolve any dispute
|
||||||
|
which may arise in connection with these Legal Terms.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 11. DISPUTE RESOLUTION */}
|
||||||
|
<section id="dispute-resolution" className="scroll-mt-24">
|
||||||
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">11. DISPUTE RESOLUTION</h2>
|
||||||
|
|
||||||
|
<h3 className="text-lg font-semibold ">Informal Negotiations</h3>
|
||||||
|
<p>
|
||||||
|
To expedite resolution and control the cost of any dispute, controversy, or claim related to these Legal
|
||||||
|
Terms (each a "Dispute" and collectively, the "Disputes") brought by either you or us (individually, a
|
||||||
|
"Party" and collectively, the "Parties"), the Parties agree to first attempt to negotiate any Dispute
|
||||||
|
(except those Disputes expressly provided below) informally for at least ___60_____ days before initiating
|
||||||
|
arbitration. Such informal negotiations commence upon written notice from one Party to the other Party.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 className="text-lg font-semibold text-[#48286E]">Binding Arbitration</h3>
|
||||||
|
<p>
|
||||||
|
Any dispute arising out of or in connection with these Legal Terms, including any question regarding its
|
||||||
|
existence, validity, or termination, shall be referred to and finally resolved by the Disputy Resolution
|
||||||
|
Center of Harris County{" "}
|
||||||
|
<a
|
||||||
|
className=""
|
||||||
|
href="https://drc.harriscountytx.gov/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
https://drc.harriscountytx.gov/
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 className="text-lg font-semibold text-[#48286E]">Restrictions</h3>
|
||||||
|
<p>
|
||||||
|
The Parties agree that any arbitration shall be limited to the Dispute between the Parties individually.
|
||||||
|
To the full extent permitted by law, (a) no arbitration shall be joined with any other proceeding; (b)
|
||||||
|
there is no right or authority for any Dispute to be arbitrated on a class-action basis or to utilize class
|
||||||
|
action procedures; and (c) there is no right or authority for any Dispute to be brought in a purported
|
||||||
|
representative capacity on behalf of the general public or any other persons.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 className="text-lg font-semibold text-[#48286E]">Exceptions to Informal Negotiations and Arbitration</h3>
|
||||||
|
<p>
|
||||||
|
The Parties agree that the following Disputes are not subject to the above provisions concerning informal
|
||||||
|
negotiations binding arbitration: (a) any Disputes seeking to enforce or protect, or concerning the validity
|
||||||
|
of, any of the intellectual property rights of a Party; (b) any Dispute related to, or arising from,
|
||||||
|
allegations of theft, piracy, invasion of privacy, or unauthorized use; and (c) any claim for injunctive
|
||||||
|
relief.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If this provision is found to be illegal or unenforceable, then neither Party will elect to arbitrate any
|
||||||
|
Dispute falling within that portion of this provision found to be illegal or unenforceable and such Dispute
|
||||||
|
shall be decided by a court of competent jurisdiction within the courts listed for jurisdiction above, and
|
||||||
|
the Parties agree to submit to the personal jurisdiction of that court.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 12. CORRECTIONS */}
|
||||||
|
<section id="corrections" className="scroll-mt-24">
|
||||||
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">12. CORRECTIONS</h2>
|
||||||
|
<p>
|
||||||
|
There may be information on the Services that contains typographical errors, inaccuracies, or omissions,
|
||||||
|
including descriptions, pricing, availability, and various other information. We reserve the right to
|
||||||
|
correct any errors, inaccuracies, or omissions and to change or update the information on the Services at
|
||||||
|
any time, without prior notice.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 13. DISCLAIMER */}
|
||||||
|
<section id="disclaimer" className="scroll-mt-24">
|
||||||
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">13. DISCLAIMER</h2>
|
||||||
|
<p className="font-semibold">
|
||||||
|
THE SERVICES ARE PROVIDED ON AN AS-IS AND AS-AVAILABLE BASIS. YOU AGREE THAT YOUR USE OF THE SERVICES WILL
|
||||||
|
BE AT YOUR SOLE RISK. TO THE FULLEST EXTENT PERMITTED BY LAW, WE DISCLAIM ALL WARRANTIES, EXPRESS OR
|
||||||
|
IMPLIED, IN CONNECTION WITH THE SERVICES AND YOUR USE THEREOF, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold">
|
||||||
|
WE MAKE NO WARRANTIES OR REPRESENTATIONS ABOUT THE ACCURACY OR COMPLETENESS OF THE SERVICES' CONTENT OR THE
|
||||||
|
CONTENT OF ANY WEBSITES OR MOBILE APPLICATIONS LINKED TO THE SERVICES AND WE WILL ASSUME NO LIABILITY OR
|
||||||
|
RESPONSIBILITY FOR ANY (1) ERRORS, MISTAKES, OR INACCURACIES OF CONTENT AND MATERIALS, (2) PERSONAL INJURY
|
||||||
|
OR PROPERTY DAMAGE, OF ANY NATURE WHATSOEVER, RESULTING FROM YOUR ACCESS TO AND USE OF THE SERVICES, (3)
|
||||||
|
ANY UNAUTHORIZED ACCESS TO OR USE OF OUR SECURE SERVERS AND/OR ANY AND ALL PERSONAL INFORMATION AND/OR
|
||||||
|
FINANCIAL INFORMATION STORED THEREIN, (4) ANY INTERRUPTION OR CESSATION OF TRANSMISSION TO OR FROM THE
|
||||||
|
SERVICES, (5) ANY BUGS, VIRUSES, TROJAN HORSES, OR THE LIKE WHICH MAY BE TRANSMITTED TO OR THROUGH THE
|
||||||
|
SERVICES BY ANY THIRD PARTY, AND/OR (6) ANY ERRORS OR OMISSIONS IN ANY CONTENT AND MATERIALS OR FOR ANY LOSS
|
||||||
|
OR DAMAGE OF ANY KIND INCURRED AS A RESULT OF THE USE OF ANY CONTENT POSTED, TRANSMITTED, OR OTHERWISE MADE
|
||||||
|
AVAILABLE VIA THE SERVICES.
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold">
|
||||||
|
WE DO NOT WARRANT, ENDORSE, GUARANTEE, OR ASSUME RESPONSIBILITY FOR ANY PRODUCT OR SERVICE ADVERTISED OR
|
||||||
|
OFFERED BY A THIRD PARTY THROUGH THE SERVICES, ANY HYPERLINKED WEBSITE, OR ANY WEBSITE OR MOBILE APPLICATION
|
||||||
|
FEATURED IN ANY BANNER OR OTHER ADVERTISING, AND WE WILL NOT BE A PARTY TO OR IN ANY WAY BE RESPONSIBLE FOR
|
||||||
|
MONITORING ANY TRANSACTION BETWEEN YOU AND ANY THIRD-PARTY PROVIDERS OF PRODUCTS OR SERVICES. AS WITH THE
|
||||||
|
PURCHASE OF A PRODUCT OR SERVICE THROUGH ANY MEDIUM OR IN ANY ENVIRONMENT, YOU SHOULD USE YOUR BEST
|
||||||
|
JUDGMENT AND EXERCISE CAUTION WHERE APPROPRIATE.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 14. LIMITATIONS OF LIABILITY */}
|
||||||
|
<section id="limitations-liability" className="scroll-mt-24">
|
||||||
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">14. LIMITATIONS OF LIABILITY</h2>
|
||||||
|
<p className="font-semibold">
|
||||||
|
IN NO EVENT WILL WE OR OUR DIRECTORS, EMPLOYEES, OR AGENTS BE LIABLE TO YOU OR ANY THIRD PARTY FOR ANY
|
||||||
|
DIRECT, INDIRECT, CONSEQUENTIAL, EXEMPLARY, INCIDENTAL, SPECIAL, OR PUNITIVE DAMAGES, INCLUDING LOST
|
||||||
|
PROFIT, LOST REVENUE, LOSS OF DATA, OR OTHER DAMAGES ARISING FROM YOUR USE OF THE SERVICES, EVEN IF WE HAVE
|
||||||
|
BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold">
|
||||||
|
NOTWITHSTANDING ANYTHING TO THE CONTRARY CONTAINED HEREIN, OUR LIABILITY TO YOU FOR ANY CAUSE WHATSOEVER
|
||||||
|
AND REGARDLESS OF THE FORM OF THE ACTION, WILL AT ALL TIMES BE LIMITED TO THE LESSER OF THE AMOUNT PAID, IF
|
||||||
|
ANY, BY YOU TO US OR .
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold">
|
||||||
|
CERTAIN US STATE LAWS AND INTERNATIONAL LAWS DO NOT ALLOW LIMITATIONS ON IMPLIED WARRANTIES OR THE
|
||||||
|
EXCLUSION OR LIMITATION OF CERTAIN DAMAGES. IF THESE LAWS APPLY TO YOU, SOME OR ALL OF THE ABOVE
|
||||||
|
DISCLAIMERS OR LIMITATIONS MAY NOT APPLY TO YOU, AND YOU MAY HAVE ADDITIONAL RIGHTS.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 15. INDEMNIFICATION */}
|
||||||
|
<section id="indemnification" className="scroll-mt-24">
|
||||||
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">15. INDEMNIFICATION</h2>
|
||||||
|
<p>
|
||||||
|
You agree to defend, indemnify, and hold us harmless, including our subsidiaries, affiliates, and all of
|
||||||
|
our respective officers, agents, partners, and employees, from and against any loss, damage, liability,
|
||||||
|
claim, or demand, including reasonable attorneys’ fees and expenses, made by any third party due to or
|
||||||
|
arising out of: (1) use of the Services; (2) breach of these Legal Terms; (3) any breach of your
|
||||||
|
representations and warranties set forth in these Legal Terms; (4) your violation of the rights of a third
|
||||||
|
party, including but not limited to intellectual property rights; or (5) any overt harmful act toward any
|
||||||
|
other user of the Services with whom you connected via the Services.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Notwithstanding the foregoing, we reserve the right, at your expense, to assume the exclusive defense and
|
||||||
|
control of any matter for which you are required to indemnify us, and you agree to cooperate, at your
|
||||||
|
expense, with our defense of such claims. We will use reasonable efforts to notify you of any such claim,
|
||||||
|
action, or proceeding which is subject to this indemnification upon becoming aware of it.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 16. USER DATA */}
|
||||||
|
<section id="user-data" className="scroll-mt-24">
|
||||||
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">16. USER DATA</h2>
|
||||||
|
<p>
|
||||||
|
We will maintain certain data that you transmit to the Services for the purpose of managing the performance
|
||||||
|
of the Services, as well as data relating to your use of the Services. Although we perform regular routine
|
||||||
|
backups of data, you are solely responsible for all data that you transmit or that relates to any activity
|
||||||
|
you have undertaken using the Services. You agree that we shall have no liability to you for any loss or
|
||||||
|
corruption of any such data, and you hereby waive any right of action against us arising from any such loss
|
||||||
|
or corruption of such data.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 17. ELECTRONIC COMMUNICATIONS */}
|
||||||
|
<section id="electronic-comms" className="scroll-mt-24">
|
||||||
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">
|
||||||
|
17. ELECTRONIC COMMUNICATIONS, TRANSACTIONS, AND SIGNATURES
|
||||||
</h2>
|
</h2>
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p>
|
||||||
<p className="text-gray-700 leading-relaxed">
|
Visiting the Services, sending us emails, and completing online forms constitute electronic communications.
|
||||||
The Site may invite you to chat, contribute to, or participate in blogs, message boards, online forums, and other functionality, and may provide you with the opportunity to create, submit, post, display, transmit, perform, publish, distribute, or broadcast content and materials to us or on the Site.
|
You consent to receive electronic communications, and you agree that all agreements, notices, disclosures,
|
||||||
|
and other communications we provide to you electronically, via email and on the Services, satisfy any legal
|
||||||
|
requirement that such communication be in writing.
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold">
|
||||||
|
YOU HEREBY AGREE TO THE USE OF ELECTRONIC SIGNATURES, CONTRACTS, ORDERS, AND OTHER RECORDS, AND TO
|
||||||
|
ELECTRONIC DELIVERY OF NOTICES, POLICIES, AND RECORDS OF TRANSACTIONS INITIATED OR COMPLETED BY US OR VIA
|
||||||
|
THE SERVICES.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You hereby waive any rights or requirements under any statutes, regulations, rules, ordinances, or other
|
||||||
|
laws in any jurisdiction which require an original signature or delivery or retention of non-electronic
|
||||||
|
records, or to payments or the granting of credits by any means other than electronic means.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 18. MISCELLANEOUS */}
|
||||||
|
<section id="miscellaneous" className="scroll-mt-24">
|
||||||
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">18. MISCELLANEOUS</h2>
|
||||||
|
<p>
|
||||||
|
These Legal Terms and any policies or operating rules posted by us on the Services or in respect to the
|
||||||
|
Services constitute the entire agreement and understanding between you and us. Our failure to exercise or
|
||||||
|
enforce any right or provision of these Legal Terms shall not operate as a waiver of such right or
|
||||||
|
provision.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
These Legal Terms operate to the fullest extent permissible by law. We may assign any or all of our rights
|
||||||
|
and obligations to others at any time. We shall not be responsible or liable for any loss, damage, delay,
|
||||||
|
or failure to act caused by any cause beyond our reasonable control.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If any provision or part of a provision of these Legal Terms is determined to be unlawful, void, or
|
||||||
|
unenforceable, that provision or part of the provision is deemed severable from these Legal Terms and does
|
||||||
|
not affect the validity and enforceability of any remaining provisions.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
There is no joint venture, partnership, employment or agency relationship created between you and us as a
|
||||||
|
result of these Legal Terms or use of the Services. You agree that these Legal Terms will not be construed
|
||||||
|
against us by virtue of having drafted them. You hereby waive any and all defenses you may have based on
|
||||||
|
the electronic form of these Legal Terms and the lack of signing by the parties hereto to execute these
|
||||||
|
Legal Terms.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 19. CONTACT US */}
|
||||||
|
<section id="contact-us" className="scroll-mt-24">
|
||||||
|
<h2 className="text-xl sm:text-2xl font-bold text-[#48286E]">19. CONTACT US</h2>
|
||||||
|
<p>
|
||||||
|
In order to resolve a complaint regarding the Services or to receive further information regarding use of
|
||||||
|
the Services, please contact us at:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="not-prose mt-4 ">
|
||||||
|
<p className="font-semibold mb-2">LOAFers, Inc.</p>
|
||||||
|
<p className="">PO Box 7207</p>
|
||||||
|
<p className="">Houston, TX 77249</p>
|
||||||
|
<p className="">United States</p>
|
||||||
|
<p className="mt-3">
|
||||||
|
<a className="" href="mailto:info@loaftx.org">
|
||||||
|
info@loaftx.org
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Section 6: Contribution License */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
|
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
6. Contribution License
|
|
||||||
</h2>
|
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
By posting your Contributions to any part of the Site, you automatically grant, and you represent and warrant that you have the right to grant, to us an unrestricted, unlimited, irrevocable, perpetual, non-exclusive, transferable, royalty-free, fully-paid, worldwide right, and license to host, use, copy, reproduce, disclose, sell, resell, publish, broadcast, retitle, archive, store, cache, publicly perform, publicly display, reformat, translate, transmit, excerpt (in whole or in part), and distribute such Contributions.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Section 7: Submissions */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
|
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
7. Submissions
|
|
||||||
</h2>
|
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
You acknowledge and agree that any questions, comments, suggestions, ideas, feedback, or other information regarding the Site ("Submissions") provided by you to us are non-confidential and shall become our sole property.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Section 8: Site Management */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
|
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
8. Site Management
|
|
||||||
</h2>
|
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
We reserve the right, but not the obligation, to: (1) monitor the Site for violations of these Terms of Service; (2) take appropriate legal action against anyone who, in our sole discretion, violates the law or these Terms of Service; (3) refuse, restrict access to, limit the availability of, or disable (to the extent technologically feasible) any of your Contributions; (4) remove from the Site or otherwise disable all files and content that are excessive in size or are in any way burdensome to our systems.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Section 9: Term and Termination */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
|
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
9. Term and Termination
|
|
||||||
</h2>
|
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
These Terms of Service shall remain in full force and effect while you use the Site. Without limiting any other provision of these Terms of Service, we reserve the right to, in our sole discretion and without notice or liability, deny access to and use of the Site to any person for any reason or for no reason.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Section 10: Modifications and Interruptions */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
|
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
10. Modifications and Interruptions
|
|
||||||
</h2>
|
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
We reserve the right to change, modify, or remove the contents of the Site at any time or for any reason at our sole discretion without notice. We also reserve the right to modify or discontinue all or part of the Site without notice at any time.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Section 11: Governing Law */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
|
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
11. Governing Law
|
|
||||||
</h2>
|
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
These Terms of Service and your use of the Site are governed by and construed in accordance with the laws of the State of Texas applicable to agreements made and to be entirely performed within the State of Texas, without regard to its conflict of law principles.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Section 12: Dispute Resolution */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
|
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
12. Dispute Resolution
|
|
||||||
</h2>
|
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
Any legal action of whatever nature brought by either you or us shall be commenced or prosecuted in the state and federal courts located in Harris County, Texas, and the parties hereby consent to, and waive all defenses of lack of personal jurisdiction and forum non conveniens with respect to venue and jurisdiction in such state and federal courts.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Section 13: Corrections */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
|
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
13. Corrections
|
|
||||||
</h2>
|
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
There may be information on the Site that contains typographical errors, inaccuracies, or omissions that may relate to the Site, including descriptions, pricing, availability, and various other information. We reserve the right to correct any errors, inaccuracies, or omissions and to change or update the information on the Site at any time, without prior notice.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Section 14: Disclaimer */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
|
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
14. Disclaimer
|
|
||||||
</h2>
|
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
The Site is provided on an as-is and as-available basis. You agree that your use of the Site and our services will be at your sole risk. To the fullest extent permitted by law, we disclaim all warranties, express or implied, in connection with the Site and your use thereof.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Section 15: Limitations of Liability */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
|
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
15. Limitations of Liability
|
|
||||||
</h2>
|
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
In no event will we or our directors, employees, or agents be liable to you or any third party for any direct, indirect, consequential, exemplary, incidental, special, or punitive damages, including lost profit, lost revenue, loss of data, or other damages arising from your use of the Site.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Section 16: Indemnification */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
|
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
16. Indemnification
|
|
||||||
</h2>
|
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
You agree to defend, indemnify, and hold us harmless, including our subsidiaries, affiliates, and all of our respective officers, agents, partners, and employees, from and against any loss, damage, liability, claim, or demand, including reasonable attorneys' fees and expenses, made by any third party due to or arising out of your use of the Site or breach of these Terms of Service.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Section 17: User Data */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
|
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
17. User Data
|
|
||||||
</h2>
|
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
We will maintain certain data that you transmit to the Site for the purpose of managing the Site, as well as data relating to your use of the Site. Although we perform regular routine backups of data, you are solely responsible for all data that you transmit or that relates to any activity you have undertaken using the Site.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Section 18: Electronic Communications */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
|
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
18. Electronic Communications, Transactions, and Signatures
|
|
||||||
</h2>
|
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
Visiting the Site, sending us emails, and completing online forms constitute electronic communications. You consent to receive electronic communications, and you agree that all agreements, notices, disclosures, and other communications we provide to you electronically, via email and on the Site, satisfy any legal requirement that such communication be in writing.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Section 19: Contact Us */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-[#422268] mb-4"
|
|
||||||
style={{ fontFamily: "'Inter', sans-serif" }}>
|
|
||||||
19. Contact Us
|
|
||||||
</h2>
|
|
||||||
<div className="prose max-w-none space-y-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
In order to resolve a complaint regarding the Site or to receive further information regarding use of the Site, please contact us at:
|
|
||||||
</p>
|
|
||||||
<div className="bg-[#F8F7FB] p-4 rounded-lg border border-[#ddd8eb]">
|
|
||||||
<p className="font-semibold text-gray-800">LOAFers, Inc.</p>
|
|
||||||
<p className="text-gray-700">PO Box 7207</p>
|
|
||||||
<p className="text-gray-700">Houston, TX 77249</p>
|
|
||||||
<p className="text-gray-700 mt-2">
|
|
||||||
Email: <a href="mailto:info@loaftx.org" className="text-[#664fa3] hover:text-[#422268] font-semibold">info@loaftx.org</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Back to Home Link */}
|
{/* Back to Home Link */}
|
||||||
<div className="mt-8 text-center">
|
<div className="mt-8 text-center">
|
||||||
<Link to="/" className="text-[#664fa3] hover:text-[#422268] font-semibold transition-colors inline-flex items-center gap-2"
|
<Link to="/" className="text-[#664fa3] hover:text-[#422268] font-semibold transition-colors inline-flex items-center gap-2"
|
||||||
@@ -309,9 +594,12 @@ export default function TermsOfService() {
|
|||||||
<span>←</span> Back to Home
|
<span>←</span> Back to Home
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
</div>
|
|
||||||
<PublicFooter />
|
<PublicFooter />
|
||||||
</>
|
</>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import api from '../../utils/api';
|
import api from '../../utils/api';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import { Button } from '../../components/ui/button';
|
import { Button } from '../../components/ui/button';
|
||||||
@@ -32,6 +33,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const AdminBylaws = () => {
|
const AdminBylaws = () => {
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
const [bylaws, setBylaws] = useState([]);
|
const [bylaws, setBylaws] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
@@ -44,7 +46,7 @@ const AdminBylaws = () => {
|
|||||||
version: '',
|
version: '',
|
||||||
effective_date: '',
|
effective_date: '',
|
||||||
document_url: '',
|
document_url: '',
|
||||||
document_type: 'google_drive',
|
document_type: 'link',
|
||||||
is_current: false
|
is_current: false
|
||||||
});
|
});
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
@@ -71,9 +73,10 @@ const AdminBylaws = () => {
|
|||||||
version: '',
|
version: '',
|
||||||
effective_date: new Date().toISOString().split('T')[0],
|
effective_date: new Date().toISOString().split('T')[0],
|
||||||
document_url: '',
|
document_url: '',
|
||||||
document_type: 'google_drive',
|
document_type: 'link',
|
||||||
is_current: bylaws.length === 0 // Auto-check if this is the first bylaws
|
is_current: bylaws.length === 0 // Auto-check if this is the first bylaws
|
||||||
});
|
});
|
||||||
|
setUploadedFile(null);
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -183,6 +186,7 @@ const AdminBylaws = () => {
|
|||||||
Manage LOAF governing bylaws and version history
|
Manage LOAF governing bylaws and version history
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{hasPermission('bylaws.create') && (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
className="bg-[#664fa3] text-white hover:bg-[#533a82] rounded-full flex items-center gap-2"
|
className="bg-[#664fa3] text-white hover:bg-[#533a82] rounded-full flex items-center gap-2"
|
||||||
@@ -190,6 +194,7 @@ const AdminBylaws = () => {
|
|||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
Add Version
|
Add Version
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Current Bylaws */}
|
{/* Current Bylaws */}
|
||||||
@@ -225,6 +230,7 @@ const AdminBylaws = () => {
|
|||||||
<ExternalLink className="h-4 w-4 mr-1" />
|
<ExternalLink className="h-4 w-4 mr-1" />
|
||||||
View
|
View
|
||||||
</Button>
|
</Button>
|
||||||
|
{hasPermission('bylaws.edit') && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -233,6 +239,8 @@ const AdminBylaws = () => {
|
|||||||
>
|
>
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
|
{hasPermission('bylaws.delete') && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -241,22 +249,25 @@ const AdminBylaws = () => {
|
|||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4 text-sm text-[#664fa3]">
|
<div className="flex items-center gap-4 text-sm text-[#664fa3]">
|
||||||
<span>Effective Date: <strong>{formatDate(currentBylaws.effective_date)}</strong></span>
|
<span>Effective Date: <strong>{formatDate(currentBylaws.effective_date)}</strong></span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>Document Type: <strong>{currentBylaws.document_type === 'google_drive' ? 'Google Drive' : currentBylaws.document_type.toUpperCase()}</strong></span>
|
<span>Document Type: <strong>{currentBylaws.document_type === 'upload' ? 'PDF Upload' : 'Link'}</strong></span>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<Card className="p-12 text-center">
|
<Card className="p-12 text-center">
|
||||||
<Scale className="h-16 w-16 text-[#ddd8eb] mx-auto mb-4" />
|
<Scale className="h-16 w-16 text-[#ddd8eb] mx-auto mb-4" />
|
||||||
<p className="text-[#664fa3] text-lg mb-4">No current bylaws set</p>
|
<p className="text-[#664fa3] text-lg mb-4">No current bylaws set</p>
|
||||||
|
{hasPermission('bylaws.create') && (
|
||||||
<Button onClick={handleCreate} className="bg-[#664fa3] text-white">
|
<Button onClick={handleCreate} className="bg-[#664fa3] text-white">
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
Create Bylaws
|
Create Bylaws
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -289,6 +300,7 @@ const AdminBylaws = () => {
|
|||||||
>
|
>
|
||||||
<ExternalLink className="h-4 w-4" />
|
<ExternalLink className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
{hasPermission('bylaws.edit') && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -297,6 +309,8 @@ const AdminBylaws = () => {
|
|||||||
>
|
>
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
|
{hasPermission('bylaws.delete') && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -305,6 +319,7 @@ const AdminBylaws = () => {
|
|||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -363,14 +378,16 @@ const AdminBylaws = () => {
|
|||||||
<Label htmlFor="document_type">Document Type *</Label>
|
<Label htmlFor="document_type">Document Type *</Label>
|
||||||
<Select
|
<Select
|
||||||
value={formData.document_type}
|
value={formData.document_type}
|
||||||
onValueChange={(value) => setFormData({ ...formData, document_type: value })}
|
onValueChange={(value) => {
|
||||||
|
setFormData({ ...formData, document_type: value, document_url: '' });
|
||||||
|
setUploadedFile(null);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="google_drive">Google Drive</SelectItem>
|
<SelectItem value="link">Link</SelectItem>
|
||||||
<SelectItem value="pdf">PDF</SelectItem>
|
|
||||||
<SelectItem value="upload">Upload</SelectItem>
|
<SelectItem value="upload">Upload</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -391,6 +408,11 @@ const AdminBylaws = () => {
|
|||||||
Selected: {uploadedFile.name}
|
Selected: {uploadedFile.name}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{selectedBylaws && !uploadedFile && (
|
||||||
|
<p className="text-sm text-[#664fa3] mt-1">
|
||||||
|
Current file will be kept if no new file is selected
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
@@ -399,12 +421,11 @@ const AdminBylaws = () => {
|
|||||||
id="document_url"
|
id="document_url"
|
||||||
value={formData.document_url}
|
value={formData.document_url}
|
||||||
onChange={(e) => setFormData({ ...formData, document_url: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, document_url: e.target.value })}
|
||||||
placeholder="https://drive.google.com/file/d/..."
|
placeholder="https://docs.google.com/... or https://example.com/file.pdf"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-[#664fa3] mt-1">
|
<p className="text-sm text-[#664fa3] mt-1">
|
||||||
{formData.document_type === 'google_drive' && 'Paste the shareable link to your Google Drive file'}
|
Paste the shareable link to your document (Google Drive, Dropbox, PDF URL, etc.)
|
||||||
{formData.document_type === 'pdf' && 'Paste the URL to your PDF file'}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import api from '../../utils/api';
|
|||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import { Button } from '../../components/ui/button';
|
import { Button } from '../../components/ui/button';
|
||||||
import { Badge } from '../../components/ui/badge';
|
import { Badge } from '../../components/ui/badge';
|
||||||
import { Users, Calendar, Clock, CheckCircle, Mail, AlertCircle } from 'lucide-react';
|
import { Users, Calendar, Clock, CheckCircle, Mail, AlertCircle,Globe } from 'lucide-react';
|
||||||
|
|
||||||
const AdminDashboard = () => {
|
const AdminDashboard = () => {
|
||||||
const [stats, setStats] = useState({
|
const [stats, setStats] = useState({
|
||||||
@@ -56,6 +56,7 @@ const AdminDashboard = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<div className='flex justify-between items-center'>
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-4xl md:text-5xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h1 className="text-4xl md:text-5xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
Admin Dashboard
|
Admin Dashboard
|
||||||
@@ -64,6 +65,15 @@ const AdminDashboard = () => {
|
|||||||
Manage users, events, and membership applications.
|
Manage users, events, and membership applications.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<Link to={'/'}>
|
||||||
|
<Button
|
||||||
|
className="bg-[#664fa3] text-white hover:bg-[#533a82] rounded-full flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Globe />
|
||||||
|
View Public Site
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Stats Grid */}
|
{/* Stats Grid */}
|
||||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import { Button } from '../../components/ui/button';
|
import { Button } from '../../components/ui/button';
|
||||||
import { Input } from '../../components/ui/input';
|
import { Input } from '../../components/ui/input';
|
||||||
@@ -31,6 +32,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const AdminDonations = () => {
|
const AdminDonations = () => {
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
const [donations, setDonations] = useState([]);
|
const [donations, setDonations] = useState([]);
|
||||||
const [filteredDonations, setFilteredDonations] = useState([]);
|
const [filteredDonations, setFilteredDonations] = useState([]);
|
||||||
const [stats, setStats] = useState({});
|
const [stats, setStats] = useState({});
|
||||||
@@ -269,6 +271,7 @@ const AdminDonations = () => {
|
|||||||
className="pl-10 rounded-full border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
className="pl-10 rounded-full border-2 border-[#ddd8eb] focus:border-[#664fa3]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{hasPermission('donations.export') && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
@@ -296,6 +299,7 @@ const AdminDonations = () => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filters Row */}
|
{/* Filters Row */}
|
||||||
|
|||||||
548
src/pages/admin/AdminEventAttendance.js
Normal file
548
src/pages/admin/AdminEventAttendance.js
Normal file
@@ -0,0 +1,548 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||||
|
import api from '../../utils/api';
|
||||||
|
import { Card } from '../../components/ui/card';
|
||||||
|
import { Button } from '../../components/ui/button';
|
||||||
|
import { Input } from '../../components/ui/input';
|
||||||
|
import { Badge } from '../../components/ui/badge';
|
||||||
|
import { Checkbox } from '../../components/ui/checkbox';
|
||||||
|
import {
|
||||||
|
ArrowLeft,
|
||||||
|
Calendar,
|
||||||
|
MapPin,
|
||||||
|
Download,
|
||||||
|
Check,
|
||||||
|
X,
|
||||||
|
Search,
|
||||||
|
Users,
|
||||||
|
UserCheck,
|
||||||
|
UserX,
|
||||||
|
HelpCircle
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
const AdminEventAttendance = () => {
|
||||||
|
const { eventId } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [event, setEvent] = useState(null);
|
||||||
|
const [rsvps, setRsvps] = useState([]);
|
||||||
|
const [filteredRsvps, setFilteredRsvps] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
// Filters and search
|
||||||
|
const [activeTab, setActiveTab] = useState('all');
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
|
// Bulk selection
|
||||||
|
const [selectedRsvps, setSelectedRsvps] = useState(new Set());
|
||||||
|
const [selectAll, setSelectAll] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchEventAndRsvps();
|
||||||
|
}, [eventId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
filterRsvps();
|
||||||
|
}, [rsvps, activeTab, searchQuery]);
|
||||||
|
|
||||||
|
const fetchEventAndRsvps = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const [eventRes, rsvpsRes] = await Promise.all([
|
||||||
|
api.get(`/admin/events/${eventId}`),
|
||||||
|
api.get(`/admin/events/${eventId}/rsvps`)
|
||||||
|
]);
|
||||||
|
setEvent(eventRes.data);
|
||||||
|
setRsvps(rsvpsRes.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch event data:', error);
|
||||||
|
toast.error('Failed to load event data');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterRsvps = () => {
|
||||||
|
let filtered = [...rsvps];
|
||||||
|
|
||||||
|
// Filter by RSVP status tab
|
||||||
|
if (activeTab !== 'all') {
|
||||||
|
filtered = filtered.filter(rsvp => rsvp.rsvp_status === activeTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by search query
|
||||||
|
if (searchQuery) {
|
||||||
|
const query = searchQuery.toLowerCase();
|
||||||
|
filtered = filtered.filter(rsvp =>
|
||||||
|
rsvp.user_name?.toLowerCase().includes(query) ||
|
||||||
|
rsvp.user_email?.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredRsvps(filtered);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectAll = () => {
|
||||||
|
if (selectAll) {
|
||||||
|
setSelectedRsvps(new Set());
|
||||||
|
} else {
|
||||||
|
setSelectedRsvps(new Set(filteredRsvps.map(rsvp => rsvp.user_id)));
|
||||||
|
}
|
||||||
|
setSelectAll(!selectAll);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectRsvp = (userId) => {
|
||||||
|
const newSelected = new Set(selectedRsvps);
|
||||||
|
if (newSelected.has(userId)) {
|
||||||
|
newSelected.delete(userId);
|
||||||
|
} else {
|
||||||
|
newSelected.add(userId);
|
||||||
|
}
|
||||||
|
setSelectedRsvps(newSelected);
|
||||||
|
setSelectAll(newSelected.size === filteredRsvps.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBulkAttendance = async (attended) => {
|
||||||
|
if (selectedRsvps.size === 0) {
|
||||||
|
toast.error('Please select at least one RSVP');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSaving(true);
|
||||||
|
const updates = Array.from(selectedRsvps).map(userId => ({
|
||||||
|
user_id: userId,
|
||||||
|
attended
|
||||||
|
}));
|
||||||
|
|
||||||
|
await api.put(`/admin/events/${eventId}/attendance`, { updates });
|
||||||
|
|
||||||
|
toast.success(`Marked ${selectedRsvps.size} ${selectedRsvps.size === 1 ? 'person' : 'people'} as ${attended ? 'attended' : 'not attended'}`);
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
await fetchEventAndRsvps();
|
||||||
|
|
||||||
|
// Clear selection
|
||||||
|
setSelectedRsvps(new Set());
|
||||||
|
setSelectAll(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update attendance:', error);
|
||||||
|
toast.error('Failed to update attendance');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIndividualAttendance = async (userId, attended) => {
|
||||||
|
try {
|
||||||
|
setSaving(true);
|
||||||
|
const updates = [{
|
||||||
|
user_id: userId,
|
||||||
|
attended
|
||||||
|
}];
|
||||||
|
|
||||||
|
await api.put(`/admin/events/${eventId}/attendance`, { updates });
|
||||||
|
|
||||||
|
toast.success(`Attendance ${attended ? 'confirmed' : 'removed'}`);
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
await fetchEventAndRsvps();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update attendance:', error);
|
||||||
|
toast.error('Failed to update attendance');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportToCSV = () => {
|
||||||
|
if (filteredRsvps.length === 0) {
|
||||||
|
toast.error('No RSVPs to export');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSV header
|
||||||
|
const headers = ['Name', 'Email', 'RSVP Status', 'Attended', 'Attended At'];
|
||||||
|
|
||||||
|
// CSV rows
|
||||||
|
const rows = filteredRsvps.map(rsvp => [
|
||||||
|
`"${rsvp.user_name}"`,
|
||||||
|
`"${rsvp.user_email}"`,
|
||||||
|
`"${rsvp.rsvp_status.toUpperCase()}"`,
|
||||||
|
rsvp.attended ? 'Yes' : 'No',
|
||||||
|
rsvp.attended_at ? `"${moment(rsvp.attended_at).format('YYYY-MM-DD HH:mm A')}"` : ''
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Combine headers and rows
|
||||||
|
const csvContent = [
|
||||||
|
headers.join(','),
|
||||||
|
...rows.map(row => row.join(','))
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
// Create blob and download
|
||||||
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
link.setAttribute('href', url);
|
||||||
|
link.setAttribute('download', `${event?.title.replace(/\s+/g, '_')}_RSVPs_${moment().format('YYYY-MM-DD')}.csv`);
|
||||||
|
link.style.visibility = 'hidden';
|
||||||
|
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
|
||||||
|
toast.success('CSV exported successfully');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStats = () => {
|
||||||
|
const total = rsvps.length;
|
||||||
|
const yesCount = rsvps.filter(r => r.rsvp_status === 'yes').length;
|
||||||
|
const noCount = rsvps.filter(r => r.rsvp_status === 'no').length;
|
||||||
|
const maybeCount = rsvps.filter(r => r.rsvp_status === 'maybe').length;
|
||||||
|
const attendedCount = rsvps.filter(r => r.attended).length;
|
||||||
|
|
||||||
|
return { total, yesCount, noCount, maybeCount, attendedCount };
|
||||||
|
};
|
||||||
|
|
||||||
|
const stats = getStats();
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="text-[#664fa3]">Loading event data...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!event) {
|
||||||
|
return (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<p className="text-[#664fa3] mb-4">Event not found</p>
|
||||||
|
<Button onClick={() => navigate('/admin/events')} className="bg-[#664fa3] hover:bg-[#422268] text-white rounded-xl">
|
||||||
|
Back to Events
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Button
|
||||||
|
onClick={() => navigate('/admin/events')}
|
||||||
|
variant="outline"
|
||||||
|
className="border-[#ddd8eb] text-[#664fa3] 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-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Event Attendance
|
||||||
|
</h1>
|
||||||
|
<p className="text-[#664fa3] mt-1" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
Manage RSVPs and track attendance for this event
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={exportToCSV}
|
||||||
|
className="bg-[#81B29A] hover:bg-[#6a9a83] text-white rounded-xl"
|
||||||
|
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
Export to CSV
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Event Details Card */}
|
||||||
|
<Card className="p-6 bg-white border-[#ddd8eb] rounded-xl">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-semibold text-[#422268] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
{event.title}
|
||||||
|
</h2>
|
||||||
|
<div className="flex flex-wrap gap-4 text-[#664fa3]" 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>
|
||||||
|
</div>
|
||||||
|
{event.location && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<MapPin className="h-4 w-4" />
|
||||||
|
<span>{event.location}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge className={`${event.published ? 'bg-[#81B29A]' : 'bg-[#ddd8eb]'} text-white px-3 py-1`}>
|
||||||
|
{event.published ? 'Published' : 'Draft'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Statistics Cards */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||||
|
<Card className="p-4 bg-white border-[#ddd8eb] rounded-xl">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Users className="h-8 w-8 text-[#664fa3]" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total RSVPs</p>
|
||||||
|
<p className="text-2xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>{stats.total}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4 bg-white border-[#ddd8eb] rounded-xl">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<UserCheck className="h-8 w-8 text-[#81B29A]" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Yes</p>
|
||||||
|
<p className="text-2xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>{stats.yesCount}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4 bg-white border-[#ddd8eb] rounded-xl">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<UserX className="h-8 w-8 text-[#E07A5F]" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>No</p>
|
||||||
|
<p className="text-2xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>{stats.noCount}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4 bg-white border-[#ddd8eb] rounded-xl">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<HelpCircle className="h-8 w-8 text-[#F2CC8F]" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Maybe</p>
|
||||||
|
<p className="text-2xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>{stats.maybeCount}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4 bg-white border-[#ddd8eb] rounded-xl">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Check className="h-8 w-8 text-[#664fa3]" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Attended</p>
|
||||||
|
<p className="text-2xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>{stats.attendedCount}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters and Actions */}
|
||||||
|
<Card className="p-6 bg-white border-[#ddd8eb] rounded-xl">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Tab Filters */}
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={() => setActiveTab('all')}
|
||||||
|
variant={activeTab === 'all' ? 'default' : 'outline'}
|
||||||
|
className={`rounded-xl ${
|
||||||
|
activeTab === 'all'
|
||||||
|
? 'bg-[#664fa3] hover:bg-[#422268] text-white'
|
||||||
|
: 'border-[#ddd8eb] text-[#664fa3] hover:bg-[#F8F7FB]'
|
||||||
|
}`}
|
||||||
|
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||||
|
>
|
||||||
|
All ({stats.total})
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => setActiveTab('yes')}
|
||||||
|
variant={activeTab === 'yes' ? 'default' : 'outline'}
|
||||||
|
className={`rounded-xl ${
|
||||||
|
activeTab === 'yes'
|
||||||
|
? 'bg-[#81B29A] hover:bg-[#6a9a83] text-white'
|
||||||
|
: 'border-[#ddd8eb] text-[#664fa3] hover:bg-[#F8F7FB]'
|
||||||
|
}`}
|
||||||
|
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||||
|
>
|
||||||
|
Yes ({stats.yesCount})
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => setActiveTab('no')}
|
||||||
|
variant={activeTab === 'no' ? 'default' : 'outline'}
|
||||||
|
className={`rounded-xl ${
|
||||||
|
activeTab === 'no'
|
||||||
|
? 'bg-[#E07A5F] hover:bg-[#d16b54] text-white'
|
||||||
|
: 'border-[#ddd8eb] text-[#664fa3] hover:bg-[#F8F7FB]'
|
||||||
|
}`}
|
||||||
|
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||||
|
>
|
||||||
|
No ({stats.noCount})
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => setActiveTab('maybe')}
|
||||||
|
variant={activeTab === 'maybe' ? 'default' : 'outline'}
|
||||||
|
className={`rounded-xl ${
|
||||||
|
activeTab === 'maybe'
|
||||||
|
? 'bg-[#F2CC8F] hover:bg-[#e8bf7a] text-[#422268]'
|
||||||
|
: 'border-[#ddd8eb] text-[#664fa3] hover:bg-[#F8F7FB]'
|
||||||
|
}`}
|
||||||
|
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||||
|
>
|
||||||
|
Maybe ({stats.maybeCount})
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 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-[#664fa3]" />
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search by name or email..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-10 border-[#ddd8eb] rounded-xl"
|
||||||
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedRsvps.size > 0 && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Badge className="bg-[#664fa3] text-white px-3 py-1">
|
||||||
|
{selectedRsvps.size} selected
|
||||||
|
</Badge>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleBulkAttendance(true)}
|
||||||
|
disabled={saving}
|
||||||
|
className="bg-[#81B29A] hover:bg-[#6a9a83] text-white rounded-xl"
|
||||||
|
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||||
|
>
|
||||||
|
<Check className="h-4 w-4 mr-1" />
|
||||||
|
Mark Attended
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleBulkAttendance(false)}
|
||||||
|
disabled={saving}
|
||||||
|
className="bg-[#E07A5F] hover:bg-[#d16b54] text-white rounded-xl"
|
||||||
|
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4 mr-1" />
|
||||||
|
Mark Not Attended
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* RSVP Table */}
|
||||||
|
<Card className="bg-white border-[#ddd8eb] rounded-xl overflow-hidden">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-[#F8F7FB] border-b border-[#ddd8eb]">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left">
|
||||||
|
<Checkbox
|
||||||
|
checked={selectAll}
|
||||||
|
onCheckedChange={handleSelectAll}
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Email
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
RSVP Status
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-center text-sm font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Attendance
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Attended At
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredRsvps.length > 0 ? (
|
||||||
|
filteredRsvps.map((rsvp) => (
|
||||||
|
<tr key={rsvp.user_id} className="border-b border-[#ddd8eb] hover:bg-[#F8F7FB] 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-[#422268]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
{rsvp.user_name}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
{rsvp.user_email}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<Badge
|
||||||
|
className={`${
|
||||||
|
rsvp.rsvp_status === 'yes'
|
||||||
|
? 'bg-[#81B29A]'
|
||||||
|
: rsvp.rsvp_status === 'no'
|
||||||
|
? 'bg-[#E07A5F]'
|
||||||
|
: 'bg-[#F2CC8F] text-[#422268]'
|
||||||
|
} text-white text-xs px-2 py-1`}
|
||||||
|
>
|
||||||
|
{rsvp.rsvp_status.toUpperCase()}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center">
|
||||||
|
{rsvp.attended ? (
|
||||||
|
<Button
|
||||||
|
onClick={() => handleIndividualAttendance(rsvp.user_id, false)}
|
||||||
|
disabled={saving}
|
||||||
|
size="sm"
|
||||||
|
className="bg-[#81B29A] hover:bg-[#6a9a83] text-white rounded-lg min-w-[120px]"
|
||||||
|
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||||
|
>
|
||||||
|
<Check className="h-3 w-3 mr-1" />
|
||||||
|
Attended
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={() => handleIndividualAttendance(rsvp.user_id, true)}
|
||||||
|
disabled={saving}
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="border-[#ddd8eb] text-[#664fa3] hover:bg-[#81B29A] hover:text-white hover:border-[#81B29A] rounded-lg min-w-[120px]"
|
||||||
|
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3 mr-1" />
|
||||||
|
Not Attended
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
{rsvp.attended_at ? moment(rsvp.attended_at).format('MMM D, YYYY h:mm A') : '-'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan="6" className="px-4 py-12 text-center">
|
||||||
|
<p className="text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
|
{searchQuery ? 'No RSVPs match your search' : 'No RSVPs for this filter'}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminEventAttendance;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import api from '../../utils/api';
|
import api from '../../utils/api';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
@@ -8,16 +9,14 @@ import { Input } from '../../components/ui/input';
|
|||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '../../components/ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '../../components/ui/dialog';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Calendar, MapPin, Users, Plus, Edit, Trash2, Eye, EyeOff } from 'lucide-react';
|
import { Calendar, MapPin, Users, Plus, Edit, Trash2, Eye, EyeOff } from 'lucide-react';
|
||||||
import { AttendanceDialog } from '../../components/AttendanceDialog';
|
|
||||||
|
|
||||||
const AdminEvents = () => {
|
const AdminEvents = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const [events, setEvents] = useState([]);
|
const [events, setEvents] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||||
const [editingEvent, setEditingEvent] = useState(null);
|
const [editingEvent, setEditingEvent] = useState(null);
|
||||||
const [attendanceDialogOpen, setAttendanceDialogOpen] = useState(false);
|
|
||||||
const [selectedEvent, setSelectedEvent] = useState(null);
|
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
title: '',
|
title: '',
|
||||||
@@ -342,19 +341,16 @@ const AdminEvents = () => {
|
|||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="space-y-2 pt-4 border-t border-[#ddd8eb]">
|
<div className="space-y-2 pt-4 border-t border-[#ddd8eb]">
|
||||||
{/* Mark Attendance Button */}
|
{/* Manage Attendance Button */}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => navigate(`/admin/events/${event.id}/attendance`)}
|
||||||
setSelectedEvent(event);
|
|
||||||
setAttendanceDialogOpen(true);
|
|
||||||
}}
|
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-full border-[#81B29A] text-[#81B29A] hover:bg-[#81B29A] hover:text-white"
|
className="w-full border-[#81B29A] text-[#81B29A] hover:bg-[#81B29A] hover:text-white"
|
||||||
data-testid={`mark-attendance-${event.id}`}
|
data-testid={`mark-attendance-${event.id}`}
|
||||||
>
|
>
|
||||||
<Users className="h-4 w-4 mr-2" />
|
<Users className="h-4 w-4 mr-2" />
|
||||||
Mark Attendance ({event.rsvp_count || 0} RSVPs)
|
Manage Attendance ({event.rsvp_count || 0} RSVPs)
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Other Actions */}
|
{/* Other Actions */}
|
||||||
@@ -419,14 +415,6 @@ const AdminEvents = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Attendance Dialog */}
|
|
||||||
<AttendanceDialog
|
|
||||||
event={selectedEvent}
|
|
||||||
open={attendanceDialogOpen}
|
|
||||||
onOpenChange={setAttendanceDialogOpen}
|
|
||||||
onSuccess={fetchEvents}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import api from '../../utils/api';
|
import api from '../../utils/api';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import { Button } from '../../components/ui/button';
|
import { Button } from '../../components/ui/button';
|
||||||
@@ -31,6 +32,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const AdminFinancials = () => {
|
const AdminFinancials = () => {
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
const [reports, setReports] = useState([]);
|
const [reports, setReports] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
@@ -42,7 +44,7 @@ const AdminFinancials = () => {
|
|||||||
year: new Date().getFullYear(),
|
year: new Date().getFullYear(),
|
||||||
title: '',
|
title: '',
|
||||||
document_url: '',
|
document_url: '',
|
||||||
document_type: 'google_drive'
|
document_type: 'link'
|
||||||
});
|
});
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
@@ -67,8 +69,9 @@ const AdminFinancials = () => {
|
|||||||
year: new Date().getFullYear(),
|
year: new Date().getFullYear(),
|
||||||
title: '',
|
title: '',
|
||||||
document_url: '',
|
document_url: '',
|
||||||
document_type: 'google_drive'
|
document_type: 'link'
|
||||||
});
|
});
|
||||||
|
setUploadedFile(null);
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -161,6 +164,7 @@ const AdminFinancials = () => {
|
|||||||
Manage annual financial reports
|
Manage annual financial reports
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{hasPermission('financials.create') && (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
className="bg-[#664fa3] text-white hover:bg-[#533a82] rounded-full flex items-center gap-2"
|
className="bg-[#664fa3] text-white hover:bg-[#533a82] rounded-full flex items-center gap-2"
|
||||||
@@ -168,6 +172,7 @@ const AdminFinancials = () => {
|
|||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
Add Report
|
Add Report
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Reports List */}
|
{/* Reports List */}
|
||||||
@@ -175,10 +180,12 @@ const AdminFinancials = () => {
|
|||||||
<Card className="p-12 text-center">
|
<Card className="p-12 text-center">
|
||||||
<TrendingUp className="h-16 w-16 text-[#ddd8eb] mx-auto mb-4" />
|
<TrendingUp className="h-16 w-16 text-[#ddd8eb] mx-auto mb-4" />
|
||||||
<p className="text-[#664fa3] text-lg mb-4">No financial reports yet</p>
|
<p className="text-[#664fa3] text-lg mb-4">No financial reports yet</p>
|
||||||
|
{hasPermission('financials.create') && (
|
||||||
<Button onClick={handleCreate} className="bg-[#664fa3] text-white">
|
<Button onClick={handleCreate} className="bg-[#664fa3] text-white">
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
Create First Report
|
Create First Report
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -208,7 +215,9 @@ const AdminFinancials = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{(hasPermission('financials.edit') || hasPermission('financials.delete')) && (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
{hasPermission('financials.edit') && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -217,6 +226,8 @@ const AdminFinancials = () => {
|
|||||||
>
|
>
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
|
{hasPermission('financials.delete') && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -225,7 +236,9 @@ const AdminFinancials = () => {
|
|||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
@@ -274,14 +287,16 @@ const AdminFinancials = () => {
|
|||||||
<Label htmlFor="document_type">Document Type *</Label>
|
<Label htmlFor="document_type">Document Type *</Label>
|
||||||
<Select
|
<Select
|
||||||
value={formData.document_type}
|
value={formData.document_type}
|
||||||
onValueChange={(value) => setFormData({ ...formData, document_type: value })}
|
onValueChange={(value) => {
|
||||||
|
setFormData({ ...formData, document_type: value, document_url: '' });
|
||||||
|
setUploadedFile(null);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="google_drive">Google Drive</SelectItem>
|
<SelectItem value="link">Link</SelectItem>
|
||||||
<SelectItem value="pdf">PDF</SelectItem>
|
|
||||||
<SelectItem value="upload">Upload</SelectItem>
|
<SelectItem value="upload">Upload</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -302,6 +317,11 @@ const AdminFinancials = () => {
|
|||||||
Selected: {uploadedFile.name}
|
Selected: {uploadedFile.name}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{selectedReport && !uploadedFile && (
|
||||||
|
<p className="text-sm text-[#664fa3] mt-1">
|
||||||
|
Current file will be kept if no new file is selected
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
@@ -310,12 +330,11 @@ const AdminFinancials = () => {
|
|||||||
id="document_url"
|
id="document_url"
|
||||||
value={formData.document_url}
|
value={formData.document_url}
|
||||||
onChange={(e) => setFormData({ ...formData, document_url: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, document_url: e.target.value })}
|
||||||
placeholder="https://drive.google.com/file/d/..."
|
placeholder="https://docs.google.com/... or https://example.com/file.pdf"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-[#664fa3] mt-1">
|
<p className="text-sm text-[#664fa3] mt-1">
|
||||||
{formData.document_type === 'google_drive' && 'Paste the shareable link to your Google Drive file'}
|
Paste the shareable link to your document (Google Drive, Dropbox, PDF URL, etc.)
|
||||||
{formData.document_type === 'pdf' && 'Paste the URL to your PDF file'}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import api from '../../utils/api';
|
import api from '../../utils/api';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import { Button } from '../../components/ui/button';
|
import { Button } from '../../components/ui/button';
|
||||||
@@ -14,11 +16,12 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '../../components/ui/select';
|
} from '../../components/ui/select';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '../../components/ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '../../components/ui/dialog';
|
||||||
import { Upload, Trash2, Edit, X, ImageIcon, Calendar, MapPin } from 'lucide-react';
|
import { Upload, Trash2, Edit, X, ImageIcon, Calendar, MapPin, AlertCircle } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
const AdminGallery = () => {
|
const AdminGallery = () => {
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
const [events, setEvents] = useState([]);
|
const [events, setEvents] = useState([]);
|
||||||
const [selectedEvent, setSelectedEvent] = useState(null);
|
const [selectedEvent, setSelectedEvent] = useState(null);
|
||||||
const [galleryImages, setGalleryImages] = useState([]);
|
const [galleryImages, setGalleryImages] = useState([]);
|
||||||
@@ -179,7 +182,33 @@ const AdminGallery = () => {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedEvent && (
|
{/* Empty State Message */}
|
||||||
|
{events.length === 0 && (
|
||||||
|
<div className="mt-4 p-4 bg-[#f1eef9] border-2 border-[#DDD8EB] rounded-xl">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<AlertCircle className="h-5 w-5 text-[#664fa3] flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="text-sm font-semibold text-[#422268] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
No Events Available
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-[#664fa3] 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="bg-[#664fa3] hover:bg-[#422268] text-white rounded-xl text-sm"
|
||||||
|
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||||
|
>
|
||||||
|
<Calendar className="h-4 w-4 mr-2" />
|
||||||
|
Create Your First Event
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedEvent && hasPermission('gallery.upload') && (
|
||||||
<div className="pt-4">
|
<div className="pt-4">
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
@@ -240,7 +269,9 @@ const AdminGallery = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Overlay with Actions */}
|
{/* Overlay with Actions */}
|
||||||
|
{(hasPermission('gallery.edit') || hasPermission('gallery.delete')) && (
|
||||||
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity rounded-xl flex flex-col items-center justify-center gap-2">
|
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity rounded-xl flex flex-col items-center justify-center gap-2">
|
||||||
|
{hasPermission('gallery.edit') && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => openEditCaption(image)}
|
onClick={() => openEditCaption(image)}
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -250,6 +281,8 @@ const AdminGallery = () => {
|
|||||||
<Edit className="h-4 w-4 mr-1" />
|
<Edit className="h-4 w-4 mr-1" />
|
||||||
Caption
|
Caption
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
|
{hasPermission('gallery.delete') && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleDeleteImage(image.id)}
|
onClick={() => handleDeleteImage(image.id)}
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -259,7 +292,9 @@ const AdminGallery = () => {
|
|||||||
<Trash2 className="h-4 w-4 mr-1" />
|
<Trash2 className="h-4 w-4 mr-1" />
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Caption Preview */}
|
{/* Caption Preview */}
|
||||||
{image.caption && (
|
{image.caption && (
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import PaymentActivationDialog from '../../components/PaymentActivationDialog';
|
|||||||
import ConfirmationDialog from '../../components/ConfirmationDialog';
|
import ConfirmationDialog from '../../components/ConfirmationDialog';
|
||||||
import CreateMemberDialog from '../../components/CreateMemberDialog';
|
import CreateMemberDialog from '../../components/CreateMemberDialog';
|
||||||
import InviteStaffDialog from '../../components/InviteStaffDialog';
|
import InviteStaffDialog from '../../components/InviteStaffDialog';
|
||||||
import ImportMembersDialog from '../../components/ImportMembersDialog';
|
import WordPressImportWizard from '../../components/WordPressImportWizard';
|
||||||
|
|
||||||
const AdminMembers = () => {
|
const AdminMembers = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -569,7 +569,7 @@ const AdminMembers = () => {
|
|||||||
onSuccess={fetchMembers}
|
onSuccess={fetchMembers}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ImportMembersDialog
|
<WordPressImportWizard
|
||||||
open={importDialogOpen}
|
open={importDialogOpen}
|
||||||
onOpenChange={setImportDialogOpen}
|
onOpenChange={setImportDialogOpen}
|
||||||
onSuccess={fetchMembers}
|
onSuccess={fetchMembers}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import api from '../../utils/api';
|
import api from '../../utils/api';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import { Button } from '../../components/ui/button';
|
import { Button } from '../../components/ui/button';
|
||||||
@@ -32,6 +33,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const AdminNewsletters = () => {
|
const AdminNewsletters = () => {
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
const [newsletters, setNewsletters] = useState([]);
|
const [newsletters, setNewsletters] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
@@ -44,7 +46,7 @@ const AdminNewsletters = () => {
|
|||||||
description: '',
|
description: '',
|
||||||
published_date: '',
|
published_date: '',
|
||||||
document_url: '',
|
document_url: '',
|
||||||
document_type: 'google_docs'
|
document_type: 'link'
|
||||||
});
|
});
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
@@ -70,8 +72,9 @@ const AdminNewsletters = () => {
|
|||||||
description: '',
|
description: '',
|
||||||
published_date: new Date().toISOString().split('T')[0],
|
published_date: new Date().toISOString().split('T')[0],
|
||||||
document_url: '',
|
document_url: '',
|
||||||
document_type: 'google_docs'
|
document_type: 'link'
|
||||||
});
|
});
|
||||||
|
setUploadedFile(null);
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -189,6 +192,7 @@ const AdminNewsletters = () => {
|
|||||||
Create and manage newsletter archive
|
Create and manage newsletter archive
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{hasPermission('newsletters.create') && (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
className="bg-[#664fa3] text-white hover:bg-[#533a82] rounded-full flex items-center gap-2"
|
className="bg-[#664fa3] text-white hover:bg-[#533a82] rounded-full flex items-center gap-2"
|
||||||
@@ -196,6 +200,7 @@ const AdminNewsletters = () => {
|
|||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
Add Newsletter
|
Add Newsletter
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Newsletters List */}
|
{/* Newsletters List */}
|
||||||
@@ -203,10 +208,12 @@ const AdminNewsletters = () => {
|
|||||||
<Card className="p-12 text-center">
|
<Card className="p-12 text-center">
|
||||||
<FileText className="h-16 w-16 text-[#ddd8eb] mx-auto mb-4" />
|
<FileText className="h-16 w-16 text-[#ddd8eb] mx-auto mb-4" />
|
||||||
<p className="text-[#664fa3] text-lg mb-4">No newsletters yet</p>
|
<p className="text-[#664fa3] text-lg mb-4">No newsletters yet</p>
|
||||||
|
{hasPermission('newsletters.create') && (
|
||||||
<Button onClick={handleCreate} className="bg-[#664fa3] text-white">
|
<Button onClick={handleCreate} className="bg-[#664fa3] text-white">
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
Create First Newsletter
|
Create First Newsletter
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -232,7 +239,7 @@ const AdminNewsletters = () => {
|
|||||||
{formatDate(newsletter.published_date)}
|
{formatDate(newsletter.published_date)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="outline" className="border-[#664fa3] text-[#664fa3]">
|
<Badge variant="outline" className="border-[#664fa3] text-[#664fa3]">
|
||||||
{newsletter.document_type === 'google_docs' ? 'Google Docs' : newsletter.document_type.toUpperCase()}
|
{newsletter.document_type === 'upload' ? 'PDF Upload' : 'Link'}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -245,7 +252,9 @@ const AdminNewsletters = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{(hasPermission('newsletters.edit') || hasPermission('newsletters.delete')) && (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
{hasPermission('newsletters.edit') && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -254,6 +263,8 @@ const AdminNewsletters = () => {
|
|||||||
>
|
>
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
|
{hasPermission('newsletters.delete') && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -262,7 +273,9 @@ const AdminNewsletters = () => {
|
|||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
@@ -322,14 +335,16 @@ const AdminNewsletters = () => {
|
|||||||
<Label htmlFor="document_type">Document Type *</Label>
|
<Label htmlFor="document_type">Document Type *</Label>
|
||||||
<Select
|
<Select
|
||||||
value={formData.document_type}
|
value={formData.document_type}
|
||||||
onValueChange={(value) => setFormData({ ...formData, document_type: value })}
|
onValueChange={(value) => {
|
||||||
|
setFormData({ ...formData, document_type: value, document_url: '' });
|
||||||
|
setUploadedFile(null);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="google_docs">Google Docs</SelectItem>
|
<SelectItem value="link">Link</SelectItem>
|
||||||
<SelectItem value="pdf">PDF</SelectItem>
|
|
||||||
<SelectItem value="upload">Upload</SelectItem>
|
<SelectItem value="upload">Upload</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -350,6 +365,11 @@ const AdminNewsletters = () => {
|
|||||||
Selected: {uploadedFile.name}
|
Selected: {uploadedFile.name}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{selectedNewsletter && !uploadedFile && (
|
||||||
|
<p className="text-sm text-[#664fa3] mt-1">
|
||||||
|
Current file will be kept if no new file is selected
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
@@ -358,12 +378,11 @@ const AdminNewsletters = () => {
|
|||||||
id="document_url"
|
id="document_url"
|
||||||
value={formData.document_url}
|
value={formData.document_url}
|
||||||
onChange={(e) => setFormData({ ...formData, document_url: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, document_url: e.target.value })}
|
||||||
placeholder="https://docs.google.com/document/d/..."
|
placeholder="https://docs.google.com/document/d/... or https://example.com/file.pdf"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-[#664fa3] mt-1">
|
<p className="text-sm text-[#664fa3] mt-1">
|
||||||
{formData.document_type === 'google_docs' && 'Paste the shareable link to your Google Doc'}
|
Paste the shareable link to your document (Google Docs, Dropbox, PDF URL, etc.)
|
||||||
{formData.document_type === 'pdf' && 'Paste the URL to your PDF file'}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import api from '../../utils/api';
|
import api from '../../utils/api';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import { Button } from '../../components/ui/button';
|
import { Button } from '../../components/ui/button';
|
||||||
@@ -24,6 +25,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const AdminPlans = () => {
|
const AdminPlans = () => {
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
const [plans, setPlans] = useState([]);
|
const [plans, setPlans] = useState([]);
|
||||||
const [filteredPlans, setFilteredPlans] = useState([]);
|
const [filteredPlans, setFilteredPlans] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -136,6 +138,7 @@ const AdminPlans = () => {
|
|||||||
Manage membership plans and pricing.
|
Manage membership plans and pricing.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{hasPermission('subscriptions.plans') && (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleCreatePlan}
|
onClick={handleCreatePlan}
|
||||||
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-6"
|
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-6"
|
||||||
@@ -143,6 +146,7 @@ const AdminPlans = () => {
|
|||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
Create Plan
|
Create Plan
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -286,6 +290,7 @@ const AdminPlans = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
|
{hasPermission('subscriptions.plans') && (
|
||||||
<div className="flex flex-col sm:flex-row gap-2 pt-4 border-t border-[#ddd8eb]">
|
<div className="flex flex-col sm:flex-row gap-2 pt-4 border-t border-[#ddd8eb]">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleEditPlan(plan)}
|
onClick={() => handleEditPlan(plan)}
|
||||||
@@ -307,6 +312,7 @@ const AdminPlans = () => {
|
|||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Warning for plans with subscribers */}
|
{/* Warning for plans with subscribers */}
|
||||||
{plan.subscriber_count > 0 && (
|
{plan.subscriber_count > 0 && (
|
||||||
@@ -328,7 +334,7 @@ const AdminPlans = () => {
|
|||||||
? 'Try adjusting your filters'
|
? 'Try adjusting your filters'
|
||||||
: 'Create your first subscription plan to get started'}
|
: 'Create your first subscription plan to get started'}
|
||||||
</p>
|
</p>
|
||||||
{!searchQuery && activeFilter === 'all' && (
|
{!searchQuery && activeFilter === 'all' && hasPermission('subscriptions.plans') && (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleCreatePlan}
|
onClick={handleCreatePlan}
|
||||||
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-8"
|
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-8"
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ import CreateStaffDialog from '../../components/CreateStaffDialog';
|
|||||||
import InviteStaffDialog from '../../components/InviteStaffDialog';
|
import InviteStaffDialog from '../../components/InviteStaffDialog';
|
||||||
import PendingInvitationsTable from '../../components/PendingInvitationsTable';
|
import PendingInvitationsTable from '../../components/PendingInvitationsTable';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { UserCog, Search, Shield, UserPlus, Mail, Edit, Eye } from 'lucide-react';
|
import { UserCog, Search, Shield, UserPlus, Mail, Edit, Eye, Trash2, UserCheck, UserX } from 'lucide-react';
|
||||||
|
|
||||||
const AdminStaff = () => {
|
const AdminStaff = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission, user } = useAuth();
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
const [filteredUsers, setFilteredUsers] = useState([]);
|
const [filteredUsers, setFilteredUsers] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -71,6 +71,32 @@ const AdminStaff = () => {
|
|||||||
setFilteredUsers(filtered);
|
setFilteredUsers(filtered);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToggleStatus = async (userId, currentStatus) => {
|
||||||
|
const newStatus = currentStatus === 'active' ? 'inactive' : 'active';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.put(`/admin/users/${userId}/status`, { status: newStatus });
|
||||||
|
toast.success(`User ${newStatus === 'active' ? 'activated' : 'deactivated'} successfully`);
|
||||||
|
fetchStaff(); // Refresh list
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error.response?.data?.detail || 'Failed to update user status');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteUser = async (userId, userName) => {
|
||||||
|
if (!window.confirm(`Are you sure you want to delete ${userName}? This action cannot be undone.`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.delete(`/admin/users/${userId}`);
|
||||||
|
toast.success('User deleted successfully');
|
||||||
|
fetchStaff(); // Refresh list
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error.response?.data?.detail || 'Failed to delete user');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getRoleBadge = (role) => {
|
const getRoleBadge = (role) => {
|
||||||
const config = {
|
const config = {
|
||||||
superadmin: { label: 'Superadmin', className: 'bg-[#664fa3] text-white' },
|
superadmin: { label: 'Superadmin', className: 'bg-[#664fa3] text-white' },
|
||||||
@@ -116,7 +142,7 @@ const AdminStaff = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
{hasPermission('users.invite') && (
|
{hasPermission('users.create') && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setInviteDialogOpen(true)}
|
onClick={() => setInviteDialogOpen(true)}
|
||||||
className="bg-[#664fa3] hover:bg-[#422268] text-white rounded-xl h-12 px-6"
|
className="bg-[#664fa3] hover:bg-[#422268] text-white rounded-xl h-12 px-6"
|
||||||
@@ -250,7 +276,7 @@ const AdminStaff = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2 flex-wrap">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => navigate(`/admin/users/${user.id}`)}
|
onClick={() => navigate(`/admin/users/${user.id}`)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -259,6 +285,41 @@ const AdminStaff = () => {
|
|||||||
<Edit className="h-4 w-4 mr-2" />
|
<Edit className="h-4 w-4 mr-2" />
|
||||||
Manage
|
Manage
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{hasPermission('users.status') && (
|
||||||
|
<Button
|
||||||
|
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'
|
||||||
|
: 'border-green-500 text-green-600 hover:bg-green-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{user.status === 'active' ? (
|
||||||
|
<>
|
||||||
|
<UserX className="h-4 w-4 mr-2" />
|
||||||
|
Deactivate
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<UserCheck className="h-4 w-4 mr-2" />
|
||||||
|
Activate
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasPermission('users.delete') && (
|
||||||
|
<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 rounded-full px-4 py-2"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import { Button } from '../../components/ui/button';
|
import { Button } from '../../components/ui/button';
|
||||||
import { Input } from '../../components/ui/input';
|
import { Input } from '../../components/ui/input';
|
||||||
@@ -44,6 +45,7 @@ import {
|
|||||||
} from '../../components/ui/dropdown-menu';
|
} from '../../components/ui/dropdown-menu';
|
||||||
|
|
||||||
const AdminSubscriptions = () => {
|
const AdminSubscriptions = () => {
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
const [subscriptions, setSubscriptions] = useState([]);
|
const [subscriptions, setSubscriptions] = useState([]);
|
||||||
const [filteredSubscriptions, setFilteredSubscriptions] = useState([]);
|
const [filteredSubscriptions, setFilteredSubscriptions] = useState([]);
|
||||||
const [plans, setPlans] = useState([]);
|
const [plans, setPlans] = useState([]);
|
||||||
@@ -412,6 +414,7 @@ Proceed with activation?`;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Export Dropdown */}
|
{/* Export Dropdown */}
|
||||||
|
{hasPermission('subscriptions.export') && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
@@ -439,6 +442,7 @@ Proceed with activation?`;
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -503,6 +507,7 @@ Proceed with activation?`;
|
|||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex gap-2 pt-2">
|
<div className="flex gap-2 pt-2">
|
||||||
|
{hasPermission('subscriptions.edit') && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -512,7 +517,8 @@ Proceed with activation?`;
|
|||||||
<Edit className="h-4 w-4 mr-2" />
|
<Edit className="h-4 w-4 mr-2" />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
{sub.status === 'active' && (
|
)}
|
||||||
|
{sub.status === 'active' && hasPermission('subscriptions.cancel') && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -607,6 +613,7 @@ Proceed with activation?`;
|
|||||||
</td>
|
</td>
|
||||||
<td className="p-4">
|
<td className="p-4">
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
{hasPermission('subscriptions.edit') && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -615,7 +622,8 @@ Proceed with activation?`;
|
|||||||
>
|
>
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
{sub.status === 'active' && (
|
)}
|
||||||
|
{sub.status === 'active' && hasPermission('subscriptions.cancel') && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import api from '../../utils/api';
|
import api from '../../utils/api';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import { Button } from '../../components/ui/button';
|
import { Button } from '../../components/ui/button';
|
||||||
@@ -35,6 +36,7 @@ import ConfirmationDialog from '../../components/ConfirmationDialog';
|
|||||||
import RejectionDialog from '../../components/RejectionDialog';
|
import RejectionDialog from '../../components/RejectionDialog';
|
||||||
|
|
||||||
const AdminValidations = () => {
|
const AdminValidations = () => {
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
const [pendingUsers, setPendingUsers] = useState([]);
|
const [pendingUsers, setPendingUsers] = useState([]);
|
||||||
const [filteredUsers, setFilteredUsers] = useState([]);
|
const [filteredUsers, setFilteredUsers] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -74,7 +76,7 @@ const AdminValidations = () => {
|
|||||||
try {
|
try {
|
||||||
const response = await api.get('/admin/users');
|
const response = await api.get('/admin/users');
|
||||||
const pending = response.data.filter(user =>
|
const pending = response.data.filter(user =>
|
||||||
['pending_email', 'pending_validation', 'pre_validated', 'payment_pending'].includes(user.status)
|
['pending_email', 'pending_validation', 'pre_validated', 'payment_pending', 'rejected'].includes(user.status)
|
||||||
);
|
);
|
||||||
setPendingUsers(pending);
|
setPendingUsers(pending);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -218,12 +220,28 @@ const AdminValidations = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleReactivateUser = async (user) => {
|
||||||
|
setActionLoading(user.id);
|
||||||
|
try {
|
||||||
|
await api.put(`/admin/users/${user.id}/status`, {
|
||||||
|
status: 'pending_validation'
|
||||||
|
});
|
||||||
|
toast.success(`${user.first_name} ${user.last_name} has been reactivated and moved to pending validation`);
|
||||||
|
fetchPendingUsers();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error.response?.data?.detail || 'Failed to reactivate user');
|
||||||
|
} finally {
|
||||||
|
setActionLoading(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getStatusBadge = (status) => {
|
const getStatusBadge = (status) => {
|
||||||
const config = {
|
const config = {
|
||||||
pending_email: { label: 'Awaiting Email', className: 'bg-orange-100 text-orange-700' },
|
pending_email: { label: 'Awaiting Email', className: 'bg-orange-100 text-orange-700' },
|
||||||
pending_validation: { label: 'Pending Validation', className: 'bg-gray-200 text-gray-700' },
|
pending_validation: { label: 'Pending Validation', className: 'bg-gray-200 text-gray-700' },
|
||||||
pre_validated: { label: 'Pre-Validated', className: 'bg-[#81B29A] text-white' },
|
pre_validated: { label: 'Pre-Validated', className: 'bg-[#81B29A] text-white' },
|
||||||
payment_pending: { label: 'Payment Pending', className: 'bg-orange-500 text-white' }
|
payment_pending: { label: 'Payment Pending', className: 'bg-orange-500 text-white' },
|
||||||
|
rejected: { label: 'Rejected', className: 'bg-red-100 text-red-700' }
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusConfig = config[status];
|
const statusConfig = config[status];
|
||||||
@@ -302,6 +320,12 @@ const AdminValidations = () => {
|
|||||||
{pendingUsers.filter(u => u.status === 'payment_pending').length}
|
{pendingUsers.filter(u => u.status === 'payment_pending').length}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-red-600 mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Rejected</p>
|
||||||
|
<p className="text-3xl font-semibold text-red-800" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
{pendingUsers.filter(u => u.status === 'rejected').length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -327,6 +351,8 @@ const AdminValidations = () => {
|
|||||||
<SelectItem value="pending_email">Awaiting Email</SelectItem>
|
<SelectItem value="pending_email">Awaiting Email</SelectItem>
|
||||||
<SelectItem value="pending_validation">Pending Validation</SelectItem>
|
<SelectItem value="pending_validation">Pending Validation</SelectItem>
|
||||||
<SelectItem value="pre_validated">Pre-Validated</SelectItem>
|
<SelectItem value="pre_validated">Pre-Validated</SelectItem>
|
||||||
|
<SelectItem value="payment_pending">Payment Pending</SelectItem>
|
||||||
|
<SelectItem value="rejected">Rejected</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -384,8 +410,18 @@ const AdminValidations = () => {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{user.status === 'pending_email' ? (
|
{user.status === 'rejected' ? (
|
||||||
|
<Button
|
||||||
|
onClick={() => handleReactivateUser(user)}
|
||||||
|
disabled={actionLoading === user.id}
|
||||||
|
size="sm"
|
||||||
|
className="bg-[#81B29A] text-white hover:bg-[#6FA087]"
|
||||||
|
>
|
||||||
|
{actionLoading === user.id ? 'Reactivating...' : 'Reactivate'}
|
||||||
|
</Button>
|
||||||
|
) : user.status === 'pending_email' ? (
|
||||||
<>
|
<>
|
||||||
|
{hasPermission('users.approve') && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleBypassAndValidateRequest(user)}
|
onClick={() => handleBypassAndValidateRequest(user)}
|
||||||
disabled={actionLoading === user.id}
|
disabled={actionLoading === user.id}
|
||||||
@@ -394,6 +430,8 @@ const AdminValidations = () => {
|
|||||||
>
|
>
|
||||||
{actionLoading === user.id ? 'Validating...' : 'Bypass & Validate'}
|
{actionLoading === user.id ? 'Validating...' : 'Bypass & Validate'}
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
|
{hasPermission('users.approve') && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleRejectUser(user)}
|
onClick={() => handleRejectUser(user)}
|
||||||
disabled={actionLoading === user.id}
|
disabled={actionLoading === user.id}
|
||||||
@@ -404,9 +442,11 @@ const AdminValidations = () => {
|
|||||||
<X className="h-4 w-4 mr-1" />
|
<X className="h-4 w-4 mr-1" />
|
||||||
Reject
|
Reject
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : user.status === 'payment_pending' ? (
|
) : user.status === 'payment_pending' ? (
|
||||||
<>
|
<>
|
||||||
|
{hasPermission('subscriptions.activate') && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleActivatePayment(user)}
|
onClick={() => handleActivatePayment(user)}
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -415,6 +455,8 @@ const AdminValidations = () => {
|
|||||||
<CheckCircle className="h-4 w-4 mr-1" />
|
<CheckCircle className="h-4 w-4 mr-1" />
|
||||||
Activate Payment
|
Activate Payment
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
|
{hasPermission('users.approve') && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleRejectUser(user)}
|
onClick={() => handleRejectUser(user)}
|
||||||
disabled={actionLoading === user.id}
|
disabled={actionLoading === user.id}
|
||||||
@@ -425,9 +467,11 @@ const AdminValidations = () => {
|
|||||||
<X className="h-4 w-4 mr-1" />
|
<X className="h-4 w-4 mr-1" />
|
||||||
Reject
|
Reject
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
{hasPermission('users.approve') && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleValidateRequest(user)}
|
onClick={() => handleValidateRequest(user)}
|
||||||
disabled={actionLoading === user.id}
|
disabled={actionLoading === user.id}
|
||||||
@@ -436,6 +480,8 @@ const AdminValidations = () => {
|
|||||||
>
|
>
|
||||||
{actionLoading === user.id ? 'Validating...' : 'Validate'}
|
{actionLoading === user.id ? 'Validating...' : 'Validate'}
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
|
{hasPermission('users.approve') && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleRejectUser(user)}
|
onClick={() => handleRejectUser(user)}
|
||||||
disabled={actionLoading === user.id}
|
disabled={actionLoading === user.id}
|
||||||
@@ -446,6 +492,7 @@ const AdminValidations = () => {
|
|||||||
<X className="h-4 w-4 mr-1" />
|
<X className="h-4 w-4 mr-1" />
|
||||||
Reject
|
Reject
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
93
src/pages/members/MemberCalendar.css
Normal file
93
src/pages/members/MemberCalendar.css
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/* Member Calendar Custom Styles */
|
||||||
|
|
||||||
|
.member-calendar .rbc-header {
|
||||||
|
padding: 12px 6px;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #422268;
|
||||||
|
background-color: #f9f7fc;
|
||||||
|
border-bottom: 2px solid #ddd8eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-calendar .rbc-today {
|
||||||
|
background-color: #f1eef9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-calendar .rbc-off-range-bg {
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-calendar .rbc-event {
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-calendar .rbc-event:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-calendar .rbc-toolbar button {
|
||||||
|
color: #664fa3;
|
||||||
|
border-color: #ddd8eb;
|
||||||
|
font-family: 'Nunito Sans', sans-serif;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background-color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-calendar .rbc-toolbar button:hover {
|
||||||
|
background-color: #f1eef9;
|
||||||
|
border-color: #664fa3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-calendar .rbc-toolbar button:active,
|
||||||
|
.member-calendar .rbc-toolbar button.rbc-active {
|
||||||
|
background-color: #664fa3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-calendar .rbc-month-view {
|
||||||
|
border: 1px solid #ddd8eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-calendar .rbc-day-bg {
|
||||||
|
border-color: #ddd8eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-calendar .rbc-date-cell {
|
||||||
|
padding: 8px;
|
||||||
|
font-family: 'Nunito Sans', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure toolbar buttons are clickable */
|
||||||
|
.member-calendar .rbc-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-calendar .rbc-toolbar button {
|
||||||
|
outline: none;
|
||||||
|
border: 1px solid #ddd8eb;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-calendar .rbc-btn-group button {
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-calendar .rbc-btn-group button:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-calendar .rbc-btn-group button:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useMemo } from 'react';
|
|||||||
import { Calendar, momentLocalizer } from 'react-big-calendar';
|
import { Calendar, momentLocalizer } from 'react-big-calendar';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import 'react-big-calendar/lib/css/react-big-calendar.css';
|
import 'react-big-calendar/lib/css/react-big-calendar.css';
|
||||||
|
import './MemberCalendar.css';
|
||||||
import api from '../../utils/api';
|
import api from '../../utils/api';
|
||||||
import Navbar from '../../components/Navbar';
|
import Navbar from '../../components/Navbar';
|
||||||
import MemberFooter from '../../components/MemberFooter';
|
import MemberFooter from '../../components/MemberFooter';
|
||||||
@@ -26,6 +27,8 @@ export default function MemberCalendar() {
|
|||||||
const [selectedEvent, setSelectedEvent] = useState(null);
|
const [selectedEvent, setSelectedEvent] = useState(null);
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
const [rsvpLoading, setRsvpLoading] = useState(false);
|
const [rsvpLoading, setRsvpLoading] = useState(false);
|
||||||
|
const [currentDate, setCurrentDate] = useState(new Date());
|
||||||
|
const [currentView, setCurrentView] = useState('month');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchEvents();
|
fetchEvents();
|
||||||
@@ -58,6 +61,14 @@ export default function MemberCalendar() {
|
|||||||
setIsDialogOpen(true);
|
setIsDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleNavigate = (newDate) => {
|
||||||
|
setCurrentDate(newDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewChange = (newView) => {
|
||||||
|
setCurrentView(newView);
|
||||||
|
};
|
||||||
|
|
||||||
const handleRSVP = async (status) => {
|
const handleRSVP = async (status) => {
|
||||||
if (!selectedEvent) return;
|
if (!selectedEvent) return;
|
||||||
|
|
||||||
@@ -171,10 +182,13 @@ export default function MemberCalendar() {
|
|||||||
startAccessor="start"
|
startAccessor="start"
|
||||||
endAccessor="end"
|
endAccessor="end"
|
||||||
style={{ height: 700 }}
|
style={{ height: 700 }}
|
||||||
|
date={currentDate}
|
||||||
|
view={currentView}
|
||||||
|
onNavigate={handleNavigate}
|
||||||
|
onView={handleViewChange}
|
||||||
onSelectEvent={handleSelectEvent}
|
onSelectEvent={handleSelectEvent}
|
||||||
eventPropGetter={eventStyleGetter}
|
eventPropGetter={eventStyleGetter}
|
||||||
views={['month', 'week', 'day', 'agenda']}
|
views={['month', 'week', 'day', 'agenda']}
|
||||||
defaultView="month"
|
|
||||||
popup
|
popup
|
||||||
className="member-calendar"
|
className="member-calendar"
|
||||||
/>
|
/>
|
||||||
@@ -320,64 +334,6 @@ export default function MemberCalendar() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style jsx global>{`
|
|
||||||
.member-calendar .rbc-header {
|
|
||||||
padding: 12px 6px;
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #422268;
|
|
||||||
background-color: #f9f7fc;
|
|
||||||
border-bottom: 2px solid #ddd8eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-calendar .rbc-today {
|
|
||||||
background-color: #f1eef9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-calendar .rbc-off-range-bg {
|
|
||||||
background-color: #fafafa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-calendar .rbc-event {
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 2px 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-calendar .rbc-event:hover {
|
|
||||||
opacity: 0.85;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-calendar .rbc-toolbar button {
|
|
||||||
color: #664fa3;
|
|
||||||
border-color: #ddd8eb;
|
|
||||||
font-family: 'Nunito Sans', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-calendar .rbc-toolbar button:hover {
|
|
||||||
background-color: #f1eef9;
|
|
||||||
border-color: #664fa3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-calendar .rbc-toolbar button.rbc-active {
|
|
||||||
background-color: #664fa3;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-calendar .rbc-month-view {
|
|
||||||
border: 1px solid #ddd8eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-calendar .rbc-day-bg {
|
|
||||||
border-color: #ddd8eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-calendar .rbc-date-cell {
|
|
||||||
padding: 8px;
|
|
||||||
font-family: 'Nunito Sans', sans-serif;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
<MemberFooter />
|
<MemberFooter />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ const MembersDirectory = () => {
|
|||||||
const [selectedMember, setSelectedMember] = useState(null);
|
const [selectedMember, setSelectedMember] = useState(null);
|
||||||
const [profileDialogOpen, setProfileDialogOpen] = useState(false);
|
const [profileDialogOpen, setProfileDialogOpen] = useState(false);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const pageSize = 12;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMembers();
|
fetchMembers();
|
||||||
@@ -33,6 +35,10 @@ const MembersDirectory = () => {
|
|||||||
filterMembers();
|
filterMembers();
|
||||||
}, [searchQuery, members]);
|
}, [searchQuery, members]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentPage(1);
|
||||||
|
}, [searchQuery, members]);
|
||||||
|
|
||||||
const fetchMembers = async () => {
|
const fetchMembers = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get('/members/directory');
|
const response = await api.get('/members/directory');
|
||||||
@@ -66,6 +72,14 @@ const MembersDirectory = () => {
|
|||||||
setFilteredMembers(filtered);
|
setFilteredMembers(filtered);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const totalPages = Math.max(1, Math.ceil(filteredMembers.length / pageSize));
|
||||||
|
|
||||||
|
const pageStart = (currentPage - 1) * pageSize;
|
||||||
|
|
||||||
|
const paginatedMembers = filteredMembers.slice(pageStart, pageStart + pageSize);
|
||||||
|
|
||||||
|
const totalMembers = members.length;
|
||||||
|
|
||||||
const getInitials = (firstName, lastName) => {
|
const getInitials = (firstName, lastName) => {
|
||||||
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
|
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
|
||||||
};
|
};
|
||||||
@@ -97,9 +111,15 @@ const MembersDirectory = () => {
|
|||||||
if (!dateString) return null;
|
if (!dateString) return null;
|
||||||
return new Date(dateString).toLocaleDateString('en-US', { month: 'long', day: 'numeric' });
|
return new Date(dateString).toLocaleDateString('en-US', { month: 'long', day: 'numeric' });
|
||||||
};
|
};
|
||||||
|
const Border = ({ yaxis = false }) => {
|
||||||
|
return (
|
||||||
|
yaxis ?
|
||||||
|
<div className=' border-2 w-full border-[#664FA3] my-24' />
|
||||||
|
: <div className=' border-2 w-full border-[#664FA3] mb-24' />
|
||||||
|
)
|
||||||
|
}
|
||||||
const MemberCard = ({ member }) => (
|
const MemberCard = ({ member }) => (
|
||||||
<Card className="p-6 bg-white rounded-2xl border border-[#ddd8eb] hover:shadow-lg transition-all h-full">
|
<Card className="p-6 bg-white rounded-3xl border border-[#ddd8eb] hover:shadow-lg transition-all h-full">
|
||||||
{/* Profile Photo */}
|
{/* Profile Photo */}
|
||||||
<div className="flex justify-center mb-4">
|
<div className="flex justify-center mb-4">
|
||||||
{member.profile_photo_url ? (
|
{member.profile_photo_url ? (
|
||||||
@@ -140,15 +160,17 @@ const MembersDirectory = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Member Since */}
|
{/* Member Since */}
|
||||||
|
{member.created_at && (
|
||||||
<div className="flex items-center justify-center gap-2 mb-4">
|
<div className="flex items-center justify-center gap-2 mb-4">
|
||||||
<Calendar className="h-4 w-4 text-[#664fa3]" />
|
<Calendar className="h-4 w-4 text-[#664fa3]" />
|
||||||
<span className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<span className="text-sm text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
Member since {new Date(member.member_since || member.created_at).toLocaleDateString('en-US', {
|
Member since {new Date(member.created_at).toLocaleDateString('en-US', {
|
||||||
month: 'long',
|
month: 'long',
|
||||||
year: 'numeric'
|
year: 'numeric'
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Contact Information */}
|
{/* Contact Information */}
|
||||||
<div className="space-y-3 mb-4">
|
<div className="space-y-3 mb-4">
|
||||||
@@ -257,30 +279,34 @@ const MembersDirectory = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-gradient-to-bl from-[#F9FAFB] to-[#DDD8EB]">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
||||||
<div className="max-w-7xl mx-auto px-6 py-12">
|
<div className="max-w-7xl mx-auto py-12">
|
||||||
|
|
||||||
|
{/* Header and Search bar */}
|
||||||
|
<div className='px-9'>
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="m-8 mt-14 flex flex-col sm:flex-row justify-between items-center ">
|
||||||
<h1 className="text-4xl md:text-5xl font-semibold text-[#422268] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h1 className="text-4xl md:text-5xl font-bold text-[#422268] mb-4" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||||
Members Directory
|
LOAF Members
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-lg text-[#664fa3]" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-lg " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
Connect with fellow LOAF members in our community.
|
<span className='text-foreground'>Number of current memebers in the directory: </span> <span className='text-[#664fa3] font-medium'>{totalMembers}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search Bar */}
|
{/* Search Bar */}
|
||||||
<div className="mb-8">
|
<div className="mb-24 mx-10">
|
||||||
<div className="relative max-w-xl">
|
<div className="relative w-full ">
|
||||||
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-[#664fa3]" />
|
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-[#664fa3]" />
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search by name or bio..."
|
placeholder="Search by name or bio..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="pl-12 pr-4 py-6 text-lg border-[#ddd8eb] rounded-xl focus:border-[#664fa3] focus:ring-[#664fa3]"
|
className="pl-12 pr-4 py-6 text-3xl font-medium bg-background border-foreground rounded-full focus:border-[#664fa3] focus:ring-[#664fa3]"
|
||||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -291,6 +317,11 @@ const MembersDirectory = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{/* Border Decoration */}
|
||||||
|
|
||||||
|
<Border />
|
||||||
|
|
||||||
{/* Members Grid */}
|
{/* Members Grid */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="text-center py-20">
|
<div className="text-center py-20">
|
||||||
@@ -298,7 +329,7 @@ const MembersDirectory = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : filteredMembers.length > 0 ? (
|
) : filteredMembers.length > 0 ? (
|
||||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
{filteredMembers.map((member) => (
|
{paginatedMembers.map((member) => (
|
||||||
<MemberCard key={member.id} member={member} />
|
<MemberCard key={member.id} member={member} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -316,6 +347,11 @@ const MembersDirectory = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{/* Border Decoration */}
|
||||||
|
<Border yaxis="true" />
|
||||||
|
|
||||||
{/* Info Card */}
|
{/* Info Card */}
|
||||||
{!loading && members.length > 0 && (
|
{!loading && members.length > 0 && (
|
||||||
<Card className="mt-12 p-6 bg-[#F8F7FB] border-[#ddd8eb]">
|
<Card className="mt-12 p-6 bg-[#F8F7FB] border-[#ddd8eb]">
|
||||||
@@ -524,6 +560,66 @@ const MembersDirectory = () => {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
{!loading && filteredMembers.length > 0 && (
|
||||||
|
<div className="mt-10 flex flex-col items-center gap-4 pb-12">
|
||||||
|
<p className="text-sm text-[#664fa3]" 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-[#DDD8EB] rounded-full text-[#422268] hover:bg-[#664fa3] hover:text-white"
|
||||||
|
>
|
||||||
|
First Page
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => setCurrentPage((page) => Math.max(1, page - 1))}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className="bg-[#DDD8EB] rounded-full text-[#422268] hover:bg-[#664fa3] hover:text-white"
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{Array.from({ length: totalPages }, (_, index) => {
|
||||||
|
const pageNumber = index + 1;
|
||||||
|
const isActive = pageNumber === currentPage;
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={pageNumber}
|
||||||
|
onClick={() => setCurrentPage(pageNumber)}
|
||||||
|
className={
|
||||||
|
isActive
|
||||||
|
? "bg-[#664fa3] text-white hover:bg-[#422268] rounded-full"
|
||||||
|
: "bg-[#DDD8EB] text-[#422268] hover:bg-[#664fa3] hover:text-white rounded-full"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{pageNumber}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => setCurrentPage((page) => Math.min(totalPages, page + 1))}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className="bg-[#DDD8EB] text-[#422268] hover:bg-[#664fa3] rounded-full hover:text-white"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => setCurrentPage(totalPages)}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className="bg-[#DDD8EB] text-[#422268] hover:bg-[#664fa3] rounded-full hover:text-white"
|
||||||
|
>
|
||||||
|
Last Page
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<MemberFooter />
|
<MemberFooter />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,14 +4,60 @@ const API_URL = process.env.REACT_APP_BACKEND_URL;
|
|||||||
|
|
||||||
export const api = axios.create({
|
export const api = axios.create({
|
||||||
baseURL: `${API_URL}/api`,
|
baseURL: `${API_URL}/api`,
|
||||||
|
timeout: 30000, // 30 second timeout for all requests
|
||||||
});
|
});
|
||||||
|
|
||||||
api.interceptors.request.use((config) => {
|
// Request interceptor - add auth token
|
||||||
|
api.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('[API] Request error:', error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Response interceptor - handle errors and retries
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
async (error) => {
|
||||||
|
const config = error.config;
|
||||||
|
|
||||||
|
// Don't retry if we've already retried or if it's a client error (4xx)
|
||||||
|
if (!config || config.__isRetry || (error.response && error.response.status < 500)) {
|
||||||
|
console.error('[API] Request failed:', {
|
||||||
|
url: config?.url,
|
||||||
|
method: config?.method,
|
||||||
|
status: error.response?.status,
|
||||||
|
message: error.message,
|
||||||
|
data: error.response?.data
|
||||||
|
});
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as retry to prevent infinite loops
|
||||||
|
config.__isRetry = true;
|
||||||
|
|
||||||
|
// Retry after 1 second for server errors or network issues
|
||||||
|
console.warn('[API] Retrying request after 1s:', {
|
||||||
|
url: config.url,
|
||||||
|
method: config.method,
|
||||||
|
error: error.message
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(api.request(config));
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
@@ -78,5 +78,8 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [require("tailwindcss-animate")],
|
plugins: [
|
||||||
|
require("tailwindcss-animate"),
|
||||||
|
require("@tailwindcss/typography")
|
||||||
|
],
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user