Update New Features
This commit is contained in:
216
src/components/AddToCalendarButton.js
Normal file
216
src/components/AddToCalendarButton.js
Normal file
@@ -0,0 +1,216 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Calendar, ChevronDown, Download, RefreshCw } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from './ui/dropdown-menu';
|
||||
|
||||
/**
|
||||
* AddToCalendarButton Component
|
||||
* Universal calendar export button with support for:
|
||||
* - Google Calendar (web link)
|
||||
* - Microsoft Outlook (web link)
|
||||
* - Apple Calendar / Outlook Desktop (webcal:// subscription)
|
||||
* - Download .ics file (universal import)
|
||||
* - Subscribe to personal feed (auto-sync)
|
||||
*/
|
||||
export default function AddToCalendarButton({
|
||||
event = null, // Single event object { id, title, description, start_at, end_at, location }
|
||||
showSubscribe = false, // Show "Subscribe to My Events" option
|
||||
variant = "default", // Button variant: default, outline, ghost
|
||||
size = "default" // Button size: default, sm, lg
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const backendUrl = process.env.REACT_APP_BACKEND_URL || 'http://localhost:8000';
|
||||
|
||||
// Format datetime for Google Calendar (YYYYMMDDTHHMMSSZ)
|
||||
const formatGoogleDate = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
|
||||
};
|
||||
|
||||
// Generate Google Calendar URL
|
||||
const getGoogleCalendarUrl = () => {
|
||||
if (!event) return null;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
action: 'TEMPLATE',
|
||||
text: event.title,
|
||||
dates: `${formatGoogleDate(event.start_at)}/${formatGoogleDate(event.end_at)}`,
|
||||
details: event.description || '',
|
||||
location: event.location || '',
|
||||
});
|
||||
|
||||
return `https://calendar.google.com/calendar/render?${params.toString()}`;
|
||||
};
|
||||
|
||||
// Generate Microsoft Outlook Web URL
|
||||
const getOutlookWebUrl = () => {
|
||||
if (!event) return null;
|
||||
|
||||
const startDate = new Date(event.start_at).toISOString();
|
||||
const endDate = new Date(event.end_at).toISOString();
|
||||
|
||||
const params = new URLSearchParams({
|
||||
path: '/calendar/action/compose',
|
||||
rru: 'addevent',
|
||||
subject: event.title,
|
||||
startdt: startDate,
|
||||
enddt: endDate,
|
||||
body: event.description || '',
|
||||
location: event.location || '',
|
||||
});
|
||||
|
||||
return `https://outlook.live.com/calendar/0/deeplink/compose?${params.toString()}`;
|
||||
};
|
||||
|
||||
// Get .ics download URL
|
||||
const getIcsDownloadUrl = () => {
|
||||
if (!event) return null;
|
||||
return `${backendUrl}/api/events/${event.id}/download.ics`;
|
||||
};
|
||||
|
||||
// Get webcal:// subscription URL (for Apple Calendar / Outlook Desktop)
|
||||
const getWebcalUrl = () => {
|
||||
// Convert http:// to webcal://
|
||||
const webcalUrl = backendUrl.replace(/^https?:\/\//, 'webcal://');
|
||||
return `${webcalUrl}/api/calendars/subscribe.ics`;
|
||||
};
|
||||
|
||||
// Get all events download URL
|
||||
const getAllEventsUrl = () => {
|
||||
return `${backendUrl}/api/calendars/all-events.ics`;
|
||||
};
|
||||
|
||||
// Handle calendar action
|
||||
const handleCalendarAction = (action) => {
|
||||
setIsOpen(false);
|
||||
|
||||
switch (action) {
|
||||
case 'google':
|
||||
window.open(getGoogleCalendarUrl(), '_blank');
|
||||
break;
|
||||
case 'outlook':
|
||||
window.open(getOutlookWebUrl(), '_blank');
|
||||
break;
|
||||
case 'apple':
|
||||
window.location.href = getWebcalUrl();
|
||||
break;
|
||||
case 'download':
|
||||
window.open(getIcsDownloadUrl(), '_blank');
|
||||
break;
|
||||
case 'subscribe':
|
||||
window.location.href = getWebcalUrl();
|
||||
break;
|
||||
case 'all-events':
|
||||
window.open(getAllEventsUrl(), '_blank');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant={variant} size={size} className="gap-2">
|
||||
<Calendar className="h-4 w-4" />
|
||||
Add to Calendar
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="end" className="w-64">
|
||||
{event && (
|
||||
<>
|
||||
{/* Single Event Export Options */}
|
||||
<div className="px-2 py-1.5 text-sm font-semibold text-[#422268]">
|
||||
Add This Event
|
||||
</div>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleCalendarAction('google')}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<svg className="h-4 w-4 mr-2" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
|
||||
</svg>
|
||||
Google Calendar
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleCalendarAction('outlook')}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<svg className="h-4 w-4 mr-2" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M7 2h14v20H7V2zm7 11c0 2.761-2.239 5-5 5H2c-.552 0-1-.448-1-1s.448-1 1-1h7c1.657 0 3-1.343 3-3V9c0-1.657-1.343-3-3-3H2c-.552 0-1-.448-1-1s.448-1 1-1h7c2.761 0 5 2.239 5 5v4z"/>
|
||||
</svg>
|
||||
Outlook Web
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleCalendarAction('apple')}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<svg className="h-4 w-4 mr-2" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"/>
|
||||
</svg>
|
||||
Apple Calendar
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleCalendarAction('download')}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Download .ics File
|
||||
</DropdownMenuItem>
|
||||
|
||||
{showSubscribe && <DropdownMenuSeparator />}
|
||||
</>
|
||||
)}
|
||||
|
||||
{showSubscribe && (
|
||||
<>
|
||||
{/* Subscription Options */}
|
||||
<div className="px-2 py-1.5 text-sm font-semibold text-[#422268]">
|
||||
Calendar Feeds
|
||||
</div>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleCalendarAction('subscribe')}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
Subscribe to My Events
|
||||
<div className="text-xs text-[#664fa3] mt-0.5">
|
||||
Auto-syncs your RSVP'd events
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleCalendarAction('all-events')}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Download All Events
|
||||
<div className="text-xs text-[#664fa3] mt-0.5">
|
||||
One-time import of all upcoming events
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!event && !showSubscribe && (
|
||||
<div className="px-2 py-6 text-center text-sm text-[#664fa3]">
|
||||
No event selected
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -15,7 +15,12 @@ import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
LogOut,
|
||||
Menu
|
||||
Menu,
|
||||
Image,
|
||||
FileText,
|
||||
DollarSign,
|
||||
Scale,
|
||||
HardDrive
|
||||
} from 'lucide-react';
|
||||
|
||||
const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
||||
@@ -23,6 +28,9 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
||||
const navigate = useNavigate();
|
||||
const { user, logout } = useAuth();
|
||||
const [pendingCount, setPendingCount] = useState(0);
|
||||
const [storageUsed, setStorageUsed] = useState(0);
|
||||
const [storageLimit, setStorageLimit] = useState(0);
|
||||
const [storagePercentage, setStoragePercentage] = useState(0);
|
||||
|
||||
// Fetch pending approvals count
|
||||
useEffect(() => {
|
||||
@@ -44,6 +52,33 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Fetch storage usage
|
||||
useEffect(() => {
|
||||
const fetchStorageUsage = async () => {
|
||||
try {
|
||||
const response = await api.get('/admin/storage/usage');
|
||||
setStorageUsed(response.data.total_bytes_used);
|
||||
setStorageLimit(response.data.max_bytes_allowed);
|
||||
setStoragePercentage(response.data.percentage);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch storage usage:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchStorageUsage();
|
||||
// Refresh storage usage every 60 seconds
|
||||
const interval = setInterval(fetchStorageUsage, 60000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const formatBytes = (bytes) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/login');
|
||||
@@ -85,7 +120,31 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
||||
name: 'Events',
|
||||
icon: Calendar,
|
||||
path: '/admin/events',
|
||||
disabled: true
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
name: 'Gallery',
|
||||
icon: Image,
|
||||
path: '/admin/gallery',
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
name: 'Newsletters',
|
||||
icon: FileText,
|
||||
path: '/admin/newsletters',
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
name: 'Financials',
|
||||
icon: DollarSign,
|
||||
path: '/admin/financials',
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
name: 'Bylaws',
|
||||
icon: Scale,
|
||||
path: '/admin/bylaws',
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
name: 'Roles',
|
||||
@@ -233,6 +292,48 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Storage Usage Widget */}
|
||||
<div className="mb-2">
|
||||
{isOpen ? (
|
||||
<div className="px-4 py-3 bg-[#F8F7FB] rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-[#422268]">Storage Usage</span>
|
||||
<span className="text-xs text-[#664fa3]">{storagePercentage}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-[#ddd8eb] rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all ${
|
||||
storagePercentage > 90 ? 'bg-red-500' :
|
||||
storagePercentage > 75 ? 'bg-yellow-500' :
|
||||
'bg-[#81B29A]'
|
||||
}`}
|
||||
style={{ width: `${storagePercentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-[#664fa3] mt-1">
|
||||
{formatBytes(storageUsed)} / {formatBytes(storageLimit)}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex justify-center">
|
||||
<div className="relative group">
|
||||
<HardDrive className={`h-5 w-5 ${
|
||||
storagePercentage > 90 ? 'text-red-500' :
|
||||
storagePercentage > 75 ? 'text-yellow-500' :
|
||||
'text-[#664fa3]'
|
||||
}`} />
|
||||
{storagePercentage > 75 && (
|
||||
<div className="absolute -top-1 -right-1 bg-red-500 h-2 w-2 rounded-full" />
|
||||
)}
|
||||
{/* Tooltip */}
|
||||
<div className="absolute left-full ml-2 top-1/2 -translate-y-1/2 px-3 py-2 bg-[#422268] text-white text-sm rounded-lg opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-50">
|
||||
Storage: {storagePercentage}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Logout Button */}
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
|
||||
122
src/components/MemberFooter.js
Normal file
122
src/components/MemberFooter.js
Normal file
@@ -0,0 +1,122 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Calendar, Users, User, BookOpen, FileText, DollarSign, Scale } from 'lucide-react';
|
||||
|
||||
const MemberFooter = () => {
|
||||
return (
|
||||
<footer className="bg-[#422268] text-white mt-auto">
|
||||
<div className="max-w-7xl mx-auto px-6 py-12">
|
||||
<div className="grid md:grid-cols-4 gap-8">
|
||||
{/* Logo & About */}
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
LOAF
|
||||
</h3>
|
||||
<p className="text-gray-300 text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
Lesbian Organization of Atlanta Family
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Member Resources */}
|
||||
<div>
|
||||
<h4 className="font-semibold mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Member Resources
|
||||
</h4>
|
||||
<ul className="space-y-2 text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<li>
|
||||
<Link to="/members/calendar" className="text-gray-300 hover:text-white flex items-center gap-2 transition-colors">
|
||||
<Calendar className="h-4 w-4" />
|
||||
Event Calendar
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/members/directory" className="text-gray-300 hover:text-white flex items-center gap-2 transition-colors">
|
||||
<Users className="h-4 w-4" />
|
||||
Members Directory
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/members/profile" className="text-gray-300 hover:text-white flex items-center gap-2 transition-colors">
|
||||
<User className="h-4 w-4" />
|
||||
My Profile
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/events" className="text-gray-300 hover:text-white flex items-center gap-2 transition-colors">
|
||||
<BookOpen className="h-4 w-4" />
|
||||
Events
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Documents */}
|
||||
<div>
|
||||
<h4 className="font-semibold mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Documents
|
||||
</h4>
|
||||
<ul className="space-y-2 text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<li>
|
||||
<Link to="/members/newsletters" className="text-gray-300 hover:text-white flex items-center gap-2 transition-colors">
|
||||
<FileText className="h-4 w-4" />
|
||||
Newsletters
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/members/financials" className="text-gray-300 hover:text-white flex items-center gap-2 transition-colors">
|
||||
<DollarSign className="h-4 w-4" />
|
||||
Financial Reports
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/members/bylaws" className="text-gray-300 hover:text-white flex items-center gap-2 transition-colors">
|
||||
<Scale className="h-4 w-4" />
|
||||
Bylaws
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Support */}
|
||||
<div>
|
||||
<h4 className="font-semibold mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Support
|
||||
</h4>
|
||||
<ul className="space-y-2 text-sm" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<li>
|
||||
<Link to="/profile" className="text-gray-300 hover:text-white transition-colors">
|
||||
Account Settings
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/#contact" className="text-gray-300 hover:text-white transition-colors">
|
||||
Contact Us
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/#donate" className="text-gray-300 hover:text-white transition-colors">
|
||||
Donate
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Bar */}
|
||||
<div className="border-t border-[#664fa3]">
|
||||
<div className="max-w-7xl mx-auto px-6 py-4">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center gap-4 text-sm text-gray-400" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<div className="flex gap-6">
|
||||
<a href="/#terms" className="hover:text-white transition-colors">Terms of Service</a>
|
||||
<a href="/#privacy" className="hover:text-white transition-colors">Privacy Policy</a>
|
||||
</div>
|
||||
<p>© 2025 LOAF. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemberFooter;
|
||||
@@ -2,93 +2,124 @@ import React from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { Button } from './ui/button';
|
||||
import { Users, LogOut, LayoutDashboard } from 'lucide-react';
|
||||
|
||||
const Navbar = () => {
|
||||
const { user, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// LOAF logo (local)
|
||||
const loafLogo = `${process.env.PUBLIC_URL}/loaf-logo.png`;
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="bg-white border-b border-[#ddd8eb] sticky top-0 z-50 backdrop-blur-sm bg-white/90">
|
||||
<div className="max-w-7xl mx-auto px-6 py-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<Link to={user ? "/dashboard" : "/"} className="flex items-center gap-2">
|
||||
<Users className="h-8 w-8 text-[#ff9e77]" strokeWidth={1.5} />
|
||||
<span className="text-2xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>Membership</span>
|
||||
<>
|
||||
{/* Top Header - Member Actions */}
|
||||
<header className="bg-gradient-to-r from-[#644c9f] to-[#48286e] px-16 py-4 flex justify-end items-center gap-6">
|
||||
{user && (
|
||||
<span className="text-white text-base font-medium" style={{ fontFamily: "'Poppins', sans-serif" }}>
|
||||
Welcome, {user.first_name}
|
||||
</span>
|
||||
)}
|
||||
{user?.role === 'admin' && (
|
||||
<Link to="/admin">
|
||||
<button
|
||||
className="text-white text-base font-medium hover:opacity-80 transition-opacity bg-transparent border-none cursor-pointer"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
data-testid="admin-nav-button"
|
||||
>
|
||||
Admin Panel
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{user ? (
|
||||
<>
|
||||
{user.role === 'admin' && (
|
||||
<Link to="/admin">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-[#422268] hover:text-[#ff9e77] hover:bg-[#DDD8EB]/10"
|
||||
data-testid="admin-nav-button"
|
||||
>
|
||||
<LayoutDashboard className="h-4 w-4 mr-2" />
|
||||
Admin
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
<Link to="/events">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-[#422268] hover:text-[#ff9e77] hover:bg-[#DDD8EB]/10"
|
||||
data-testid="events-nav-button"
|
||||
>
|
||||
Events
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/profile">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-[#422268] hover:text-[#ff9e77] hover:bg-[#DDD8EB]/10"
|
||||
data-testid="profile-nav-button"
|
||||
>
|
||||
Profile
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
onClick={handleLogout}
|
||||
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-6"
|
||||
data-testid="logout-button"
|
||||
>
|
||||
<LogOut className="h-4 w-4 mr-2" />
|
||||
Logout
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link to="/login">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-[#422268] hover:text-[#ff9e77] hover:bg-[#DDD8EB]/10"
|
||||
data-testid="login-nav-button"
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/register">
|
||||
<Button
|
||||
className="bg-[#DDD8EB] text-[#422268] hover:bg-white rounded-full px-6"
|
||||
data-testid="register-nav-button"
|
||||
>
|
||||
Join Us
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
)}
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="text-white text-base font-medium hover:opacity-80 transition-opacity bg-transparent border-none cursor-pointer"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
data-testid="logout-button"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
<Link to="/#donate">
|
||||
<Button
|
||||
className="bg-[#ff9e77] hover:bg-[#ff8c64] text-[#48286e] rounded-[25px] px-[54px] py-[10px] text-[16.5px] font-semibold h-[41px]"
|
||||
style={{ fontFamily: "'Montserrat', sans-serif" }}
|
||||
>
|
||||
Donate
|
||||
</Button>
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
{/* Main Header - Member Navigation */}
|
||||
<header className="bg-[#664fa3] px-16 py-2 flex justify-between items-center">
|
||||
<Link to="/dashboard">
|
||||
<img src={loafLogo} alt="LOAF Logo" className="h-28 w-28 object-contain" />
|
||||
</Link>
|
||||
<nav className="flex gap-10 items-center">
|
||||
<Link
|
||||
to="/"
|
||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
<Link
|
||||
to="/dashboard"
|
||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
to="/events"
|
||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
data-testid="events-nav-button"
|
||||
>
|
||||
Events
|
||||
</Link>
|
||||
<Link
|
||||
to="/members/calendar"
|
||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
Calendar
|
||||
</Link>
|
||||
<Link
|
||||
to="/members/directory"
|
||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
Directory
|
||||
</Link>
|
||||
<Link
|
||||
to="/members/gallery"
|
||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
Gallery
|
||||
</Link>
|
||||
<Link
|
||||
to="/members/newsletters"
|
||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
Documents
|
||||
</Link>
|
||||
<Link
|
||||
to="/profile"
|
||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
data-testid="profile-nav-button"
|
||||
>
|
||||
Profile
|
||||
</Link>
|
||||
</nav>
|
||||
</header>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
71
src/components/PublicFooter.js
Normal file
71
src/components/PublicFooter.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
const PublicFooter = () => {
|
||||
const loafLogo = `${process.env.PUBLIC_URL}/loaf-logo.png`;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Main Footer */}
|
||||
<footer className="bg-[#644c9f] px-16 py-0 flex items-center justify-center h-[420px]">
|
||||
<div className="border-t border-[rgba(0,0,0,0.1)] py-20 flex gap-30 items-center justify-center flex-1">
|
||||
<div className="w-[232px]">
|
||||
<img src={loafLogo} alt="LOAF Logo" className="w-[232px] h-[232px] object-contain" />
|
||||
</div>
|
||||
<nav className="flex gap-28 items-start justify-center w-[811px]">
|
||||
<div className="flex flex-col gap-2 w-[163px]">
|
||||
<div className="pb-4">
|
||||
<p className="text-white text-base font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>About</p>
|
||||
</div>
|
||||
<a href="/#history" className="text-[#ddd8eb] text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Inter', sans-serif" }}>History</a>
|
||||
<a href="/#mission" className="text-[#ddd8eb] text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Inter', sans-serif" }}>Mission and Values</a>
|
||||
<a href="/#board" className="text-[#ddd8eb] text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Inter', sans-serif" }}>Board of Directors</a>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 w-[148px]">
|
||||
<div className="pb-4">
|
||||
<p className="text-white text-base font-semibold" style={{ fontFamily: "'Inter', sans-serif" }}>Connect</p>
|
||||
</div>
|
||||
<Link to="/become-a-member" className="text-[#ddd8eb] text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Inter', sans-serif" }}>Become a Member</Link>
|
||||
<a href="/#contact" className="text-[#ddd8eb] text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Inter', sans-serif" }}>Contact Us</a>
|
||||
<a href="/#resources" className="text-[#ddd8eb] text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Inter', sans-serif" }}>Resources</a>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 items-center w-[271px]">
|
||||
<div className="pb-4 w-full">
|
||||
<Button className="bg-[#ff9e77] hover:bg-[#ff8c64] text-[#48286e] rounded-full px-6 py-3 text-lg font-medium w-[217px]">
|
||||
Donate
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[#ddd8eb] text-base font-medium text-center w-full" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
LOAF is supported by<br />the Hollyfield Foundation
|
||||
</p>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{/* Bottom Footer */}
|
||||
<footer className="bg-gradient-to-r from-[#48286e] to-[#644c9f] border-t border-[rgba(0,0,0,0.1)] px-16 py-6 flex justify-between items-center h-[76px]">
|
||||
<nav className="flex gap-8 items-center">
|
||||
<a href="/#terms" className="text-[#c5b4e3] text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Terms of Service
|
||||
</a>
|
||||
<a href="/#privacy" className="text-[#c5b4e3] text-base font-medium hover:text-white transition-colors" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Privacy Policy
|
||||
</a>
|
||||
</nav>
|
||||
<p className="text-[#c5b4e3] text-base font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
© 2025 LOAF. All Rights Reserved.
|
||||
</p>
|
||||
<p className="text-[#c5b4e3] text-base font-medium" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Designed and Managed by{' '}
|
||||
<a href="https://konceptkit.com/" className="text-[#d1c3e9] underline hover:text-white transition-colors">
|
||||
Koncept Kit
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublicFooter;
|
||||
108
src/components/PublicNavbar.js
Normal file
108
src/components/PublicNavbar.js
Normal file
@@ -0,0 +1,108 @@
|
||||
import React from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Button } from './ui/button';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
const PublicNavbar = () => {
|
||||
const { user, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// LOAF logo (local)
|
||||
const loafLogo = `${process.env.PUBLIC_URL}/loaf-logo.png`;
|
||||
|
||||
const handleAuthAction = () => {
|
||||
if (user) {
|
||||
logout();
|
||||
navigate('/');
|
||||
} else {
|
||||
navigate('/login');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Top Header - Auth Actions */}
|
||||
<header className="bg-gradient-to-r from-[#644c9f] to-[#48286e] px-16 py-4 flex justify-end items-center gap-6">
|
||||
<button
|
||||
onClick={handleAuthAction}
|
||||
className="text-white text-base font-medium hover:opacity-80 transition-opacity bg-transparent border-none cursor-pointer"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
{user ? 'Logout' : 'Login'}
|
||||
</button>
|
||||
{!user && (
|
||||
<Link
|
||||
to="/register"
|
||||
className="text-white text-base font-medium hover:opacity-80 transition-opacity"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
Register
|
||||
</Link>
|
||||
)}
|
||||
<Link to="/#donate">
|
||||
<Button
|
||||
className="bg-[#ff9e77] hover:bg-[#ff8c64] text-[#48286e] rounded-[25px] px-[54px] py-[10px] text-[16.5px] font-semibold h-[41px]"
|
||||
style={{ fontFamily: "'Montserrat', sans-serif" }}
|
||||
>
|
||||
Donate
|
||||
</Button>
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
{/* Main Header - Navigation */}
|
||||
<header className="bg-[#664fa3] px-16 py-2 flex justify-between items-center">
|
||||
<Link to="/">
|
||||
<img src={loafLogo} alt="LOAF Logo" className="h-28 w-28 object-contain" />
|
||||
</Link>
|
||||
<nav className="flex gap-10 items-center">
|
||||
<Link
|
||||
to="/#welcome"
|
||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
Welcome
|
||||
</Link>
|
||||
<Link
|
||||
to="/#about"
|
||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
About Us
|
||||
</Link>
|
||||
<Link
|
||||
to={user ? "/dashboard" : "/become-a-member"}
|
||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
{user ? 'Dashboard' : 'Become a Member'}
|
||||
</Link>
|
||||
{!user && (
|
||||
<Link
|
||||
to="/login"
|
||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
Members Only
|
||||
</Link>
|
||||
)}
|
||||
<Link
|
||||
to="/#resources"
|
||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
Resources
|
||||
</Link>
|
||||
<Link
|
||||
to="/#contact"
|
||||
className="text-white text-[17.5px] font-medium hover:opacity-80 transition-opacity"
|
||||
style={{ fontFamily: "'Poppins', sans-serif" }}
|
||||
>
|
||||
Contact Us
|
||||
</Link>
|
||||
</nav>
|
||||
</header>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublicNavbar;
|
||||
Reference in New Issue
Block a user