theme-provider #20

Merged
andika merged 12 commits from theme-provider into dev 2026-01-27 08:41:49 +00:00
3 changed files with 330 additions and 107 deletions
Showing only changes of commit 333ce62710 - Show all commits

View File

@@ -350,44 +350,83 @@ const AdminRoles = () => {
Select permissions for this role. You can also add permissions later.
</p>
<div className="border rounded-lg p-4 max-h-64 overflow-y-auto scrollbar-dashboard">
{Object.entries(groupedPermissions).map(([module, perms]) => (
<div key={module} className="mb-4">
<button
onClick={() => toggleModule(module)}
className="flex items-center w-full text-left font-medium mb-2 hover:text-blue-600"
>
{expandedModules[module] ? (
<ChevronUp className="w-4 h-4 mr-1" />
) : (
<ChevronDown className="w-4 h-4 mr-1" />
)}
{module.charAt(0).toUpperCase() + module.slice(1)} ({perms.length})
</button>
{expandedModules[module] && (
<div className="space-y-2 ml-5">
{perms.map(perm => (
<div key={perm.code} className="flex items-center">
<Checkbox
checked={formData.permissions.includes(perm.code)}
onCheckedChange={() => {
setFormData(prev => ({
...prev,
permissions: prev.permissions.includes(perm.code)
? prev.permissions.filter(p => p !== perm.code)
: [...prev.permissions, perm.code]
}));
}}
/>
<label className="ml-2 text-sm">
<span className="font-medium">{perm.name}</span>
<span className="text-gray-500 ml-2">({perm.code})</span>
</label>
</div>
))}
{Object.entries(groupedPermissions).map(([module, perms]) => {
const moduleCodes = perms.map(perm => perm.code);
const selectedCount = moduleCodes.filter(code => formData.permissions.includes(code)).length;
const hasPermissions = moduleCodes.length > 0;
const isAllSelected = hasPermissions && selectedCount === moduleCodes.length;
const isNoneSelected = selectedCount === 0;
return (
<div key={module} className="mb-4">
<div className="flex items-center justify-between mb-2">
<button
type="button"
onClick={() => toggleModule(module)}
className="flex items-center text-left font-medium hover:text-blue-600"
>
{expandedModules[module] ? (
<ChevronUp className="w-4 h-4 mr-1" />
) : (
<ChevronDown className="w-4 h-4 mr-1" />
)}
{module.charAt(0).toUpperCase() + module.slice(1)} ({perms.length})
</button>
<div className="flex items-center gap-3">
<button
type="button"
onClick={() => {
setFormData(prev => ({
...prev,
permissions: [...new Set([...prev.permissions, ...moduleCodes])]
}));
}}
disabled={!hasPermissions || isAllSelected}
className="text-xs font-medium text-gray-500 hover:text-brand-purple disabled:opacity-50 disabled:cursor-not-allowed"
>
Select all
</button>
<button
type="button"
onClick={() => {
setFormData(prev => ({
...prev,
permissions: prev.permissions.filter(code => !moduleCodes.includes(code))
}));
}}
disabled={!hasPermissions || isNoneSelected}
className="text-xs font-medium text-gray-500 hover:text-brand-purple disabled:opacity-50 disabled:cursor-not-allowed"
>
Deselect all
</button>
</div>
</div>
)}
</div>
))}
{expandedModules[module] && (
<div className="space-y-2 ml-5">
{perms.map(perm => (
<div key={perm.code} className="flex items-center">
<Checkbox
checked={formData.permissions.includes(perm.code)}
onCheckedChange={() => {
setFormData(prev => ({
...prev,
permissions: prev.permissions.includes(perm.code)
? prev.permissions.filter(p => p !== perm.code)
: [...prev.permissions, perm.code]
}));
}}
/>
<label className="ml-2 text-sm">
<span className="font-medium">{perm.name}</span>
<span className="text-gray-500 ml-2">({perm.code})</span>
</label>
</div>
))}
</div>
)}
</div>
);
})}
</div>
</div>
</div>

View File

@@ -5,20 +5,34 @@ import MemberFooter from '../../components/MemberFooter';
import { Card } from '../../components/ui/card';
import { Button } from '../../components/ui/button';
import { Badge } from '../../components/ui/badge';
import { Input } from '../../components/ui/input';
import { toast } from 'sonner';
import { Scale, ExternalLink, History, Check } from 'lucide-react';
import { Scale, ExternalLink, History, Check, Search, Calendar } from 'lucide-react';
export default function Bylaws() {
const [currentBylaws, setCurrentBylaws] = useState(null);
const [history, setHistory] = useState([]);
const [years, setYears] = useState([]);
const [selectedYear, setSelectedYear] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
const [showHistory, setShowHistory] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchCurrentBylaws();
fetchYears();
fetchHistory();
}, []);
const fetchYears = async () => {
try {
const response = await api.get('/bylaws/years');
setYears(response.data);
} catch (error) {
console.error('Failed to load years');
}
};
const fetchCurrentBylaws = async () => {
try {
const response = await api.get('/bylaws/current');
@@ -32,15 +46,46 @@ export default function Bylaws() {
}
};
const fetchHistory = async () => {
const fetchHistory = async (year = null) => {
try {
const response = await api.get('/bylaws/history');
const url = year ? `/bylaws/history?year=${year}` : '/bylaws/history';
const response = await api.get(url);
setHistory(response.data);
} catch (error) {
console.error('Failed to load bylaws history');
}
};
const handleYearFilter = (year) => {
setSelectedYear(year);
fetchHistory(year);
};
const clearFilter = () => {
setSelectedYear(null);
fetchHistory();
};
const filteredHistory = history.filter(bylaws =>
bylaws.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
(bylaws.version && bylaws.version.toLowerCase().includes(searchTerm.toLowerCase()))
);
const groupByYear = (items) => {
const grouped = {};
items.forEach(item => {
const year = new Date(item.effective_date).getFullYear();
if (!grouped[year]) {
grouped[year] = [];
}
grouped[year].push(item);
});
return grouped;
};
const groupedHistory = groupByYear(filteredHistory.filter(b => !b.is_current));
const sortedYears = Object.keys(groupedHistory).sort((a, b) => b - a);
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
@@ -75,6 +120,44 @@ export default function Bylaws() {
<p className="text-brand-purple mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Review the official governing bylaws and policies of the LOAF community.
</p>
{/* Filters */}
<div className="flex gap-4 flex-wrap items-center">
{/* Search */}
<div className="relative flex-1 min-w-[300px]">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-brand-purple " />
<Input
type="text"
placeholder="Search bylaws..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 border-[var(--neutral-800)] focus:border-brand-purple "
/>
</div>
{/* Year Filter */}
<div className="flex gap-2 flex-wrap">
<Button
onClick={clearFilter}
variant={selectedYear === null ? "default" : "outline"}
size="sm"
className={selectedYear === null ? "bg-brand-purple hover:bg-[var(--purple-muted)] text-white" : "border-brand-lavender text-brand-lavender "}
>
All Years
</Button>
{years.map(year => (
<Button
key={year}
onClick={() => handleYearFilter(year)}
variant={selectedYear === year ? "default" : "outline"}
size="sm"
className={selectedYear === year ? "bg-brand-purple text-white" : "border-brand-purple text-brand-purple "}
>
{year}
</Button>
))}
</div>
</div>
</div>
{/* Current Bylaws */}
@@ -124,7 +207,7 @@ export default function Bylaws() {
)}
{/* Version History Toggle */}
{history.length > 1 && (
{filteredHistory.filter(b => !b.is_current).length > 0 && (
<div className="mb-6">
<Button
onClick={() => setShowHistory(!showHistory)}
@@ -132,41 +215,48 @@ export default function Bylaws() {
className="w-full border-[var(--neutral-800)] text-brand-purple hover:bg-[var(--lavender-300)] rounded-full flex items-center justify-center gap-2"
>
<History className="h-4 w-4" />
{showHistory ? 'Hide' : 'View'} Version History ({history.length - 1} previous {history.length - 1 === 1 ? 'version' : 'versions'})
{showHistory ? 'Hide' : 'View'} Version History ({filteredHistory.filter(b => !b.is_current).length} previous {filteredHistory.filter(b => !b.is_current).length === 1 ? 'version' : 'versions'})
</Button>
</div>
)}
{/* Version History */}
{showHistory && history.length > 1 && (
<div className="space-y-4">
<h3 className="text-xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
Previous Versions
</h3>
{history.filter(b => !b.is_current).map(bylaws => (
<Card key={bylaws.id} className="p-6 bg-[var(--lavender-600)] rounded-xl border border-[var(--neutral-800)]">
<div className="flex items-center justify-between">
<div>
<h4 className="text-lg font-semibold text-[var(--purple-ink)] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
{bylaws.title}
</h4>
<div className="flex items-center gap-3 text-sm text-brand-purple ">
<span>Version {bylaws.version}</span>
<span></span>
<span>Effective {formatDate(bylaws.effective_date)}</span>
</div>
</div>
<Button
onClick={() => window.open(bylaws.document_url, '_blank')}
variant="outline"
size="sm"
className="border-brand-purple text-brand-purple hover:bg-[var(--lavender-300)] rounded-full flex items-center gap-2"
>
<ExternalLink className="h-4 w-4" />
View
</Button>
{showHistory && filteredHistory.filter(b => !b.is_current).length > 0 && (
<div className="space-y-6">
{sortedYears.map(year => (
<div key={year}>
<h3 className="text-xl font-semibold text-[var(--purple-ink)] mb-4 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<Calendar className="h-5 w-5" />
{year}
</h3>
<div className="space-y-4">
{groupedHistory[year].map(bylaws => (
<Card key={bylaws.id} className="p-6 bg-[var(--lavender-600)] rounded-xl border border-[var(--neutral-800)]">
<div className="flex items-center justify-between">
<div>
<h4 className="text-lg font-semibold text-[var(--purple-ink)] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
{bylaws.title}
</h4>
<div className="flex items-center gap-3 text-sm text-brand-purple ">
<span>Version {bylaws.version}</span>
<span></span>
<span>Effective {formatDate(bylaws.effective_date)}</span>
</div>
</div>
<Button
onClick={() => window.open(bylaws.document_url, '_blank')}
variant="outline"
size="sm"
className="border-brand-purple text-brand-purple hover:bg-[var(--lavender-300)] rounded-full flex items-center gap-2"
>
<ExternalLink className="h-4 w-4" />
View
</Button>
</div>
</Card>
))}
</div>
</Card>
</div>
))}
</div>
)}

View File

@@ -5,20 +5,36 @@ import MemberFooter from '../../components/MemberFooter';
import { Card } from '../../components/ui/card';
import { Button } from '../../components/ui/button';
import { Badge } from '../../components/ui/badge';
import { Input } from '../../components/ui/input';
import { toast } from 'sonner';
import { DollarSign, ExternalLink, TrendingUp } from 'lucide-react';
import { DollarSign, ExternalLink, TrendingUp, Search, Calendar } from 'lucide-react';
export default function Financials() {
const [reports, setReports] = useState([]);
const [years, setYears] = useState([]);
const [selectedYear, setSelectedYear] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchYears();
fetchReports();
}, []);
const fetchReports = async () => {
const fetchYears = async () => {
try {
const response = await api.get('/financials');
const response = await api.get('/financials/years');
setYears(response.data);
} catch (error) {
console.error('Failed to load years');
}
};
const fetchReports = async (year = null) => {
try {
setLoading(true);
const url = year ? `/financials?year=${year}` : '/financials';
const response = await api.get(url);
setReports(response.data);
} catch (error) {
toast.error('Failed to load financial reports');
@@ -27,6 +43,36 @@ export default function Financials() {
}
};
const handleYearFilter = (year) => {
setSelectedYear(year);
fetchReports(year);
};
const clearFilter = () => {
setSelectedYear(null);
fetchReports();
};
const filteredReports = reports.filter(report =>
report.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
report.year.toString().includes(searchTerm)
);
const groupByYear = (items) => {
const grouped = {};
items.forEach(item => {
const year = item.year;
if (!grouped[year]) {
grouped[year] = [];
}
grouped[year].push(item);
});
return grouped;
};
const groupedReports = groupByYear(filteredReports);
const sortedYears = Object.keys(groupedReports).sort((a, b) => b - a);
if (loading) {
return (
<div className="min-h-screen bg-background">
@@ -53,56 +99,104 @@ export default function Financials() {
<p className="text-brand-purple mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Access annual financial reports and stay informed about LOAF's fiscal responsibility.
</p>
{/* Filters */}
<div className="flex gap-4 flex-wrap items-center">
{/* Search */}
<div className="relative flex-1 min-w-[300px]">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-brand-purple " />
<Input
type="text"
placeholder="Search financial reports..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 border-[var(--neutral-800)] focus:border-brand-purple "
/>
</div>
{/* Year Filter */}
<div className="flex gap-2 flex-wrap">
<Button
onClick={clearFilter}
variant={selectedYear === null ? "default" : "outline"}
size="sm"
className={selectedYear === null ? "bg-brand-purple hover:bg-[var(--purple-muted)] text-white" : "border-brand-lavender text-brand-lavender "}
>
All Years
</Button>
{years.map(year => (
<Button
key={year}
onClick={() => handleYearFilter(year)}
variant={selectedYear === year ? "default" : "outline"}
size="sm"
className={selectedYear === year ? "bg-brand-purple text-white" : "border-brand-purple text-brand-purple "}
>
{year}
</Button>
))}
</div>
</div>
</div>
{/* Reports List */}
{reports.length === 0 ? (
{filteredReports.length === 0 ? (
<Card className="p-12 text-center bg-background rounded-2xl border border-[var(--neutral-800)]">
<TrendingUp className="h-16 w-16 text-[var(--neutral-800)] mx-auto mb-4" />
<p className="text-brand-purple text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
No financial reports available yet
No financial reports found
</p>
</Card>
) : (
<div className="space-y-6">
{reports.map(report => (
<Card key={report.id} className="p-8 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-lg transition-shadow">
<div className="flex items-center gap-6">
{/* Year Badge */}
<div className="bg-gradient-to-br from-brand-purple to-[var(--purple-ink)] p-6 rounded-xl text-white min-w-[120px] text-center">
<DollarSign className="h-8 w-8 mx-auto mb-2" />
<div className="text-3xl font-bold" style={{ fontFamily: "'Inter', sans-serif" }}>
{report.year}
</div>
<div className="text-sm opacity-90">Fiscal Year</div>
</div>
<div className="space-y-8">
{sortedYears.map(year => (
<div key={year}>
<h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
<Calendar className="h-6 w-6" />
{year}
</h2>
<div className="space-y-6">
{groupedReports[year].map(report => (
<Card key={report.id} className="p-8 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-lg transition-shadow">
<div className="flex items-center gap-6">
{/* Year Badge */}
<div className="bg-gradient-to-br from-brand-purple to-[var(--purple-ink)] p-6 rounded-xl text-white min-w-[120px] text-center">
<DollarSign className="h-8 w-8 mx-auto mb-2" />
<div className="text-3xl font-bold" style={{ fontFamily: "'Inter', sans-serif" }}>
{report.year}
</div>
<div className="text-sm opacity-90">Fiscal Year</div>
</div>
{/* Report Details */}
<div className="flex-1">
<h3 className="text-2xl font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
{report.title}
</h3>
<div className="flex items-center gap-2 mb-4">
<Badge variant="outline" className="border-brand-purple text-brand-purple ">
{report.document_type === 'google_drive' ? 'Google Drive' : report.document_type.toUpperCase()}
</Badge>
</div>
<Button
onClick={() => window.open(report.document_url, '_blank')}
className="bg-brand-purple text-white hover:bg-[var(--purple-muted)] rounded-full flex items-center gap-2"
>
<ExternalLink className="h-4 w-4" />
View Report
</Button>
</div>
{/* Report Details */}
<div className="flex-1">
<h3 className="text-2xl font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
{report.title}
</h3>
<div className="flex items-center gap-2 mb-4">
<Badge variant="outline" className="border-brand-purple text-brand-purple ">
{report.document_type === 'google_drive' ? 'Google Drive' : report.document_type.toUpperCase()}
</Badge>
</div>
<Button
onClick={() => window.open(report.document_url, '_blank')}
className="bg-brand-purple text-white hover:bg-[var(--purple-muted)] rounded-full flex items-center gap-2"
>
<ExternalLink className="h-4 w-4" />
View Report
</Button>
</div>
</div>
</Card>
))}
</div>
</Card>
</div>
))}
</div>
)}
{/* Transparency Note */}
{reports.length > 0 && (
{filteredReports.length > 0 && (
<Card className="mt-8 p-6 bg-[var(--lavender-600)] border border-[var(--neutral-800)]">
<div className="flex items-start gap-3">
<TrendingUp className="h-5 w-5 text-brand-purple mt-1" />