Implement glass-bolt design system across platform
Build And Push Image / docker (push) Successful in 1m53s Details

- Removed 19 test/prototype pages while preserving BoltAI-Mockups for reference
- Created comprehensive DESIGN-SYSTEM.md documentation
- Updated and consolidated SCSS structure
- Applied subtle glassmorphic design to admin portal pages
- Updated admin members page with new glass-bolt styling
- Implemented consistent design patterns:
  - Glass cards with 60% white opacity and 4px blur
  - Subtle borders and soft shadows
  - Monaco red accent color (#dc2626)
  - Clean typography without excessive gradients
  - Hover states with gentle lift animations

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Matt 2025-09-07 17:41:41 +02:00
parent 9c812d78dd
commit 64a12ecd5b
65 changed files with 8136 additions and 10108 deletions

View File

@ -97,7 +97,10 @@
"Bash(git checkout:*)", "Bash(git checkout:*)",
"Bash(git branch:*)", "Bash(git branch:*)",
"mcp__zen__consensus", "mcp__zen__consensus",
"mcp___21st-dev_magic__21st_magic_component_refiner" "mcp___21st-dev_magic__21st_magic_component_refiner",
"Bash(timeout:*)",
"Bash(Copy-Item:*)",
"Bash(Remove-Item -Path \"Z:\\Repos\\monacousa-portal\\pages\\glass-bolt-perfect.vue\" -Force)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 953 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -0,0 +1,3 @@
{
"template": "bolt-vite-react-ts"
}

View File

@ -0,0 +1,5 @@
For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production.
By default, this template supports JSX syntax with Tailwind CSS classes, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them.
Use icons from lucide-react for logos.

25
BoltAI-Mockups/project/.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.env

View File

@ -0,0 +1,28 @@
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
);

View File

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MonacoUSA Dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4042
BoltAI-Mockups/project/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,33 @@
{
"name": "vite-react-typescript-starter",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"lucide-react": "^0.344.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@eslint/js": "^9.9.1",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.18",
"eslint": "^9.9.1",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.11",
"globals": "^15.9.0",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.1",
"typescript": "^5.5.3",
"typescript-eslint": "^8.3.0",
"vite": "^5.4.2"
}
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -0,0 +1,109 @@
import React, { useState } from 'react';
import { Sidebar } from './components/layout/Sidebar';
import { BoardDashboard } from './components/pages/BoardDashboard';
import { MemberList } from './components/pages/MemberList';
function App() {
const [currentPage, setCurrentPage] = useState('dashboard');
const renderPage = () => {
switch (currentPage) {
case 'dashboard':
return <BoardDashboard />;
case 'members':
return <MemberList />;
case 'dues':
return (
<div className="flex items-center justify-center h-96">
<div className="text-center space-y-4">
<div className="w-24 h-24 bg-gradient-to-br from-red-100 to-red-200 rounded-full flex items-center justify-center mx-auto">
<span className="text-4xl">🚧</span>
</div>
<h2 className="text-3xl font-bold text-gray-900">Dues Management</h2>
<p className="text-gray-600 max-w-md">This premium feature is currently under development. Stay tuned for advanced dues tracking capabilities.</p>
</div>
</div>
);
case 'events':
return (
<div className="flex items-center justify-center h-96">
<div className="text-center space-y-4">
<div className="w-24 h-24 bg-gradient-to-br from-red-100 to-red-200 rounded-full flex items-center justify-center mx-auto">
<span className="text-4xl">📅</span>
</div>
<h2 className="text-3xl font-bold text-gray-900">Events</h2>
<p className="text-gray-600 max-w-md">Event management system coming soon with calendar integration and RSVP tracking.</p>
</div>
</div>
);
case 'reports':
return (
<div className="flex items-center justify-center h-96">
<div className="text-center space-y-4">
<div className="w-24 h-24 bg-gradient-to-br from-red-100 to-red-200 rounded-full flex items-center justify-center mx-auto">
<span className="text-4xl">📊</span>
</div>
<h2 className="text-3xl font-bold text-gray-900">Reports</h2>
<p className="text-gray-600 max-w-md">Advanced analytics and reporting dashboard in development.</p>
</div>
</div>
);
case 'documents':
return (
<div className="flex items-center justify-center h-96">
<div className="text-center space-y-4">
<div className="w-24 h-24 bg-gradient-to-br from-red-100 to-red-200 rounded-full flex items-center justify-center mx-auto">
<span className="text-4xl">📄</span>
</div>
<h2 className="text-3xl font-bold text-gray-900">Documents</h2>
<p className="text-gray-600 max-w-md">Document management system with secure file sharing capabilities.</p>
</div>
</div>
);
case 'notifications':
return (
<div className="flex items-center justify-center h-96">
<div className="text-center space-y-4">
<div className="w-24 h-24 bg-gradient-to-br from-red-100 to-red-200 rounded-full flex items-center justify-center mx-auto">
<span className="text-4xl">🔔</span>
</div>
<h2 className="text-3xl font-bold text-gray-900">Notifications</h2>
<p className="text-gray-600 max-w-md">Smart notification center with customizable alerts and reminders.</p>
</div>
</div>
);
case 'settings':
return (
<div className="flex items-center justify-center h-96">
<div className="text-center space-y-4">
<div className="w-24 h-24 bg-gradient-to-br from-red-100 to-red-200 rounded-full flex items-center justify-center mx-auto">
<span className="text-4xl"></span>
</div>
<h2 className="text-3xl font-bold text-gray-900">Settings</h2>
<p className="text-gray-600 max-w-md">System configuration and user preferences management.</p>
</div>
</div>
);
default:
return <BoardDashboard />;
}
};
return (
<div className="flex min-h-screen bg-gradient-to-br from-red-50 via-white to-red-100/30 relative overflow-hidden">
{/* Background Elements */}
<div className="absolute top-0 left-1/4 w-96 h-96 bg-red-200/20 rounded-full blur-3xl animate-pulse-slow"></div>
<div className="absolute bottom-0 right-1/4 w-64 h-64 bg-red-300/20 rounded-full blur-2xl animate-float"></div>
<Sidebar currentPage={currentPage} onPageChange={setCurrentPage} />
<div className="flex-1 overflow-hidden relative">
<main className="max-w-7xl mx-auto px-10 py-12 relative z-10">
{renderPage()}
</main>
</div>
</div>
);
}
export default App;

View File

@ -0,0 +1,165 @@
import React, { useState } from 'react';
import { Member } from '../../types';
import { MemberCard } from './MemberCard';
import { Card } from '../ui/Card';
import { CreditCard, Sparkles } from 'lucide-react';
interface DuesManagementProps {
members: Member[];
}
export const DuesManagement: React.FC<DuesManagementProps> = ({ members }) => {
const [activeTab, setActiveTab] = useState<'overdue' | 'due-soon' | 'paid'>('overdue');
const filterMembersByTab = (tab: string) => {
switch (tab) {
case 'overdue':
return members.filter(m => m.duesStatus === 'Overdue');
case 'due-soon':
return members.filter(m => m.duesStatus === 'Due Soon');
case 'paid':
return members.filter(m => m.duesStatus === 'Paid');
default:
return [];
}
};
const filteredMembers = filterMembersByTab(activeTab).slice(0, 8);
const tabs = [
{
id: 'overdue',
label: 'Overdue',
count: members.filter(m => m.duesStatus === 'Overdue').length,
gradient: 'from-red-500 to-red-600',
icon: '⚠️'
},
{
id: 'due-soon',
label: 'Due Soon',
count: members.filter(m => m.duesStatus === 'Due Soon').length,
gradient: 'from-amber-500 to-orange-600',
icon: '⏰'
},
{
id: 'paid',
label: 'Recently Paid',
count: members.filter(m => m.duesStatus === 'Paid').length,
gradient: 'from-green-500 to-emerald-600',
icon: '✅'
},
];
return (
<div className="relative group">
{/* Background glow */}
<div className="absolute -inset-4 bg-gradient-to-r from-red-600/20 via-red-500/10 to-red-600/20 rounded-3xl blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-700"></div>
<Card className="relative bg-white/95 backdrop-blur-md border-0 shadow-ultra rounded-3xl overflow-hidden">
{/* Animated background elements */}
<div className="absolute top-0 right-0 w-64 h-64 bg-gradient-to-br from-red-50 to-red-100 rounded-full -translate-y-32 translate-x-32 opacity-50"></div>
<div className="absolute bottom-0 left-0 w-48 h-48 bg-gradient-to-tr from-red-100 to-red-200 rounded-full translate-y-24 -translate-x-24 opacity-30"></div>
<div className="relative p-10">
{/* Header */}
<div className="flex items-center justify-between mb-10">
<div className="space-y-2">
<div className="flex items-center space-x-3">
<div className="relative">
<div className="p-4 bg-gradient-to-br from-red-500 to-red-600 rounded-2xl shadow-red">
<CreditCard className="w-8 h-8 text-white" />
</div>
<div className="absolute -top-1 -right-1 w-6 h-6 bg-gradient-to-r from-yellow-400 to-orange-500 rounded-full flex items-center justify-center">
<Sparkles className="w-3 h-3 text-white" />
</div>
</div>
<div>
<h3 className="text-3xl font-black text-gray-900 tracking-tight">Dues Management</h3>
<p className="text-gray-600 font-medium">Track and manage member payments</p>
</div>
</div>
</div>
<div className="bg-gradient-to-r from-red-100 to-red-200 rounded-2xl p-4 shadow-soft">
<div className="text-center">
<p className="text-2xl font-bold text-red-800">${members.reduce((sum, m) => sum + m.dueAmount, 0).toLocaleString()}</p>
<p className="text-red-600 text-sm font-semibold">Total Outstanding</p>
</div>
</div>
</div>
{/* Enhanced Tabs */}
<div className="relative mb-10">
<div className="flex space-x-2 bg-gradient-to-r from-gray-50 to-gray-100 rounded-2xl p-2 shadow-inner">
{tabs.map((tab, index) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`relative flex-1 px-8 py-4 rounded-xl text-sm font-bold transition-all duration-300 overflow-hidden group ${
activeTab === tab.id
? `bg-gradient-to-r ${tab.gradient} text-white shadow-lg transform scale-105`
: 'text-gray-700 hover:text-gray-900 hover:bg-white/80 hover:scale-102'
}`}
style={{ animationDelay: `${index * 100}ms` }}
>
{/* Active tab shimmer effect */}
{activeTab === tab.id && (
<div className="absolute inset-0 -top-px overflow-hidden rounded-xl">
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent -skew-x-12 -translate-x-full group-hover:animate-shimmer"></div>
</div>
)}
<div className="relative z-10 flex items-center justify-center space-x-2">
<span className="text-lg">{tab.icon}</span>
<span>{tab.label}</span>
<div className={`px-2 py-1 rounded-full text-xs font-bold ${
activeTab === tab.id
? 'bg-white/20 text-white'
: 'bg-gray-200 text-gray-700'
}`}>
{tab.count}
</div>
</div>
</button>
))}
</div>
</div>
{/* Member Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{filteredMembers.map((member, index) => (
<div
key={member.id}
className="animate-scale-in"
style={{ animationDelay: `${index * 100}ms` }}
>
<MemberCard member={member} />
</div>
))}
</div>
{/* Empty State */}
{filteredMembers.length === 0 && (
<div className="text-center py-16">
<div className="w-24 h-24 bg-gradient-to-br from-gray-100 to-gray-200 rounded-full flex items-center justify-center mx-auto mb-6">
<CreditCard className="w-12 h-12 text-gray-400" />
</div>
<h4 className="text-xl font-bold text-gray-900 mb-2">No members found</h4>
<p className="text-gray-600">No members in this category at the moment.</p>
</div>
)}
{/* Load More Button */}
{filteredMembers.length >= 8 && (
<div className="text-center">
<button className="relative bg-gradient-to-r from-red-600 to-red-700 text-white px-8 py-4 rounded-2xl font-bold text-sm hover:from-red-700 hover:to-red-800 transform hover:scale-105 transition-all duration-300 shadow-red hover:shadow-neon group overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-r from-white/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-2xl"></div>
<span className="relative z-10">Load More Members</span>
</button>
</div>
)}
</div>
</Card>
</div>
);
};

View File

@ -0,0 +1,131 @@
import React from 'react';
import { Member } from '../../types';
import { Avatar } from '../ui/Avatar';
import { Badge } from '../ui/Badge';
import { Card } from '../ui/Card';
import { Mail, Phone, MoreHorizontal, Clock, AlertTriangle, CheckCircle } from 'lucide-react';
interface MemberCardProps {
member: Member;
}
export const MemberCard: React.FC<MemberCardProps> = ({ member }) => {
const getInitials = (firstName: string, lastName: string) => {
return `${firstName[0]}${lastName[0]}`.toUpperCase();
};
const getDuesStatusVariant = (status: string) => {
switch (status) {
case 'Paid': return 'success';
case 'Due Soon': return 'warning';
case 'Overdue': return 'danger';
default: return 'secondary';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'Paid': return CheckCircle;
case 'Due Soon': return Clock;
case 'Overdue': return AlertTriangle;
default: return Clock;
}
};
const formatDaysText = () => {
if (member.duesStatus === 'Overdue' && member.daysOverdue) {
return `${member.daysOverdue} days overdue`;
}
if (member.duesStatus === 'Due Soon' && member.daysTillDue) {
return `Due in ${member.daysTillDue} days`;
}
if (member.lastPaymentDate) {
return `Paid ${new Date(member.lastPaymentDate).toLocaleDateString()}`;
}
return 'No payment info';
};
const StatusIcon = getStatusIcon(member.duesStatus);
return (
<div className="group relative">
{/* Hover glow effect */}
<div className="absolute -inset-1 bg-gradient-to-r from-red-600/20 to-red-400/20 rounded-2xl blur opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<Card className="relative bg-white/90 backdrop-blur-md border border-red-100/50 hover:border-red-200 transform hover:scale-105 transition-all duration-300 shadow-soft hover:shadow-red rounded-2xl overflow-hidden">
{/* Background gradient */}
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-red-50/50 to-transparent rounded-full -translate-y-16 translate-x-16"></div>
<div className="relative p-6">
{/* Header */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-center space-x-3">
<div className="relative">
<Avatar
initials={getInitials(member.firstName, member.lastName)}
size="md"
className="ring-2 ring-red-100 group-hover:ring-red-200 transition-all duration-300"
/>
<div className="absolute -bottom-1 -right-1 w-6 h-6 bg-gradient-to-r from-green-400 to-emerald-500 rounded-full border-2 border-white flex items-center justify-center">
<div className="w-2 h-2 bg-white rounded-full"></div>
</div>
</div>
<div>
<h4 className="font-bold text-gray-900 group-hover:text-red-900 transition-colors duration-300">
{member.firstName} {member.lastName}
</h4>
<p className="text-sm text-gray-500 font-medium">{member.id}</p>
</div>
</div>
<button className="p-2 rounded-full hover:bg-red-100 transition-all duration-300 hover:scale-110 group/btn">
<MoreHorizontal size={16} className="text-gray-400 group-hover/btn:text-red-600" />
</button>
</div>
{/* Status Section */}
<div className="space-y-3 mb-6">
<div className="flex items-center space-x-2">
<StatusIcon size={16} className={`${
member.duesStatus === 'Paid' ? 'text-green-600' :
member.duesStatus === 'Due Soon' ? 'text-amber-600' :
'text-red-600'
}`} />
<Badge variant={getDuesStatusVariant(member.duesStatus)}>
{member.duesStatus}
</Badge>
</div>
<p className="text-sm font-semibold text-gray-700">{formatDaysText()}</p>
{member.dueAmount > 0 && (
<div className="bg-gradient-to-r from-red-50 to-red-100 rounded-xl p-3 border border-red-200/50">
<p className="text-lg font-black text-red-900">
${member.dueAmount}
</p>
<p className="text-red-600 text-xs font-semibold uppercase tracking-wide">Amount Due</p>
</div>
)}
</div>
{/* Action Buttons */}
<div className="flex items-center space-x-2">
<button className="flex-1 p-3 rounded-xl bg-gradient-to-r from-red-100 to-red-200 hover:from-red-200 hover:to-red-300 transition-all duration-300 hover:scale-105 group/action border border-red-200/50">
<Mail size={16} className="text-red-600 mx-auto group-hover/action:animate-bounce" />
</button>
{member.phone && (
<button className="flex-1 p-3 rounded-xl bg-gradient-to-r from-red-100 to-red-200 hover:from-red-200 hover:to-red-300 transition-all duration-300 hover:scale-105 group/action border border-red-200/50">
<Phone size={16} className="text-red-600 mx-auto group-hover/action:animate-bounce" />
</button>
)}
<button className="px-4 py-3 bg-gradient-to-r from-red-600 to-red-700 text-white rounded-xl hover:from-red-700 hover:to-red-800 transition-all duration-300 hover:scale-105 shadow-red text-sm font-bold">
View
</button>
</div>
</div>
</Card>
</div>
);
};

View File

@ -0,0 +1,122 @@
import React from 'react';
import { Card } from '../ui/Card';
import { Users, UserCheck, AlertCircle, Calendar, TrendingUp, Sparkles } from 'lucide-react';
import { DashboardStats } from '../../types';
interface StatsGridProps {
stats: DashboardStats;
}
export const StatsGrid: React.FC<StatsGridProps> = ({ stats }) => {
const statCards = [
{
title: 'Total Members',
value: stats.totalMembers,
icon: Users,
trend: `+${stats.memberTrend}%`,
gradient: 'from-red-500 via-red-600 to-red-700',
glowColor: 'red-500',
delay: '0ms',
},
{
title: 'Active Members',
value: stats.activeMembers,
icon: UserCheck,
gradient: 'from-red-600 via-red-700 to-red-800',
glowColor: 'red-600',
delay: '100ms',
},
{
title: 'Pending Dues',
value: stats.pendingDues,
icon: AlertCircle,
gradient: 'from-red-700 via-red-800 to-red-900',
glowColor: 'red-700',
delay: '200ms',
},
{
title: 'Upcoming Events',
value: stats.upcomingEvents,
icon: Calendar,
gradient: 'from-red-800 via-red-900 to-black',
glowColor: 'red-800',
delay: '300ms',
},
];
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-16">
{statCards.map((stat, index) => {
const Icon = stat.icon;
return (
<div
key={index}
className="group animate-slide-up"
style={{ animationDelay: stat.delay }}
>
<div className="relative">
{/* Glow effect */}
<div className={`absolute -inset-1 bg-gradient-to-r ${stat.gradient} rounded-3xl blur opacity-25 group-hover:opacity-75 transition duration-1000 group-hover:duration-200 animate-glow`}></div>
{/* Main card */}
<div className={`relative bg-gradient-to-br ${stat.gradient} p-8 rounded-3xl shadow-ultra border border-white/10 backdrop-blur-sm transform transition-all duration-500 hover:scale-105 hover:rotate-1 group-hover:shadow-neon overflow-hidden`}>
{/* Animated background elements */}
<div className="absolute top-0 right-0 w-32 h-32 bg-white/5 rounded-full -translate-y-16 translate-x-16 group-hover:scale-150 transition-transform duration-700"></div>
<div className="absolute bottom-0 left-0 w-24 h-24 bg-white/10 rounded-full translate-y-12 -translate-x-12 group-hover:scale-125 transition-transform duration-700"></div>
{/* Shimmer effect */}
<div className="absolute inset-0 -top-px overflow-hidden rounded-3xl">
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent -skew-x-12 -translate-x-full group-hover:animate-shimmer"></div>
</div>
<div className="relative z-10">
{/* Header */}
<div className="flex items-start justify-between mb-6">
<div className="relative">
<div className="p-4 bg-white/20 backdrop-blur-md rounded-2xl shadow-glass border border-white/30 group-hover:bg-white/30 transition-all duration-300">
<Icon className="w-8 h-8 text-white drop-shadow-lg" />
</div>
<div className="absolute -top-1 -right-1 w-6 h-6 bg-gradient-to-r from-yellow-400 to-orange-500 rounded-full flex items-center justify-center animate-pulse">
<Sparkles className="w-3 h-3 text-white" />
</div>
</div>
{stat.trend && (
<div className="bg-white/20 backdrop-blur-md rounded-full px-4 py-2 border border-white/30 group-hover:bg-white/30 transition-all duration-300">
<div className="flex items-center space-x-2">
<TrendingUp size={16} className="text-green-300" />
<span className="text-white font-bold text-sm">{stat.trend}</span>
</div>
</div>
)}
</div>
{/* Content */}
<div className="space-y-3">
<p className="text-white/80 text-sm font-semibold uppercase tracking-wider">
{stat.title}
</p>
<div className="flex items-end space-x-2">
<p className="text-5xl font-black text-white leading-none tracking-tight">
{stat.value}
</p>
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse mb-2"></div>
</div>
</div>
{/* Progress bar */}
<div className="mt-6 h-2 bg-white/20 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-white/60 to-white/80 rounded-full transition-all duration-1000 ease-out"
style={{ width: `${Math.min(100, (stat.value / 200) * 100)}%` }}
></div>
</div>
</div>
</div>
</div>
</div>
);
})}
</div>
);
};

View File

@ -0,0 +1,123 @@
import React from 'react';
import {
BarChart3,
Users,
Calendar,
Settings,
FileText,
Home,
CreditCard,
Bell,
Sparkles
} from 'lucide-react';
interface SidebarProps {
currentPage: string;
onPageChange: (page: string) => void;
}
const menuItems = [
{ id: 'dashboard', label: 'Dashboard', icon: Home },
{ id: 'members', label: 'Members', icon: Users },
{ id: 'dues', label: 'Dues Management', icon: CreditCard },
{ id: 'events', label: 'Events', icon: Calendar },
{ id: 'reports', label: 'Reports', icon: BarChart3 },
{ id: 'documents', label: 'Documents', icon: FileText },
{ id: 'notifications', label: 'Notifications', icon: Bell },
{ id: 'settings', label: 'Settings', icon: Settings },
];
export const Sidebar: React.FC<SidebarProps> = ({ currentPage, onPageChange }) => {
return (
<div className="w-72 relative">
{/* Animated Background */}
<div className="absolute inset-0 bg-gradient-to-br from-red-600 via-red-700 to-red-900 opacity-90"></div>
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
<div className="absolute top-0 right-0 w-32 h-32 bg-white/10 rounded-full blur-3xl animate-pulse-slow"></div>
<div className="absolute bottom-20 left-0 w-24 h-24 bg-red-300/20 rounded-full blur-2xl animate-float"></div>
<div className="relative h-screen flex flex-col backdrop-blur-sm">
{/* Header */}
<div className="p-8 border-b border-white/10">
<div className="flex items-center space-x-4 mb-4">
<div className="relative">
<div className="w-12 h-12 bg-gradient-to-br from-white to-red-100 rounded-2xl flex items-center justify-center shadow-ultra transform rotate-3 hover:rotate-0 transition-transform duration-300">
<Sparkles className="w-6 h-6 text-red-600" />
</div>
<div className="absolute -top-1 -right-1 w-4 h-4 bg-gradient-to-r from-yellow-400 to-orange-500 rounded-full animate-pulse"></div>
</div>
<div>
<h1 className="text-2xl font-bold text-white tracking-tight">MonacoUSA</h1>
<p className="text-red-100/80 text-sm font-medium">Elite Dashboard</p>
</div>
</div>
<div className="h-px bg-gradient-to-r from-transparent via-white/30 to-transparent"></div>
</div>
{/* Navigation */}
<nav className="flex-1 p-6 space-y-3 overflow-y-auto">
{menuItems.map((item, index) => {
const Icon = item.icon;
const isActive = currentPage === item.id;
return (
<div key={item.id} className="relative group" style={{ animationDelay: `${index * 50}ms` }}>
<button
onClick={() => onPageChange(item.id)}
className={`w-full flex items-center space-x-4 px-6 py-4 rounded-2xl text-left transition-all duration-300 relative overflow-hidden ${
isActive
? 'bg-white/20 backdrop-blur-md text-white shadow-glass border border-white/20 transform scale-105'
: 'text-red-100/80 hover:bg-white/10 hover:text-white hover:backdrop-blur-md hover:transform hover:scale-105 hover:translate-x-2'
}`}
>
{/* Active indicator */}
{isActive && (
<div className="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-white to-red-200 rounded-r-full"></div>
)}
{/* Icon container */}
<div className={`relative p-2 rounded-xl transition-all duration-300 ${
isActive
? 'bg-white/20 shadow-inner'
: 'group-hover:bg-white/10'
}`}>
<Icon size={20} className="relative z-10" />
{isActive && (
<div className="absolute inset-0 bg-white/10 rounded-xl animate-pulse"></div>
)}
</div>
<span className="font-medium tracking-wide">{item.label}</span>
{/* Hover effect */}
<div className="absolute inset-0 bg-gradient-to-r from-white/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-2xl"></div>
</button>
</div>
);
})}
</nav>
{/* Footer */}
<div className="p-6 border-t border-white/10">
<div className="relative bg-gradient-to-br from-white/10 to-white/5 backdrop-blur-md rounded-2xl p-6 border border-white/10 overflow-hidden group hover:from-white/15 hover:to-white/10 transition-all duration-300">
<div className="absolute top-0 right-0 w-20 h-20 bg-white/5 rounded-full -translate-y-10 translate-x-10"></div>
<div className="relative">
<div className="flex items-center space-x-3 mb-3">
<div className="w-8 h-8 bg-gradient-to-br from-green-400 to-emerald-500 rounded-full flex items-center justify-center">
<div className="w-2 h-2 bg-white rounded-full animate-pulse"></div>
</div>
<div>
<p className="text-white font-semibold text-sm">System Status</p>
<p className="text-green-300 text-xs">All systems operational</p>
</div>
</div>
<button className="w-full bg-white/10 hover:bg-white/20 text-white text-sm font-medium py-2 px-4 rounded-xl transition-all duration-300 border border-white/20 hover:border-white/30">
Contact Support
</button>
</div>
</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,283 @@
import React, { useState } from 'react';
import { Member } from '../../types';
import { Avatar } from '../ui/Avatar';
import { Badge } from '../ui/Badge';
import { CountryFlag } from '../ui/CountryFlag';
import { Card } from '../ui/Card';
import { ChevronUp, ChevronDown, MoreHorizontal, Edit, Trash2, Mail, Eye } from 'lucide-react';
interface MemberTableProps {
members: Member[];
}
type SortField = 'name' | 'email' | 'joinDate' | 'duesStatus';
type SortDirection = 'asc' | 'desc';
export const MemberTable: React.FC<MemberTableProps> = ({ members }) => {
const [sortField, setSortField] = useState<SortField>('name');
const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 10;
const handleSort = (field: SortField) => {
if (sortField === field) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortDirection('asc');
}
};
const sortedMembers = [...members].sort((a, b) => {
let aValue: string | number;
let bValue: string | number;
switch (sortField) {
case 'name':
aValue = `${a.firstName} ${a.lastName}`;
bValue = `${b.firstName} ${b.lastName}`;
break;
case 'email':
aValue = a.email;
bValue = b.email;
break;
case 'joinDate':
aValue = new Date(a.joinDate).getTime();
bValue = new Date(b.joinDate).getTime();
break;
case 'duesStatus':
aValue = a.duesStatus;
bValue = b.duesStatus;
break;
default:
aValue = '';
bValue = '';
}
if (sortDirection === 'asc') {
return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
} else {
return aValue > bValue ? -1 : aValue < bValue ? 1 : 0;
}
});
const paginatedMembers = sortedMembers.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
const totalPages = Math.ceil(members.length / itemsPerPage);
const getInitials = (firstName: string, lastName: string) => {
return `${firstName[0]}${lastName[0]}`.toUpperCase();
};
const getDuesStatusVariant = (status: string) => {
switch (status) {
case 'Paid': return 'success';
case 'Due Soon': return 'warning';
case 'Overdue': return 'danger';
default: return 'secondary';
}
};
const getStatusVariant = (status: string) => {
switch (status) {
case 'Active': return 'success';
case 'Inactive': return 'secondary';
case 'Pending': return 'warning';
default: return 'secondary';
}
};
const SortButton: React.FC<{ field: SortField; children: React.ReactNode }> = ({ field, children }) => (
<button
onClick={() => handleSort(field)}
className="flex items-center space-x-2 hover:text-red-900 transition-colors duration-300 group"
>
<span className="font-bold">{children}</span>
<div className="flex flex-col">
<ChevronUp
size={12}
className={`transition-colors duration-300 ${
sortField === field && sortDirection === 'asc'
? 'text-red-600'
: 'text-gray-400 group-hover:text-red-400'
}`}
/>
<ChevronDown
size={12}
className={`-mt-1 transition-colors duration-300 ${
sortField === field && sortDirection === 'desc'
? 'text-red-600'
: 'text-gray-400 group-hover:text-red-400'
}`}
/>
</div>
</button>
);
return (
<div className="relative group">
<div className="absolute -inset-2 bg-gradient-to-r from-red-600/10 via-red-500/5 to-red-600/10 rounded-3xl blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-700"></div>
<Card className="relative overflow-hidden bg-white/95 backdrop-blur-md border border-red-100/50 shadow-ultra rounded-3xl">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gradient-to-r from-red-50 via-red-100 to-red-50 border-b-2 border-red-200">
<tr>
<th className="text-left py-6 px-8 text-red-900">
<SortButton field="name">Member</SortButton>
</th>
<th className="text-left py-6 px-8 text-red-900">
<SortButton field="email">Email</SortButton>
</th>
<th className="text-left py-6 px-8 font-bold text-red-900">
Nationality
</th>
<th className="text-left py-6 px-8 font-bold text-red-900">
Status
</th>
<th className="text-left py-6 px-8 text-red-900">
<SortButton field="duesStatus">Dues Status</SortButton>
</th>
<th className="text-left py-6 px-8 font-bold text-red-900">
Member Type
</th>
<th className="text-left py-6 px-8 text-red-900">
<SortButton field="joinDate">Join Date</SortButton>
</th>
<th className="text-left py-6 px-8 font-bold text-red-900">
Actions
</th>
</tr>
</thead>
<tbody className="divide-y divide-red-100">
{paginatedMembers.map((member, index) => (
<tr
key={member.id}
className={`hover:bg-red-50 transition-all duration-300 group/row ${
index % 2 === 1 ? 'bg-red-25' : 'bg-white'
}`}
>
<td className="py-6 px-8">
<div className="flex items-center space-x-4">
<div className="relative">
<Avatar
initials={getInitials(member.firstName, member.lastName)}
size="sm"
className="ring-2 ring-red-100 group-hover/row:ring-red-200 transition-all duration-300"
/>
<div className="absolute -bottom-1 -right-1 w-4 h-4 bg-gradient-to-r from-green-400 to-emerald-500 rounded-full border-2 border-white">
<div className="w-1 h-1 bg-white rounded-full mx-auto mt-0.5"></div>
</div>
</div>
<div>
<div className="font-bold text-gray-900 group-hover/row:text-red-900 transition-colors duration-300">
{member.firstName} {member.lastName}
</div>
<div className="text-sm text-gray-500 font-medium">{member.id}</div>
</div>
</div>
</td>
<td className="py-6 px-8 text-gray-900 font-medium group-hover/row:text-red-900 transition-colors duration-300">
{member.email}
</td>
<td className="py-6 px-8">
<div className="flex space-x-1">
{member.nationality.map((code, idx) => (
<CountryFlag key={idx} code={code} />
))}
</div>
</td>
<td className="py-6 px-8">
<Badge variant={getStatusVariant(member.status)}>
{member.status}
</Badge>
</td>
<td className="py-6 px-8">
<Badge variant={getDuesStatusVariant(member.duesStatus)}>
{member.duesStatus}
</Badge>
</td>
<td className="py-6 px-8 text-gray-900 font-medium group-hover/row:text-red-900 transition-colors duration-300">
{member.memberType}
</td>
<td className="py-6 px-8 text-gray-900 font-medium group-hover/row:text-red-900 transition-colors duration-300">
{new Date(member.joinDate).toLocaleDateString()}
</td>
<td className="py-6 px-8">
<div className="flex items-center space-x-2">
<button
className="p-3 rounded-xl hover:bg-red-100 transition-all duration-300 hover:scale-110 group/btn"
title="View Details"
>
<Eye size={16} className="text-gray-400 group-hover/btn:text-red-600" />
</button>
<button
className="p-3 rounded-xl hover:bg-red-100 transition-all duration-300 hover:scale-110 group/btn"
title="Send Email"
>
<Mail size={16} className="text-gray-400 group-hover/btn:text-red-600" />
</button>
<button
className="p-3 rounded-xl hover:bg-red-100 transition-all duration-300 hover:scale-110 group/btn"
title="Edit Member"
>
<Edit size={16} className="text-gray-400 group-hover/btn:text-red-600" />
</button>
<button
className="p-3 rounded-xl hover:bg-red-100 transition-all duration-300 hover:scale-110 group/btn"
title="More Options"
>
<MoreHorizontal size={16} className="text-gray-400 group-hover/btn:text-red-600" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Enhanced Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-between px-8 py-6 border-t-2 border-red-100 bg-gradient-to-r from-red-50/50 to-red-100/50">
<div className="text-sm text-red-700 font-semibold">
Showing {(currentPage - 1) * itemsPerPage + 1} to {Math.min(currentPage * itemsPerPage, members.length)} of {members.length} members
</div>
<div className="flex space-x-2">
<button
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
disabled={currentPage === 1}
className="px-6 py-3 rounded-xl border-2 border-red-200 text-sm font-bold hover:bg-red-50 disabled:opacity-50 disabled:cursor-not-allowed text-red-700 transition-all duration-300 hover:scale-105"
>
Previous
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<button
key={page}
onClick={() => setCurrentPage(page)}
className={`px-4 py-3 rounded-xl text-sm font-bold transition-all duration-300 hover:scale-105 ${
currentPage === page
? 'bg-gradient-to-r from-red-600 to-red-700 text-white shadow-red'
: 'border-2 border-red-200 hover:bg-red-50 text-red-700'
}`}
>
{page}
</button>
))}
<button
onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
disabled={currentPage === totalPages}
className="px-6 py-3 rounded-xl border-2 border-red-200 text-sm font-bold hover:bg-red-50 disabled:opacity-50 disabled:cursor-not-allowed text-red-700 transition-all duration-300 hover:scale-105"
>
Next
</button>
</div>
</div>
)}
</Card>
</div>
);
};

View File

@ -0,0 +1,131 @@
import React from 'react';
import { currentUser, dashboardStats, members } from '../../data/mockData';
import { Avatar } from '../ui/Avatar';
import { StatsGrid } from '../dashboard/StatsGrid';
import { DuesManagement } from '../dashboard/DuesManagement';
import { Crown, Sparkles, Calendar, Bell } from 'lucide-react';
export const BoardDashboard: React.FC = () => {
const getInitials = (firstName: string, lastName: string) => {
return `${firstName[0]}${lastName[0]}`.toUpperCase();
};
return (
<div className="space-y-8">
{/* Ultra-Modern Hero Header */}
<div className="relative overflow-hidden">
{/* Animated background */}
<div className="absolute inset-0 bg-gradient-to-br from-red-600 via-red-700 to-red-900"></div>
<div className="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent"></div>
{/* Floating elements */}
<div className="absolute top-10 right-20 w-64 h-64 bg-white/5 rounded-full blur-3xl animate-float"></div>
<div className="absolute bottom-10 left-20 w-48 h-48 bg-red-300/10 rounded-full blur-2xl animate-pulse-slow"></div>
<div className="absolute top-1/2 left-1/2 w-32 h-32 bg-white/10 rounded-full blur-xl animate-bounce-slow"></div>
{/* Mesh gradient overlay */}
<div className="absolute inset-0 opacity-20 bg-mesh-gradient animate-gradient bg-400% mix-blend-overlay"></div>
<div className="relative p-12 rounded-3xl">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-8 lg:space-y-0">
{/* User Info */}
<div className="flex items-center space-x-6 animate-slide-in">
<div className="relative group">
{/* Avatar glow */}
<div className="absolute -inset-2 bg-gradient-to-r from-white/30 to-red-200/30 rounded-full blur-lg group-hover:blur-xl transition-all duration-300"></div>
<div className="relative">
<Avatar
initials={getInitials(currentUser.firstName, currentUser.lastName)}
size="xl"
className="ring-4 ring-white/40 shadow-ultra backdrop-blur-sm border-2 border-white/20 group-hover:scale-110 transition-transform duration-300"
/>
{/* Status indicator */}
<div className="absolute -bottom-2 -right-2 flex items-center space-x-1">
<div className="w-8 h-8 bg-gradient-to-r from-green-400 to-emerald-500 rounded-full border-4 border-white shadow-lg flex items-center justify-center">
<div className="w-2 h-2 bg-white rounded-full animate-pulse"></div>
</div>
</div>
{/* Crown icon */}
<div className="absolute -top-3 -right-1 w-8 h-8 bg-gradient-to-r from-yellow-400 to-orange-500 rounded-full flex items-center justify-center shadow-lg animate-bounce-slow">
<Crown className="w-4 h-4 text-white" />
</div>
</div>
</div>
<div className="space-y-3">
<div>
<h1 className="text-5xl font-black text-white mb-2 tracking-tight">
Welcome back,
<span className="block bg-gradient-to-r from-white via-red-100 to-white bg-clip-text text-transparent animate-gradient bg-300%">
{currentUser.firstName}!
</span>
</h1>
</div>
<div className="flex items-center space-x-4">
<div className="bg-white/20 backdrop-blur-md rounded-full px-6 py-3 border border-white/30 shadow-glass">
<div className="flex items-center space-x-2">
<Sparkles className="w-5 h-5 text-yellow-300" />
<span className="text-white font-bold">{currentUser.role}</span>
</div>
</div>
<div className="h-6 w-px bg-white/30"></div>
<span className="text-red-100 font-medium">MonacoUSA Association</span>
</div>
</div>
</div>
{/* Date & Quick Actions */}
<div className="space-y-6 animate-slide-up" style={{ animationDelay: '200ms' }}>
{/* Date Card */}
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-6 border border-white/20 shadow-glass hover:bg-white/15 transition-all duration-300 group">
<div className="flex items-center space-x-3 mb-3">
<Calendar className="w-6 h-6 text-white" />
<p className="text-red-100 font-semibold">Today</p>
</div>
<p className="text-2xl font-bold text-white leading-tight">
{new Date().toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</p>
<div className="mt-3 h-1 bg-white/20 rounded-full overflow-hidden">
<div className="h-full bg-gradient-to-r from-white/60 to-white/80 rounded-full w-3/4 animate-pulse"></div>
</div>
</div>
{/* Quick Actions */}
<div className="flex space-x-3">
<button className="flex-1 bg-white/10 backdrop-blur-md hover:bg-white/20 text-white p-4 rounded-xl border border-white/20 transition-all duration-300 hover:scale-105 group">
<Bell className="w-5 h-5 mx-auto mb-2 group-hover:animate-bounce" />
<span className="text-sm font-medium">Alerts</span>
</button>
<button className="flex-1 bg-white text-red-600 hover:bg-red-50 p-4 rounded-xl transition-all duration-300 hover:scale-105 shadow-lg group">
<Sparkles className="w-5 h-5 mx-auto mb-2 group-hover:animate-spin" />
<span className="text-sm font-bold">Quick Add</span>
</button>
</div>
</div>
</div>
{/* Decorative line */}
<div className="mt-8 h-px bg-gradient-to-r from-transparent via-white/30 to-transparent"></div>
</div>
</div>
{/* Statistics Grid */}
<StatsGrid stats={dashboardStats} />
{/* Dues Management */}
<DuesManagement members={members} />
</div>
);
};

View File

@ -0,0 +1,139 @@
import React, { useState } from 'react';
import { members } from '../../data/mockData';
import { MemberTable } from '../members/MemberTable';
import { Button } from '../ui/Button';
import { Search, Filter, Download, Plus, Users, Sparkles } from 'lucide-react';
export const MemberList: React.FC = () => {
const [searchQuery, setSearchQuery] = useState('');
const [activeFilter, setActiveFilter] = useState<string>('all');
const filterOptions = [
{ id: 'all', label: 'All Members', count: members.length, gradient: 'from-gray-500 to-gray-600' },
{ id: 'active', label: 'Active', count: members.filter(m => m.status === 'Active').length, gradient: 'from-green-500 to-emerald-600' },
{ id: 'inactive', label: 'Inactive', count: members.filter(m => m.status === 'Inactive').length, gradient: 'from-gray-400 to-gray-500' },
{ id: 'overdue', label: 'Overdue', count: members.filter(m => m.duesStatus === 'Overdue').length, gradient: 'from-red-500 to-red-600' },
];
const filteredMembers = members.filter(member => {
const matchesSearch =
`${member.firstName} ${member.lastName}`.toLowerCase().includes(searchQuery.toLowerCase()) ||
member.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
member.id.toLowerCase().includes(searchQuery.toLowerCase());
const matchesFilter =
activeFilter === 'all' ||
(activeFilter === 'active' && member.status === 'Active') ||
(activeFilter === 'inactive' && member.status === 'Inactive') ||
(activeFilter === 'overdue' && member.duesStatus === 'Overdue');
return matchesSearch && matchesFilter;
});
return (
<div className="space-y-8">
{/* Ultra-Modern Header */}
<div className="relative overflow-hidden">
{/* Animated background */}
<div className="absolute inset-0 bg-gradient-to-br from-red-600 via-red-700 to-red-900"></div>
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
{/* Floating elements */}
<div className="absolute top-5 right-10 w-48 h-48 bg-white/5 rounded-full blur-3xl animate-float"></div>
<div className="absolute bottom-5 left-10 w-32 h-32 bg-red-300/10 rounded-full blur-2xl animate-pulse-slow"></div>
<div className="relative p-10 rounded-3xl">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-6 lg:space-y-0">
<div className="space-y-4 animate-slide-in">
<div className="flex items-center space-x-4">
<div className="relative">
<div className="p-4 bg-white/20 backdrop-blur-md rounded-2xl shadow-glass border border-white/30">
<Users className="w-8 h-8 text-white" />
</div>
<div className="absolute -top-1 -right-1 w-6 h-6 bg-gradient-to-r from-yellow-400 to-orange-500 rounded-full flex items-center justify-center">
<Sparkles className="w-3 h-3 text-white" />
</div>
</div>
<div>
<h1 className="text-5xl font-black text-white tracking-tight">Members Directory</h1>
<p className="text-red-100 text-xl font-medium">{filteredMembers.length} members found</p>
</div>
</div>
</div>
<div className="flex items-center space-x-4 animate-slide-up" style={{ animationDelay: '200ms' }}>
<Button variant="glass" className="flex items-center space-x-2">
<Download size={16} />
<span>Export</span>
</Button>
<Button className="flex items-center space-x-2 bg-white text-red-600 hover:bg-red-50 shadow-ultra">
<Plus size={16} />
<span>Add Member</span>
</Button>
</div>
</div>
</div>
</div>
{/* Enhanced Filters and Search */}
<div className="relative group">
<div className="absolute -inset-2 bg-gradient-to-r from-red-600/10 via-red-500/5 to-red-600/10 rounded-3xl blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-700"></div>
<div className="relative bg-white/95 backdrop-blur-md rounded-3xl p-8 shadow-ultra border border-red-100/50">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-6 lg:space-y-0">
{/* Filter Chips */}
<div className="flex flex-wrap gap-3">
{filterOptions.map((option, index) => (
<button
key={option.id}
onClick={() => setActiveFilter(option.id)}
className={`relative inline-flex items-center px-6 py-3 rounded-2xl text-sm font-bold transition-all duration-300 overflow-hidden group/filter ${
activeFilter === option.id
? `bg-gradient-to-r ${option.gradient} text-white shadow-red transform scale-105`
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 border border-gray-200'
}`}
style={{ animationDelay: `${index * 100}ms` }}
>
{activeFilter === option.id && (
<div className="absolute inset-0 -top-px overflow-hidden rounded-2xl">
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent -skew-x-12 -translate-x-full group-hover/filter:animate-shimmer"></div>
</div>
)}
<span className="relative z-10">{option.label}</span>
<div className={`relative z-10 ml-2 px-2 py-1 rounded-full text-xs font-bold ${
activeFilter === option.id ? 'bg-white/20' : 'bg-gray-200 text-gray-800'
}`}>
{option.count}
</div>
</button>
))}
</div>
{/* Search and Filter */}
<div className="flex items-center space-x-4">
<div className="relative group/search">
<Search size={20} className="absolute left-4 top-1/2 transform -translate-y-1/2 text-red-400 group-focus-within/search:text-red-600 transition-colors duration-300" />
<input
type="text"
placeholder="Search members..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-12 pr-4 py-4 w-80 border-2 border-red-200 rounded-2xl focus:ring-2 focus:ring-red-500 focus:border-red-500 outline-none bg-red-50/50 text-red-900 placeholder-red-400 font-medium transition-all duration-300 hover:border-red-300"
/>
</div>
<Button variant="secondary" className="flex items-center space-x-2">
<Filter size={16} />
<span>Advanced</span>
</Button>
</div>
</div>
</div>
</div>
{/* Member Table */}
<MemberTable members={filteredMembers} />
</div>
);
};

View File

@ -0,0 +1,42 @@
import React from 'react';
interface AvatarProps {
src?: string;
alt?: string;
initials?: string;
size?: 'sm' | 'md' | 'lg' | 'xl';
className?: string;
}
const sizeClasses = {
sm: 'w-10 h-10 text-sm',
md: 'w-12 h-12 text-base',
lg: 'w-16 h-16 text-lg',
xl: 'w-20 h-20 text-xl',
};
export const Avatar: React.FC<AvatarProps> = ({
src,
alt,
initials,
size = 'md',
className = ''
}) => {
const baseClasses = `inline-flex items-center justify-center rounded-2xl bg-gradient-to-br from-red-500 via-red-600 to-red-700 text-white font-bold shadow-ultra transition-all duration-300 hover:shadow-neon ${sizeClasses[size]} ${className}`;
if (src) {
return (
<img
src={src}
alt={alt}
className={`${baseClasses} object-cover`}
/>
);
}
return (
<div className={baseClasses}>
<span className="drop-shadow-lg">{initials}</span>
</div>
);
};

View File

@ -0,0 +1,27 @@
import React from 'react';
interface BadgeProps {
children: React.ReactNode;
variant?: 'success' | 'warning' | 'danger' | 'info' | 'secondary';
className?: string;
}
const variants = {
success: 'bg-gradient-to-r from-green-100 to-emerald-100 text-green-800 border border-green-200/50 shadow-sm',
warning: 'bg-gradient-to-r from-amber-100 to-yellow-100 text-amber-800 border border-amber-200/50 shadow-sm',
danger: 'bg-gradient-to-r from-red-100 to-rose-100 text-red-800 border border-red-200/50 shadow-sm',
info: 'bg-gradient-to-r from-red-50 to-red-100 text-red-700 border border-red-200/50 shadow-sm',
secondary: 'bg-gradient-to-r from-gray-100 to-slate-100 text-gray-800 border border-gray-200/50 shadow-sm',
};
export const Badge: React.FC<BadgeProps> = ({
children,
variant = 'secondary',
className = ''
}) => {
return (
<span className={`inline-flex items-center px-3 py-1.5 rounded-full text-xs font-bold backdrop-blur-sm transition-all duration-200 hover:scale-105 ${variants[variant]} ${className}`}>
{children}
</span>
);
};

View File

@ -0,0 +1,45 @@
import React from 'react';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger' | 'glass';
size?: 'sm' | 'md' | 'lg';
children: React.ReactNode;
}
const variants = {
primary: 'relative bg-gradient-to-r from-red-600 to-red-700 text-white hover:from-red-700 hover:to-red-800 focus:ring-red-500 shadow-red transform hover:scale-105 hover:shadow-neon border border-red-500/20 overflow-hidden group',
secondary: 'bg-white/90 backdrop-blur-md text-red-700 border-2 border-red-200 hover:bg-white hover:border-red-300 focus:ring-red-500 shadow-soft hover:shadow-red transform hover:scale-105',
danger: 'bg-gradient-to-r from-red-600 to-red-700 text-white hover:from-red-700 hover:to-red-800 focus:ring-red-500 shadow-red hover:shadow-neon transform hover:scale-105',
glass: 'bg-white/10 backdrop-blur-md text-white border border-white/20 hover:bg-white/20 hover:border-white/30 shadow-glass hover:shadow-ultra transform hover:scale-105',
};
const sizes = {
sm: 'px-4 py-2 text-sm',
md: 'px-6 py-3 text-sm',
lg: 'px-8 py-4 text-base',
};
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
children,
className = '',
...props
}) => {
return (
<button
className={`inline-flex items-center justify-center rounded-xl font-semibold transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 ${variants[variant]} ${sizes[size]} ${className}`}
{...props}
>
{variant === 'primary' && (
<>
<div className="absolute inset-0 bg-gradient-to-r from-white/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-xl"></div>
<div className="absolute inset-0 -top-px overflow-hidden rounded-xl">
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent -skew-x-12 -translate-x-full group-hover:animate-shimmer"></div>
</div>
</>
)}
<span className="relative z-10">{children}</span>
</button>
);
};

View File

@ -0,0 +1,27 @@
import React from 'react';
interface CardProps {
children: React.ReactNode;
className?: string;
hover?: boolean;
glass?: boolean;
}
export const Card: React.FC<CardProps> = ({
children,
className = '',
hover = false,
glass = false
}) => {
const baseClasses = 'rounded-lg border border-slate-200';
const hoverClasses = hover ? 'transition-all duration-200 hover:shadow-md hover:border-slate-300' : '';
const glassClasses = glass
? 'bg-white/80 backdrop-blur-sm border-white/20 shadow-lg'
: 'bg-white shadow-sm';
return (
<div className={`${baseClasses} ${hoverClasses} ${glassClasses} ${className}`}>
{children}
</div>
);
};

View File

@ -0,0 +1,31 @@
import React from 'react';
interface CountryFlagProps {
code: string;
className?: string;
}
const countryNames: Record<string, string> = {
US: 'United States',
GB: 'United Kingdom',
FR: 'France',
IT: 'Italy',
ES: 'Spain',
DE: 'Germany',
CA: 'Canada',
BR: 'Brazil',
MC: 'Monaco',
};
export const CountryFlag: React.FC<CountryFlagProps> = ({ code, className = '' }) => {
const countryName = countryNames[code] || code;
return (
<span
className={`inline-block w-6 h-4 rounded-sm bg-gradient-to-r from-blue-500 to-red-500 text-white text-xs leading-4 text-center ${className}`}
title={countryName}
>
{code}
</span>
);
};

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,10 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);

View File

@ -0,0 +1,33 @@
export interface Member {
id: string;
firstName: string;
lastName: string;
email: string;
phone?: string;
avatar?: string;
nationality: string[];
status: 'Active' | 'Inactive' | 'Pending';
duesStatus: 'Paid' | 'Due Soon' | 'Overdue';
memberType: 'Regular' | 'Premium' | 'Life' | 'Honorary';
joinDate: string;
lastPaymentDate?: string;
dueAmount: number;
daysOverdue?: number;
daysTillDue?: number;
}
export interface DashboardStats {
totalMembers: number;
activeMembers: number;
pendingDues: number;
upcomingEvents: number;
memberTrend: number;
}
export interface User {
id: string;
firstName: string;
lastName: string;
role: 'Board Member' | 'Admin';
avatar?: string;
}

View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,104 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
colors: {
monaco: {
50: '#FEF2F2',
100: '#FEE2E2',
200: '#FECACA',
300: '#FCA5A5',
400: '#F87171',
500: '#EF4444',
600: '#DC2626',
700: '#B91C1C',
800: '#991B1B',
900: '#7F1D1D',
},
red: {
50: '#FEF2F2',
100: '#FEE2E2',
200: '#FECACA',
300: '#FCA5A5',
400: '#F87171',
500: '#EF4444',
600: '#DC2626',
700: '#B91C1C',
800: '#991B1B',
900: '#7F1D1D',
},
},
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
'red-gradient': 'linear-gradient(135deg, #DC2626 0%, #B91C1C 100%)',
'red-gradient-soft': 'linear-gradient(135deg, #FEF2F2 0%, #FEE2E2 100%)',
'glass-gradient': 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
'mesh-gradient': 'radial-gradient(at 40% 20%, hsla(28,100%,74%,1) 0px, transparent 50%), radial-gradient(at 80% 0%, hsla(189,100%,56%,1) 0px, transparent 50%), radial-gradient(at 0% 50%, hsla(355,100%,93%,1) 0px, transparent 50%), radial-gradient(at 80% 50%, hsla(340,100%,76%,1) 0px, transparent 50%), radial-gradient(at 0% 100%, hsla(22,100%,77%,1) 0px, transparent 50%), radial-gradient(at 80% 100%, hsla(242,100%,70%,1) 0px, transparent 50%), radial-gradient(at 0% 0%, hsla(343,100%,76%,1) 0px, transparent 50%)',
'aurora': 'linear-gradient(45deg, #ff6b6b, #ee5a24, #ff9ff3, #54a0ff, #5f27cd)',
'cyber-red': 'linear-gradient(135deg, #ff0844 0%, #ffb199 100%)',
'neon-glow': 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
},
boxShadow: {
'red': '0 4px 14px 0 rgba(220, 38, 38, 0.15)',
'red-lg': '0 10px 25px -3px rgba(220, 38, 38, 0.1), 0 4px 6px -2px rgba(220, 38, 38, 0.05)',
'soft': '0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04)',
'glass': '0 8px 32px 0 rgba(31, 38, 135, 0.37)',
'glow': '0 0 20px rgba(220, 38, 38, 0.3)',
'neon': '0 0 5px theme(colors.red.400), 0 0 20px theme(colors.red.400), 0 0 35px theme(colors.red.400)',
'ultra': '0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.05)',
'floating': '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
},
backdropBlur: {
xs: '2px',
},
animation: {
'float': 'float 6s ease-in-out infinite',
'pulse-slow': 'pulse 4s cubic-bezier(0.4, 0, 0.6, 1) infinite',
'bounce-slow': 'bounce 3s infinite',
'spin-slow': 'spin 8s linear infinite',
'gradient': 'gradient 15s ease infinite',
'glow': 'glow 2s ease-in-out infinite alternate',
'shimmer': 'shimmer 2.5s linear infinite',
'slide-up': 'slideUp 0.5s ease-out',
'slide-in': 'slideIn 0.3s ease-out',
'scale-in': 'scaleIn 0.2s ease-out',
},
keyframes: {
float: {
'0%, 100%': { transform: 'translateY(0px)' },
'50%': { transform: 'translateY(-20px)' },
},
gradient: {
'0%, 100%': { backgroundPosition: '0% 50%' },
'50%': { backgroundPosition: '100% 50%' },
},
glow: {
'0%': { boxShadow: '0 0 20px rgba(220, 38, 38, 0.3)' },
'100%': { boxShadow: '0 0 30px rgba(220, 38, 38, 0.6)' },
},
shimmer: {
'0%': { transform: 'translateX(-100%)' },
'100%': { transform: 'translateX(100%)' },
},
slideUp: {
'0%': { transform: 'translateY(20px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
slideIn: {
'0%': { transform: 'translateX(-20px)', opacity: '0' },
'100%': { transform: 'translateX(0)', opacity: '1' },
},
scaleIn: {
'0%': { transform: 'scale(0.9)', opacity: '0' },
'100%': { transform: 'scale(1)', opacity: '1' },
},
},
},
},
plugins: [],
};

View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
optimizeDeps: {
exclude: ['lucide-react'],
},
});

321
DESIGN-SYSTEM.md Normal file
View File

@ -0,0 +1,321 @@
# MonacoUSA Portal Design System - Glass Bolt Theme
## Overview
The MonacoUSA Portal uses a sophisticated glassmorphic design system inspired by bolt.ai's subtle and professional aesthetic. This design language emphasizes clarity, hierarchy, and modern visual appeal while maintaining excellent readability and performance.
## Core Design Principles
### 1. Subtle Glassmorphism
- **Primary Glass Effect**: `rgba(255, 255, 255, 0.6)` background with 4px blur
- **Ultra Glass Variant**: `rgba(255, 255, 255, 0.8)` for higher contrast areas
- **Minimal Blur**: 2-4px backdrop-filter for performance
- **Light Borders**: `rgba(0, 0, 0, 0.05)` for subtle definition
- **Soft Shadows**: `0 4px 12px rgba(0, 0, 0, 0.08)` for depth
### 2. Color Palette
#### Monaco Red Spectrum
```scss
$monaco-red-50: #fef2f2;
$monaco-red-100: #fee2e2;
$monaco-red-200: #fecaca;
$monaco-red-300: #fca5a5;
$monaco-red-400: #f87171;
$monaco-red-500: #ef4444;
$monaco-red-600: #dc2626; // Primary Brand Color
$monaco-red-700: #b91c1c;
$monaco-red-800: #991b1b;
$monaco-red-900: #7f1d1d;
```
#### Neutral Palette
```scss
$gray-50: #fafafa;
$gray-100: #f4f4f5;
$gray-200: #e4e4e7;
$gray-300: #d4d4d8;
$gray-400: #a1a1aa;
$gray-500: #71717a;
$gray-600: #52525b;
$gray-700: #3f3f46;
$gray-800: #27272a;
$gray-900: #18181b;
```
### 3. Typography
- **Font Family**: Inter, system-ui, sans-serif
- **Headings**: Bold weight, slight letter-spacing
- **Body Text**: Regular weight, optimized line-height
- **Text Colors**:
- Primary: `rgba(0, 0, 0, 0.87)`
- Secondary: `rgba(0, 0, 0, 0.6)`
- Disabled: `rgba(0, 0, 0, 0.38)`
### 4. Component Patterns
#### Glass Cards
```scss
.glass-card {
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
border: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border-radius: 16px;
padding: 1.5rem;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
transform: translateY(-2px);
}
}
```
#### Stat Cards
```scss
.stat-card {
@extend .glass-card;
.stat-icon {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(220, 38, 38, 0.08);
border-radius: 12px;
color: $monaco-red-600;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: rgba(0, 0, 0, 0.87);
}
.stat-label {
font-size: 0.875rem;
color: rgba(0, 0, 0, 0.6);
text-transform: uppercase;
letter-spacing: 0.05em;
}
}
```
#### Navigation Sidebar
```scss
.glass-sidebar {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border-right: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 4px 0 12px rgba(0, 0, 0, 0.05);
.nav-item {
padding: 0.75rem 1rem;
border-radius: 8px;
transition: all 0.2s ease;
&:hover {
background: rgba(220, 38, 38, 0.05);
}
&.active {
background: rgba(220, 38, 38, 0.1);
color: $monaco-red-600;
font-weight: 600;
}
}
}
```
#### Forms & Inputs
```scss
.glass-input {
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 8px;
padding: 0.75rem 1rem;
transition: all 0.2s ease;
&:focus {
outline: none;
border-color: $monaco-red-600;
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1);
}
}
```
#### Buttons
```scss
.btn-glass {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(4px);
border: 1px solid rgba(0, 0, 0, 0.1);
padding: 0.75rem 1.5rem;
border-radius: 8px;
font-weight: 600;
transition: all 0.2s ease;
&:hover {
background: rgba(255, 255, 255, 1);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
&.btn-primary {
background: rgba(220, 38, 38, 0.9);
color: white;
border-color: transparent;
&:hover {
background: rgba(220, 38, 38, 1);
}
}
}
```
### 5. Layout Patterns
#### Dashboard Grid
- **Desktop**: 4 columns for stat cards
- **Tablet**: 2 columns
- **Mobile**: Single column
- **Gap**: 1.5rem between cards
#### Page Structure
```
┌─────────────────────────────────────┐
│ Sidebar │ Main Content │
│ 250px │ Flexible │
│ │ ┌───────────────────────┐ │
│ │ │ Header Section │ │
│ │ ├───────────────────────┤ │
│ │ │ Stats Grid │ │
│ │ ├───────────────────────┤ │
│ │ │ Content Cards │ │
│ │ └───────────────────────┘ │
└─────────────────────────────────────┘
```
### 6. Animation & Transitions
#### Hover Effects
- **Lift**: `transform: translateY(-2px)`
- **Shadow Enhancement**: Increase shadow opacity/blur
- **Duration**: 200-300ms
- **Easing**: `cubic-bezier(0.4, 0, 0.2, 1)`
#### Page Transitions
- **Fade In**: 300ms ease-out
- **Slide Up**: 400ms ease-out with 20px offset
### 7. Responsive Breakpoints
```scss
$breakpoint-xs: 320px; // Small phones
$breakpoint-sm: 640px; // Phones
$breakpoint-md: 768px; // Tablets
$breakpoint-lg: 1024px; // Desktop
$breakpoint-xl: 1280px; // Large desktop
$breakpoint-2xl: 1536px; // Extra large desktop
```
### 8. Accessibility
#### Contrast Requirements
- **Text on Glass**: Minimum 4.5:1 contrast ratio
- **Interactive Elements**: 3:1 contrast ratio
- **Focus Indicators**: Visible outline with 3px offset
#### Focus States
```scss
*:focus-visible {
outline: 2px solid $monaco-red-600;
outline-offset: 3px;
border-radius: 4px;
}
```
### 9. Performance Optimizations
#### Blur Performance
- Limit backdrop-filter to essential elements
- Use will-change sparingly
- Prefer transform over position changes
- Group glass elements to reduce paint areas
#### CSS Variables for Dynamic Theming
```css
:root {
--glass-bg: rgba(255, 255, 255, 0.6);
--glass-blur: 4px;
--glass-border: rgba(0, 0, 0, 0.05);
--glass-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
```
### 10. Implementation Guidelines
#### Portal-Specific Styles
**Admin Portal**
- Full glass sidebar with navigation
- Complex data tables with glass headers
- System monitoring cards
- Advanced form controls
**Board Portal**
- Executive dashboard layout
- Meeting management cards
- Document viewers with glass frames
- Governance tools
**Member Portal**
- Simplified navigation
- Personal dashboard
- Event registration forms
- Resource cards
**Authentication Pages**
- Centered glass card layout
- Minimal distractions
- Clear call-to-action buttons
- Subtle branding elements
### 11. Best Practices
1. **Consistency**: Use predefined glass classes, don't create variations
2. **Performance**: Test blur effects on lower-end devices
3. **Accessibility**: Always ensure sufficient contrast
4. **Responsiveness**: Test all breakpoints thoroughly
5. **Browser Support**: Provide fallbacks for browsers without backdrop-filter
### 12. Migration Checklist
When updating existing pages to the Glass Bolt theme:
- [ ] Replace solid backgrounds with glass effects
- [ ] Update color scheme to use Monaco red accents
- [ ] Apply consistent border-radius (8-16px)
- [ ] Add hover states with lift effect
- [ ] Ensure proper spacing (1.5rem standard gap)
- [ ] Test backdrop-filter browser support
- [ ] Verify contrast ratios meet WCAG standards
- [ ] Update button styles to glass variants
- [ ] Apply glass effect to navigation elements
- [ ] Test responsive behavior on all breakpoints
## Version History
- **v2.0.0** (Current) - Glass Bolt Theme
- Subtle glassmorphism inspired by bolt.ai
- Improved performance with minimal blur
- Enhanced accessibility with better contrast
- Consistent design language across all portals
- **v1.0.0** - Original design system
- Basic Material Design implementation
- Monaco red color scheme
- Standard component library

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,406 +0,0 @@
<template>
<div class="auth-page">
<div class="auth-container auth-container--small">
<div
v-motion
:initial="{ opacity: 0, y: 20 }"
:enter="{ opacity: 1, y: 0 }"
class="auth-content"
>
<!-- Logo -->
<div class="auth-logo">
<img src="/MONACOUSA-Flags_376x376.png" alt="MonacoUSA" class="logo" />
<h1>MonacoUSA Portal</h1>
</div>
<!-- Step 1: Request Reset -->
<div v-if="!emailSent" class="reset-step">
<div class="auth-header">
<Icon name="lock" class="auth-header__icon" />
<h2>Forgot Your Password?</h2>
<p>No worries! Enter your email and we'll send you reset instructions.</p>
</div>
<form class="auth-form" @submit.prevent="handleResetRequest">
<FloatingInput
v-model="email"
label="Email Address"
type="email"
variant="glass"
leftIcon="mail"
helperText="Enter the email associated with your account"
:error="error"
required
/>
<MonacoButton
type="submit"
variant="primary"
size="lg"
block
:loading="loading"
>
Send Reset Instructions
</MonacoButton>
<MonacoButton
variant="ghost"
size="lg"
block
@click="goBack"
>
Back to Login
</MonacoButton>
</form>
</div>
<!-- Step 2: Email Sent Confirmation -->
<div v-else class="success-step">
<div class="success-icon">
<Icon name="mail" />
</div>
<div class="auth-header">
<h2>Check Your Email</h2>
<p>We've sent password reset instructions to:</p>
<p class="email-display">{{ email }}</p>
</div>
<div class="instructions">
<h3>What's next?</h3>
<ol>
<li>Check your email inbox (and spam folder)</li>
<li>Click the reset link in the email</li>
<li>Create your new password</li>
</ol>
</div>
<div class="resend-section">
<p>Didn't receive the email?</p>
<button
class="resend-button"
@click="handleResend"
:disabled="resendCooldown > 0"
>
{{ resendCooldown > 0 ? `Resend in ${resendCooldown}s` : 'Resend Email' }}
</button>
</div>
<MonacoButton
variant="primary"
size="lg"
block
@click="goBack"
>
Return to Login
</MonacoButton>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onUnmounted } from 'vue'
import FloatingInput from '~/components/ui/FloatingInput.vue'
import MonacoButton from '~/components/ui/MonacoButton.vue'
import Icon from '~/components/ui/Icon.vue'
const email = ref('')
const error = ref('')
const loading = ref(false)
const emailSent = ref(false)
const resendCooldown = ref(0)
let cooldownInterval: number | null = null
const handleResetRequest = async () => {
error.value = ''
loading.value = true
// Simulate API call
setTimeout(() => {
loading.value = false
emailSent.value = true
startResendCooldown()
}, 2000)
}
const handleResend = () => {
if (resendCooldown.value > 0) return
// Simulate resending email
console.log('Resending to:', email.value)
startResendCooldown()
}
const startResendCooldown = () => {
resendCooldown.value = 60
if (cooldownInterval) {
clearInterval(cooldownInterval)
}
cooldownInterval = setInterval(() => {
resendCooldown.value--
if (resendCooldown.value <= 0 && cooldownInterval) {
clearInterval(cooldownInterval)
cooldownInterval = null
}
}, 1000)
}
const goBack = () => {
window.location.href = '/auth/login'
}
onUnmounted(() => {
if (cooldownInterval) {
clearInterval(cooldownInterval)
}
})
</script>
<style scoped lang="scss">
.auth-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #fef2f2 0%, #ffffff 100%);
padding: 2rem;
position: relative;
overflow: hidden;
// Background decoration
&::before,
&::after {
content: '';
position: absolute;
border-radius: 50%;
background: linear-gradient(135deg,
rgba(220, 38, 38, 0.05) 0%,
rgba(220, 38, 38, 0.02) 100%);
}
&::before {
width: 500px;
height: 500px;
top: -250px;
left: -250px;
}
&::after {
width: 400px;
height: 400px;
bottom: -200px;
right: -200px;
}
}
.auth-container {
position: relative;
max-width: 500px;
width: 100%;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 24px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
overflow: hidden;
z-index: 1;
&--small {
max-width: 450px;
}
}
.auth-content {
padding: 3rem;
}
.auth-logo {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
margin-bottom: 2rem;
img {
width: 48px;
height: 48px;
}
h1 {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
color: #dc2626;
}
}
.auth-header {
text-align: center;
margin-bottom: 2rem;
&__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
margin-bottom: 1rem;
background: linear-gradient(135deg,
rgba(220, 38, 38, 0.1) 0%,
rgba(220, 38, 38, 0.05) 100%);
border-radius: 16px;
color: #dc2626;
svg {
width: 32px;
height: 32px;
}
}
h2 {
margin: 0 0 0.75rem;
font-size: 1.75rem;
font-weight: 700;
color: #27272a;
}
p {
margin: 0;
color: #6b7280;
line-height: 1.5;
}
}
.auth-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.success-step {
text-align: center;
}
.success-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
margin-bottom: 1.5rem;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
border-radius: 50%;
color: white;
animation: successPulse 2s ease-in-out infinite;
svg {
width: 40px;
height: 40px;
}
}
@keyframes successPulse {
0%, 100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4);
}
50% {
transform: scale(1.05);
box-shadow: 0 0 0 20px rgba(16, 185, 129, 0);
}
}
.email-display {
margin-top: 0.5rem;
padding: 0.75rem;
background: rgba(220, 38, 38, 0.05);
border-radius: 8px;
font-weight: 600;
color: #dc2626;
}
.instructions {
margin: 2rem 0;
padding: 1.5rem;
background: rgba(107, 114, 128, 0.05);
border-radius: 12px;
text-align: left;
h3 {
margin: 0 0 1rem;
font-size: 1rem;
font-weight: 600;
color: #27272a;
}
ol {
margin: 0;
padding-left: 1.5rem;
li {
margin-bottom: 0.5rem;
color: #6b7280;
font-size: 0.875rem;
&:last-child {
margin-bottom: 0;
}
}
}
}
.resend-section {
margin: 2rem 0;
padding: 1.5rem;
background: linear-gradient(135deg,
rgba(220, 38, 38, 0.03) 0%,
rgba(220, 38, 38, 0.01) 100%);
border-radius: 12px;
p {
margin: 0 0 0.75rem;
font-size: 0.875rem;
color: #6b7280;
}
}
.resend-button {
padding: 0.5rem 1rem;
background: none;
border: 2px solid #dc2626;
border-radius: 8px;
color: #dc2626;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
&:hover:not(:disabled) {
background: #dc2626;
color: white;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
// Responsive
@media (max-width: 640px) {
.auth-content {
padding: 2rem;
}
.auth-logo {
flex-direction: column;
h1 {
font-size: 1.25rem;
}
}
}
</style>

View File

@ -1,466 +0,0 @@
<template>
<div class="auth-page">
<div class="auth-container">
<!-- Left Panel - Form -->
<div
v-motion
:initial="{ opacity: 0, x: -50 }"
:enter="{ opacity: 1, x: 0 }"
class="auth-panel auth-panel--form"
>
<div class="auth-logo">
<img src="/MONACOUSA-Flags_376x376.png" alt="MonacoUSA" class="logo" />
<h1>MonacoUSA Portal</h1>
</div>
<div class="auth-header">
<h2>Welcome Back</h2>
<p>Sign in to access your Monaco community</p>
</div>
<form class="auth-form" @submit.prevent="handleLogin">
<FloatingInput
v-model="form.email"
label="Email Address"
type="email"
variant="glass"
leftIcon="mail"
:error="errors.email"
required
/>
<FloatingInput
v-model="form.password"
label="Password"
type="password"
variant="glass"
leftIcon="lock"
:error="errors.password"
required
/>
<div class="auth-options">
<label class="checkbox-label">
<input type="checkbox" v-model="form.remember" />
<span>Remember me</span>
</label>
<a href="/auth/forgot-password" class="link">Forgot password?</a>
</div>
<MonacoButton
type="submit"
variant="primary"
size="lg"
block
:loading="loading"
>
Sign In
</MonacoButton>
<div class="auth-divider">
<span>or continue with</span>
</div>
<div class="social-buttons">
<button type="button" class="social-button">
<Icon name="globe" />
<span>Google</span>
</button>
<button type="button" class="social-button">
<Icon name="briefcase" />
<span>LinkedIn</span>
</button>
</div>
</form>
<div class="auth-footer">
<p>Don't have an account? <a href="/auth/signup" class="link">Sign up</a></p>
</div>
</div>
<!-- Right Panel - Visual -->
<div
v-motion
:initial="{ opacity: 0, x: 50 }"
:enter="{ opacity: 1, x: 0, transition: { delay: 200 } }"
class="auth-panel auth-panel--visual"
>
<div class="visual-content">
<div class="visual-gradient"></div>
<div class="visual-pattern"></div>
<div class="visual-text">
<h3>Connect with Monaco's Elite Business Community</h3>
<p>Join exclusive events, network with leaders, and grow your business in the heart of luxury and innovation.</p>
<div class="stats">
<div class="stat">
<span class="stat__value">500+</span>
<span class="stat__label">Members</span>
</div>
<div class="stat">
<span class="stat__value">50+</span>
<span class="stat__label">Events/Year</span>
</div>
<div class="stat">
<span class="stat__value">25+</span>
<span class="stat__label">Countries</span>
</div>
</div>
</div>
<div class="visual-decoration">
<div class="decoration-circle decoration-circle--1"></div>
<div class="decoration-circle decoration-circle--2"></div>
<div class="decoration-circle decoration-circle--3"></div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import FloatingInput from '~/components/ui/FloatingInput.vue'
import MonacoButton from '~/components/ui/MonacoButton.vue'
import Icon from '~/components/ui/Icon.vue'
const form = ref({
email: '',
password: '',
remember: false
})
const errors = ref({
email: '',
password: ''
})
const loading = ref(false)
const handleLogin = async () => {
loading.value = true
errors.value = { email: '', password: '' }
// Simulate API call
setTimeout(() => {
loading.value = false
console.log('Login with:', form.value)
}, 2000)
}
</script>
<style scoped lang="scss">
.auth-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #fef2f2 0%, #ffffff 100%);
padding: 2rem;
}
.auth-container {
display: grid;
grid-template-columns: 1fr 1fr;
max-width: 1200px;
width: 100%;
min-height: 700px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(20px);
border-radius: 24px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.auth-panel {
padding: 3rem;
display: flex;
flex-direction: column;
&--form {
justify-content: center;
}
&--visual {
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
position: relative;
overflow: hidden;
}
}
.auth-logo {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 3rem;
img {
width: 48px;
height: 48px;
}
h1 {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
color: #dc2626;
}
}
.auth-header {
margin-bottom: 2rem;
h2 {
margin: 0 0 0.5rem;
font-size: 2rem;
font-weight: 700;
color: #27272a;
}
p {
margin: 0;
color: #6b7280;
}
}
.auth-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.auth-options {
display: flex;
justify-content: space-between;
align-items: center;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
input[type="checkbox"] {
width: 1.25rem;
height: 1.25rem;
accent-color: #dc2626;
cursor: pointer;
}
span {
font-size: 0.875rem;
color: #6b7280;
}
}
.link {
color: #dc2626;
text-decoration: none;
font-size: 0.875rem;
font-weight: 500;
transition: opacity 0.2s;
&:hover {
opacity: 0.8;
text-decoration: underline;
}
}
.auth-divider {
position: relative;
text-align: center;
margin: 1rem 0;
span {
position: relative;
padding: 0 1rem;
background: white;
color: #a3a3a3;
font-size: 0.875rem;
}
&::before {
content: '';
position: absolute;
left: 0;
right: 0;
top: 50%;
height: 1px;
background: #e5e5e5;
}
}
.social-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.social-button {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem;
background: white;
border: 2px solid #e5e5e5;
border-radius: 12px;
font-size: 0.875rem;
font-weight: 500;
color: #27272a;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #f5f5f5;
border-color: #dc2626;
transform: translateY(-2px);
}
}
.auth-footer {
margin-top: 2rem;
text-align: center;
p {
margin: 0;
font-size: 0.875rem;
color: #6b7280;
}
}
.visual-content {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
color: white;
z-index: 1;
}
.visual-gradient {
position: absolute;
inset: 0;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.1) 0%,
transparent 100%
);
z-index: 1;
}
.visual-pattern {
position: absolute;
inset: 0;
opacity: 0.1;
background-image: repeating-linear-gradient(
45deg,
transparent,
transparent 35px,
rgba(255, 255, 255, 0.1) 35px,
rgba(255, 255, 255, 0.1) 70px
);
z-index: 0;
}
.visual-text {
position: relative;
z-index: 2;
h3 {
margin: 0 0 1rem;
font-size: 2rem;
font-weight: 700;
}
p {
margin: 0 0 3rem;
font-size: 1.125rem;
opacity: 0.95;
line-height: 1.6;
}
}
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
}
.stat {
display: flex;
flex-direction: column;
&__value {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 0.25rem;
}
&__label {
font-size: 0.875rem;
opacity: 0.9;
}
}
.visual-decoration {
position: absolute;
inset: 0;
z-index: 0;
overflow: hidden;
}
.decoration-circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.05);
&--1 {
width: 300px;
height: 300px;
top: -150px;
right: -150px;
}
&--2 {
width: 200px;
height: 200px;
bottom: -100px;
left: -100px;
}
&--3 {
width: 150px;
height: 150px;
top: 50%;
right: 10%;
transform: translateY(-50%);
}
}
// Responsive
@media (max-width: 1024px) {
.auth-container {
grid-template-columns: 1fr;
max-width: 500px;
}
.auth-panel--visual {
display: none;
}
}
@media (max-width: 640px) {
.auth-panel {
padding: 2rem;
}
.social-buttons {
grid-template-columns: 1fr;
}
}
</style>

View File

@ -1,747 +0,0 @@
<template>
<div class="auth-page">
<div class="auth-container auth-container--wide">
<!-- Progress Bar -->
<div class="progress-bar">
<div
class="progress-bar__fill"
:style="{ width: `${(step / 3) * 100}%` }"
></div>
</div>
<!-- Step 1: Account Info -->
<div
v-if="step === 1"
v-motion
:initial="{ opacity: 0, x: 50 }"
:enter="{ opacity: 1, x: 0 }"
class="signup-step"
>
<div class="auth-logo">
<img src="/MONACOUSA-Flags_376x376.png" alt="MonacoUSA" class="logo" />
<h1>MonacoUSA Portal</h1>
</div>
<div class="auth-header">
<h2>Create Your Account</h2>
<p>Join Monaco's premier business community</p>
</div>
<form class="auth-form" @submit.prevent="nextStep">
<div class="form-row">
<FloatingInput
v-model="form.firstName"
label="First Name"
variant="glass"
leftIcon="user"
required
/>
<FloatingInput
v-model="form.lastName"
label="Last Name"
variant="glass"
leftIcon="user"
required
/>
</div>
<FloatingInput
v-model="form.email"
label="Email Address"
type="email"
variant="glass"
leftIcon="mail"
helperText="We'll use this for account notifications"
required
/>
<FloatingInput
v-model="form.password"
label="Password"
type="password"
variant="glass"
leftIcon="lock"
helperText="Minimum 8 characters with uppercase and number"
required
/>
<FloatingInput
v-model="form.confirmPassword"
label="Confirm Password"
type="password"
variant="glass"
leftIcon="lock"
:error="passwordError"
required
/>
<div class="password-strength">
<span class="password-strength__label">Password Strength:</span>
<div class="password-strength__bars">
<span
v-for="i in 4"
:key="i"
class="password-strength__bar"
:class="{ 'password-strength__bar--filled': i <= passwordStrength }"
></span>
</div>
<span class="password-strength__text">{{ passwordStrengthText }}</span>
</div>
<MonacoButton
type="submit"
variant="primary"
size="lg"
block
>
Continue to Profile
</MonacoButton>
</form>
<div class="auth-footer">
<p>Already have an account? <a href="/auth/login" class="link">Sign in</a></p>
</div>
</div>
<!-- Step 2: Profile Info -->
<div
v-if="step === 2"
v-motion
:initial="{ opacity: 0, x: 50 }"
:enter="{ opacity: 1, x: 0 }"
class="signup-step"
>
<div class="step-header">
<button @click="previousStep" class="back-button">
<Icon name="arrow-left" />
Back
</button>
<h2>Professional Information</h2>
</div>
<form class="auth-form" @submit.prevent="nextStep">
<FloatingInput
v-model="form.company"
label="Company Name"
variant="glass"
leftIcon="building"
required
/>
<FloatingInput
v-model="form.title"
label="Job Title"
variant="glass"
leftIcon="briefcase"
required
/>
<div class="form-row">
<FloatingInput
v-model="form.phone"
label="Phone Number"
type="tel"
variant="glass"
leftIcon="phone"
required
/>
<FloatingInput
v-model="form.linkedin"
label="LinkedIn Profile"
variant="glass"
leftIcon="link"
helperText="Optional"
/>
</div>
<div class="form-group">
<label class="form-label">Industry</label>
<select v-model="form.industry" class="form-select">
<option value="">Select your industry</option>
<option value="finance">Finance & Banking</option>
<option value="tech">Technology</option>
<option value="realestate">Real Estate</option>
<option value="hospitality">Hospitality</option>
<option value="retail">Retail & Luxury</option>
<option value="consulting">Consulting</option>
<option value="legal">Legal Services</option>
<option value="other">Other</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Bio</label>
<textarea
v-model="form.bio"
class="form-textarea"
placeholder="Tell us about yourself and your business interests..."
rows="4"
></textarea>
</div>
<MonacoButton
type="submit"
variant="primary"
size="lg"
block
>
Continue to Membership
</MonacoButton>
</form>
</div>
<!-- Step 3: Membership -->
<div
v-if="step === 3"
v-motion
:initial="{ opacity: 0, x: 50 }"
:enter="{ opacity: 1, x: 0 }"
class="signup-step"
>
<div class="step-header">
<button @click="previousStep" class="back-button">
<Icon name="arrow-left" />
Back
</button>
<h2>Choose Your Membership</h2>
</div>
<div class="membership-plans">
<div
v-for="plan in membershipPlans"
:key="plan.id"
class="plan-card"
:class="{ 'plan-card--selected': form.membershipPlan === plan.id }"
@click="form.membershipPlan = plan.id"
>
<div class="plan-card__header">
<h3 class="plan-card__name">{{ plan.name }}</h3>
<span class="plan-card__price">${{ plan.price }}/year</span>
</div>
<ul class="plan-card__features">
<li v-for="feature in plan.features" :key="feature">
<Icon name="check" />
{{ feature }}
</li>
</ul>
<span v-if="plan.popular" class="plan-card__badge">Most Popular</span>
</div>
</div>
<div class="terms-section">
<label class="checkbox-label">
<input type="checkbox" v-model="form.agreeTerms" />
<span>
I agree to the <a href="/terms" class="link">Terms of Service</a>
and <a href="/privacy" class="link">Privacy Policy</a>
</span>
</label>
<label class="checkbox-label">
<input type="checkbox" v-model="form.agreeNewsletter" />
<span>Send me updates about events and opportunities</span>
</label>
</div>
<MonacoButton
variant="primary"
size="lg"
block
:disabled="!form.agreeTerms || !form.membershipPlan"
@click="handleSignup"
>
Complete Registration
</MonacoButton>
</div>
<!-- Success State -->
<div
v-if="step === 4"
v-motion
:initial="{ opacity: 0, scale: 0.9 }"
:enter="{ opacity: 1, scale: 1 }"
class="success-state"
>
<div class="success-icon">🎉</div>
<h2>Welcome to MonacoUSA!</h2>
<p>Your account has been created successfully.</p>
<p>Please check your email to verify your account.</p>
<MonacoButton variant="primary" size="lg" @click="goToLogin">
Go to Login
</MonacoButton>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import FloatingInput from '~/components/ui/FloatingInput.vue'
import MonacoButton from '~/components/ui/MonacoButton.vue'
import Icon from '~/components/ui/Icon.vue'
const step = ref(1)
const form = ref({
// Step 1
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: '',
// Step 2
company: '',
title: '',
phone: '',
linkedin: '',
industry: '',
bio: '',
// Step 3
membershipPlan: '',
agreeTerms: false,
agreeNewsletter: true
})
const membershipPlans = [
{
id: 'basic',
name: 'Basic',
price: 250,
features: [
'Access to member directory',
'Monthly newsletter',
'Event invitations',
'Basic networking features'
]
},
{
id: 'professional',
name: 'Professional',
price: 500,
popular: true,
features: [
'Everything in Basic',
'Priority event registration',
'Enhanced profile features',
'Business matchmaking',
'Quarterly exclusive events'
]
},
{
id: 'executive',
name: 'Executive',
price: 1000,
features: [
'Everything in Professional',
'VIP event access',
'Personal concierge service',
'Board meeting participation',
'Guest passes (5/year)',
'Premium networking tools'
]
}
]
const passwordError = computed(() => {
if (form.value.confirmPassword && form.value.password !== form.value.confirmPassword) {
return 'Passwords do not match'
}
return ''
})
const passwordStrength = computed(() => {
const password = form.value.password
if (!password) return 0
let strength = 0
if (password.length >= 8) strength++
if (/[A-Z]/.test(password)) strength++
if (/[0-9]/.test(password)) strength++
if (/[^A-Za-z0-9]/.test(password)) strength++
return strength
})
const passwordStrengthText = computed(() => {
const texts = ['', 'Weak', 'Fair', 'Good', 'Strong']
return texts[passwordStrength.value]
})
const nextStep = () => {
step.value++
}
const previousStep = () => {
step.value--
}
const handleSignup = () => {
console.log('Signup with:', form.value)
step.value = 4
}
const goToLogin = () => {
window.location.href = '/auth/login'
}
</script>
<style scoped lang="scss">
.auth-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #fef2f2 0%, #ffffff 100%);
padding: 2rem;
}
.auth-container {
max-width: 500px;
width: 100%;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(20px);
border-radius: 24px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
overflow: hidden;
&--wide {
max-width: 800px;
}
}
.progress-bar {
height: 4px;
background: rgba(220, 38, 38, 0.1);
&__fill {
height: 100%;
background: linear-gradient(90deg, #dc2626 0%, #b91c1c 100%);
transition: width 0.3s ease;
}
}
.signup-step {
padding: 3rem;
}
.auth-logo {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 2rem;
img {
width: 48px;
height: 48px;
}
h1 {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
color: #dc2626;
}
}
.auth-header {
margin-bottom: 2rem;
h2 {
margin: 0 0 0.5rem;
font-size: 2rem;
font-weight: 700;
color: #27272a;
}
p {
margin: 0;
color: #6b7280;
}
}
.step-header {
margin-bottom: 2rem;
h2 {
margin: 0.5rem 0 0;
font-size: 1.5rem;
font-weight: 700;
color: #27272a;
}
}
.back-button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0;
background: none;
border: none;
color: #6b7280;
font-size: 0.875rem;
cursor: pointer;
transition: color 0.2s;
&:hover {
color: #dc2626;
}
}
.auth-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-label {
font-size: 0.875rem;
font-weight: 500;
color: #27272a;
}
.form-select,
.form-textarea {
padding: 0.75rem 1rem;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(20px);
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 12px;
font-size: 1rem;
color: #27272a;
transition: all 0.2s;
&:focus {
outline: none;
border-color: #dc2626;
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1);
}
}
.form-textarea {
resize: vertical;
min-height: 100px;
font-family: inherit;
}
.password-strength {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: rgba(220, 38, 38, 0.05);
border-radius: 8px;
&__label {
font-size: 0.875rem;
color: #6b7280;
}
&__bars {
display: flex;
gap: 0.25rem;
flex: 1;
}
&__bar {
height: 4px;
flex: 1;
background: #e5e5e5;
border-radius: 2px;
transition: background 0.3s;
&--filled {
background: #dc2626;
}
}
&__text {
font-size: 0.875rem;
font-weight: 500;
color: #dc2626;
}
}
.membership-plans {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-bottom: 2rem;
}
.plan-card {
position: relative;
padding: 1.5rem;
background: white;
border: 2px solid #e5e5e5;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: #dc2626;
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
&--selected {
border-color: #dc2626;
background: rgba(220, 38, 38, 0.05);
}
&__header {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e5e5e5;
}
&__name {
margin: 0 0 0.5rem;
font-size: 1.125rem;
font-weight: 600;
color: #27272a;
}
&__price {
font-size: 1.5rem;
font-weight: 700;
color: #dc2626;
}
&__features {
list-style: none;
padding: 0;
margin: 0;
li {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
font-size: 0.875rem;
color: #6b7280;
svg {
width: 1rem;
height: 1rem;
color: #10b981;
flex-shrink: 0;
}
}
}
&__badge {
position: absolute;
top: -0.5rem;
right: 1rem;
padding: 0.25rem 0.75rem;
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
color: white;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
}
}
.terms-section {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 2rem;
}
.checkbox-label {
display: flex;
align-items: flex-start;
gap: 0.5rem;
cursor: pointer;
input[type="checkbox"] {
margin-top: 0.125rem;
width: 1.25rem;
height: 1.25rem;
accent-color: #dc2626;
cursor: pointer;
flex-shrink: 0;
}
span {
font-size: 0.875rem;
color: #6b7280;
line-height: 1.5;
}
}
.link {
color: #dc2626;
text-decoration: none;
font-weight: 500;
&:hover {
text-decoration: underline;
}
}
.auth-footer {
margin-top: 2rem;
text-align: center;
p {
margin: 0;
font-size: 0.875rem;
color: #6b7280;
}
}
.success-state {
padding: 4rem;
text-align: center;
.success-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
h2 {
margin: 0 0 1rem;
font-size: 2rem;
font-weight: 700;
color: #27272a;
}
p {
margin: 0 0 0.5rem;
color: #6b7280;
&:last-of-type {
margin-bottom: 2rem;
}
}
}
// Responsive
@media (max-width: 768px) {
.membership-plans {
grid-template-columns: 1fr;
}
.form-row {
grid-template-columns: 1fr;
}
.signup-step {
padding: 2rem;
}
}
</style>

View File

@ -1,886 +0,0 @@
<template>
<div class="board-dashboard-v2">
<!-- Executive Header -->
<div class="executive-header">
<h1 class="dashboard-title">Executive Dashboard</h1>
<p class="dashboard-subtitle">Strategic insights and governance overview</p>
</div>
<!-- KPI Cards with Neumorphic Design -->
<div class="kpi-grid">
<div class="kpi-card neumorphic-card" v-for="kpi in kpis" :key="kpi.id">
<div class="kpi-header">
<div class="kpi-icon-wrapper neumorphic-inset">
<Icon :name="kpi.icon" class="kpi-icon" :style="{ color: kpi.color }" />
</div>
<div class="kpi-trend" :class="kpi.trendType">
<Icon :name="kpi.trendIcon" class="trend-icon" />
<span>{{ kpi.trendValue }}</span>
</div>
</div>
<div class="kpi-content">
<div class="kpi-value">{{ kpi.value }}</div>
<div class="kpi-label">{{ kpi.label }}</div>
<div class="kpi-progress">
<div class="progress-bar neumorphic-inset">
<div class="progress-fill" :style="{ width: kpi.progress + '%', background: kpi.color }"></div>
</div>
<span class="progress-text">{{ kpi.progress }}% of target</span>
</div>
</div>
</div>
</div>
<!-- Strategic Initiatives & Governance -->
<div class="governance-grid">
<!-- Strategic Initiatives -->
<div class="initiative-card neumorphic-card">
<div class="card-header">
<Icon name="mdi:target" class="header-icon" />
<h2>Strategic Initiatives</h2>
<div class="morphing-select-wrapper">
<button class="select-trigger neumorphic-button small" @click="toggleQuarter">
<span>{{ selectedQuarter }}</span>
<Icon name="mdi:chevron-down" class="dropdown-icon" :class="{ 'rotate': showQuarter }" />
</button>
<Transition name="morph">
<div v-if="showQuarter" class="morphing-dropdown">
<div
v-for="quarter in quarters"
:key="quarter"
class="dropdown-option"
@click="selectQuarter(quarter)"
>
{{ quarter }}
</div>
</div>
</Transition>
</div>
</div>
<div class="initiatives-list">
<div v-for="initiative in strategicInitiatives" :key="initiative.id" class="initiative-item">
<div class="initiative-header">
<span class="initiative-name">{{ initiative.name }}</span>
<span class="initiative-status" :class="initiative.status">{{ initiative.statusText }}</span>
</div>
<div class="initiative-progress neumorphic-inset">
<div class="progress-bar-slim">
<div class="progress-fill-slim" :style="{ width: initiative.progress + '%' }"></div>
</div>
</div>
<div class="initiative-meta">
<span class="initiative-owner">Owner: {{ initiative.owner }}</span>
<span class="initiative-deadline">Due: {{ initiative.deadline }}</span>
</div>
</div>
</div>
</div>
<!-- Committee Overview -->
<div class="committee-card neumorphic-card">
<div class="card-header">
<Icon name="mdi:account-group-outline" class="header-icon" />
<h2>Committee Activities</h2>
</div>
<div class="committee-grid">
<div v-for="committee in committees" :key="committee.id" class="committee-item neumorphic-inset">
<div class="committee-header">
<Icon :name="committee.icon" class="committee-icon" :style="{ color: committee.color }" />
<h3>{{ committee.name }}</h3>
</div>
<div class="committee-stats">
<div class="stat">
<span class="stat-value">{{ committee.members }}</span>
<span class="stat-label">Members</span>
</div>
<div class="stat">
<span class="stat-value">{{ committee.meetings }}</span>
<span class="stat-label">Meetings</span>
</div>
</div>
<button class="neumorphic-button small full-width">View Details</button>
</div>
</div>
</div>
</div>
<!-- Financial Overview -->
<div class="financial-section neumorphic-card">
<div class="card-header">
<Icon name="mdi:finance" class="header-icon" />
<h2>Financial Overview</h2>
<div class="time-selector">
<button
v-for="period in timePeriods"
:key="period"
class="time-button neumorphic-button small"
:class="{ 'active': selectedPeriod === period }"
@click="selectedPeriod = period"
>
{{ period }}
</button>
</div>
</div>
<div class="financial-grid">
<div class="revenue-chart">
<h3>Revenue Trend</h3>
<div class="chart-placeholder neumorphic-inset">
<Icon name="mdi:chart-line" class="chart-icon" />
<span>Revenue chart visualization</span>
</div>
</div>
<div class="financial-metrics">
<div v-for="metric in financialMetrics" :key="metric.id" class="metric-item">
<div class="metric-label">{{ metric.label }}</div>
<div class="metric-value" :class="metric.type">{{ metric.value }}</div>
<div class="metric-change">
<Icon :name="metric.changeIcon" class="change-icon" />
<span>{{ metric.change }} from last period</span>
</div>
</div>
</div>
</div>
</div>
<!-- Governance Actions -->
<div class="governance-actions">
<div class="action-card neumorphic-card">
<Icon name="mdi:calendar-check" class="action-icon" />
<h3>Board Meetings</h3>
<p>Schedule and manage board meetings</p>
<button class="neumorphic-button primary">Schedule Meeting</button>
</div>
<div class="action-card neumorphic-card">
<Icon name="mdi:file-document-outline" class="action-icon" />
<h3>Documents</h3>
<p>Access governance documents</p>
<button class="neumorphic-button primary">View Documents</button>
</div>
<div class="action-card neumorphic-card">
<Icon name="mdi:vote" class="action-icon" />
<h3>Resolutions</h3>
<p>Review and vote on resolutions</p>
<button class="neumorphic-button primary">View Resolutions</button>
</div>
<div class="action-card neumorphic-card">
<Icon name="mdi:chart-box-outline" class="action-icon" />
<h3>Reports</h3>
<p>Generate executive reports</p>
<button class="neumorphic-button primary">Generate Report</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
// Define page meta
definePageMeta({
layout: 'board',
middleware: 'auth'
})
// KPIs
const kpis = ref([
{
id: 1,
label: 'Member Growth',
value: '24.8%',
icon: 'mdi:account-multiple-plus',
color: '#10B981',
trendType: 'positive',
trendIcon: 'mdi:trending-up',
trendValue: '+5.2%',
progress: 82
},
{
id: 2,
label: 'Revenue YTD',
value: '$2.4M',
icon: 'mdi:cash-multiple',
color: '#3B82F6',
trendType: 'positive',
trendIcon: 'mdi:trending-up',
trendValue: '+12.3%',
progress: 68
},
{
id: 3,
label: 'Member Retention',
value: '94.5%',
icon: 'mdi:account-heart',
color: '#CC0000',
trendType: 'positive',
trendIcon: 'mdi:trending-up',
trendValue: '+2.1%',
progress: 95
},
{
id: 4,
label: 'NPS Score',
value: '72',
icon: 'mdi:emoticon-happy',
color: '#F59E0B',
trendType: 'neutral',
trendIcon: 'mdi:minus',
trendValue: '0%',
progress: 72
}
])
// Strategic Initiatives
const strategicInitiatives = ref([
{
id: 1,
name: 'Digital Transformation Initiative',
status: 'on-track',
statusText: 'On Track',
progress: 65,
owner: 'John Smith',
deadline: 'Q2 2024'
},
{
id: 2,
name: 'Member Experience Enhancement',
status: 'ahead',
statusText: 'Ahead',
progress: 78,
owner: 'Sarah Johnson',
deadline: 'Q1 2024'
},
{
id: 3,
name: 'International Expansion',
status: 'at-risk',
statusText: 'At Risk',
progress: 42,
owner: 'Mike Chen',
deadline: 'Q3 2024'
}
])
// Committees
const committees = ref([
{
id: 1,
name: 'Finance',
icon: 'mdi:calculator',
color: '#3B82F6',
members: 7,
meetings: 12
},
{
id: 2,
name: 'Governance',
icon: 'mdi:gavel',
color: '#CC0000',
members: 5,
meetings: 8
},
{
id: 3,
name: 'Audit',
icon: 'mdi:magnify',
color: '#F59E0B',
members: 4,
meetings: 10
},
{
id: 4,
name: 'Compensation',
icon: 'mdi:cash',
color: '#10B981',
members: 6,
meetings: 6
}
])
// Financial Metrics
const financialMetrics = ref([
{
id: 1,
label: 'Total Revenue',
value: '$2.4M',
type: 'positive',
changeIcon: 'mdi:arrow-up',
change: '+12.3%'
},
{
id: 2,
label: 'Operating Expenses',
value: '$1.8M',
type: 'neutral',
changeIcon: 'mdi:arrow-up',
change: '+8.1%'
},
{
id: 3,
label: 'Net Profit',
value: '$620K',
type: 'positive',
changeIcon: 'mdi:arrow-up',
change: '+24.5%'
},
{
id: 4,
label: 'Cash Flow',
value: '$450K',
type: 'positive',
changeIcon: 'mdi:arrow-up',
change: '+15.2%'
}
])
// Dropdown states
const showQuarter = ref(false)
const selectedQuarter = ref('Q4 2023')
const quarters = ref(['Q1 2023', 'Q2 2023', 'Q3 2023', 'Q4 2023', 'Q1 2024'])
// Time period selector
const selectedPeriod = ref('YTD')
const timePeriods = ref(['MTD', 'QTD', 'YTD'])
// Methods
const toggleQuarter = () => {
showQuarter.value = !showQuarter.value
}
const selectQuarter = (quarter) => {
selectedQuarter.value = quarter
showQuarter.value = false
}
onMounted(() => {
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('.morphing-select-wrapper')) {
showQuarter.value = false
}
})
})
</script>
<style lang="scss" scoped>
@import '@/assets/scss/design-system-v2.scss';
.board-dashboard-v2 {
padding: 2rem;
background: linear-gradient(135deg, $neutral-50 0%, $neutral-100 100%);
min-height: 100vh;
}
// Executive Header
.executive-header {
text-align: center;
margin-bottom: 3rem;
.dashboard-title {
font-size: $text-4xl;
font-weight: $font-bold;
background: linear-gradient(135deg, $primary-600, $primary-800);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
}
.dashboard-subtitle {
color: $neutral-600;
font-size: $text-lg;
}
}
// KPI Grid
.kpi-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
margin-bottom: 3rem;
}
.kpi-card {
@include neumorphic-card('md');
padding: 1.5rem;
.kpi-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.kpi-icon-wrapper {
width: 48px;
height: 48px;
border-radius: $radius-lg;
display: flex;
align-items: center;
justify-content: center;
box-shadow: $shadow-inset-sm;
.kpi-icon {
width: 24px;
height: 24px;
}
}
.kpi-trend {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: $text-sm;
font-weight: $font-semibold;
&.positive { color: $success-500; }
&.negative { color: $error-500; }
&.neutral { color: $neutral-600; }
.trend-icon {
width: 16px;
height: 16px;
}
}
.kpi-value {
font-size: $text-3xl;
font-weight: $font-bold;
color: $neutral-800;
margin-bottom: 0.5rem;
}
.kpi-label {
color: $neutral-600;
font-size: $text-sm;
margin-bottom: 1rem;
}
.kpi-progress {
.progress-bar {
height: 8px;
border-radius: $radius-full;
overflow: hidden;
margin-bottom: 0.5rem;
.progress-fill {
height: 100%;
border-radius: $radius-full;
transition: width 0.5s $spring-smooth;
}
}
.progress-text {
font-size: $text-xs;
color: $neutral-500;
}
}
}
// Governance Grid
.governance-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin-bottom: 2rem;
@media (max-width: $breakpoint-lg) {
grid-template-columns: 1fr;
}
}
.initiative-card,
.committee-card {
@include neumorphic-card('md');
padding: 2rem;
.card-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1.5rem;
position: relative;
.header-icon {
width: 24px;
height: 24px;
color: $primary-600;
}
h2 {
font-size: $text-xl;
font-weight: $font-semibold;
color: $neutral-800;
flex: 1;
}
}
}
// Strategic Initiatives
.initiatives-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.initiative-item {
padding: 1rem;
border-radius: $radius-lg;
background: $neutral-50;
.initiative-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.initiative-name {
font-weight: $font-medium;
color: $neutral-800;
font-size: $text-sm;
}
.initiative-status {
padding: 0.25rem 0.75rem;
border-radius: $radius-full;
font-size: $text-xs;
font-weight: $font-medium;
&.on-track {
background: rgba($success-500, 0.1);
color: $success-500;
}
&.ahead {
background: rgba($blue-500, 0.1);
color: $blue-500;
}
&.at-risk {
background: rgba($warning-500, 0.1);
color: $warning-500;
}
}
.initiative-progress {
margin-bottom: 0.75rem;
padding: 0.25rem;
border-radius: $radius-full;
.progress-bar-slim {
height: 4px;
border-radius: $radius-full;
background: rgba($neutral-300, 0.3);
.progress-fill-slim {
height: 100%;
background: linear-gradient(135deg, $primary-600, $primary-700);
border-radius: $radius-full;
transition: width 0.5s $spring-smooth;
}
}
}
.initiative-meta {
display: flex;
justify-content: space-between;
font-size: $text-xs;
color: $neutral-600;
}
}
// Committee Grid
.committee-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.committee-item {
padding: 1rem;
border-radius: $radius-lg;
.committee-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
.committee-icon {
width: 20px;
height: 20px;
}
h3 {
font-size: $text-sm;
font-weight: $font-semibold;
color: $neutral-800;
}
}
.committee-stats {
display: flex;
justify-content: space-around;
margin-bottom: 1rem;
.stat {
text-align: center;
.stat-value {
display: block;
font-size: $text-xl;
font-weight: $font-bold;
color: $neutral-800;
}
.stat-label {
font-size: $text-xs;
color: $neutral-600;
}
}
}
}
// Financial Section
.financial-section {
@include neumorphic-card('lg');
padding: 2rem;
margin-bottom: 2rem;
.time-selector {
display: flex;
gap: 0.5rem;
.time-button {
&.active {
background: linear-gradient(145deg, $primary-600, $primary-700);
color: white;
}
}
}
}
.financial-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
@media (max-width: $breakpoint-md) {
grid-template-columns: 1fr;
}
}
.revenue-chart {
h3 {
font-size: $text-lg;
margin-bottom: 1rem;
color: $neutral-800;
}
.chart-placeholder {
height: 200px;
border-radius: $radius-lg;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: $neutral-500;
.chart-icon {
width: 48px;
height: 48px;
margin-bottom: 0.5rem;
opacity: 0.5;
}
}
}
.financial-metrics {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.metric-item {
padding: 1rem;
.metric-label {
font-size: $text-xs;
color: $neutral-600;
margin-bottom: 0.5rem;
}
.metric-value {
font-size: $text-xl;
font-weight: $font-bold;
margin-bottom: 0.5rem;
&.positive { color: $success-500; }
&.negative { color: $error-500; }
&.neutral { color: $neutral-800; }
}
.metric-change {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: $text-xs;
color: $neutral-600;
.change-icon {
width: 14px;
height: 14px;
}
}
}
// Governance Actions
.governance-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.action-card {
@include neumorphic-card('md');
padding: 2rem;
text-align: center;
transition: all $transition-base;
&:hover {
@include neumorphic-card('lg');
transform: translateY(-2px);
}
.action-icon {
width: 48px;
height: 48px;
color: $primary-600;
margin-bottom: 1rem;
}
h3 {
font-size: $text-lg;
font-weight: $font-semibold;
color: $neutral-800;
margin-bottom: 0.5rem;
}
p {
color: $neutral-600;
font-size: $text-sm;
margin-bottom: 1.5rem;
}
}
// Morphing Dropdown
.morphing-select-wrapper {
position: relative;
.select-trigger {
display: flex;
align-items: center;
gap: 0.5rem;
.dropdown-icon {
width: 16px;
height: 16px;
transition: transform 0.3s $spring-smooth;
&.rotate {
transform: rotate(180deg);
}
}
}
}
.morphing-dropdown {
@include morphing-dropdown();
position: absolute;
top: calc(100% + 8px);
right: 0;
min-width: 150px;
z-index: $z-dropdown;
.dropdown-option {
padding: 0.5rem 1rem;
cursor: pointer;
transition: all $transition-fast;
color: $neutral-700;
font-size: $text-sm;
&:hover {
background: rgba($blue-500, 0.1);
color: $blue-600;
padding-left: 1.25rem;
}
}
}
// Neumorphic Elements
.neumorphic-card {
background: linear-gradient(145deg, #ffffff, #f0f0f0);
border-radius: $radius-xl;
box-shadow: $shadow-soft-md;
}
.neumorphic-button {
@include neumorphic-button();
padding: 0.75rem 1.5rem;
border: none;
border-radius: $radius-lg;
font-weight: $font-medium;
color: $neutral-700;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
&.primary {
background: linear-gradient(145deg, $primary-600, $primary-700);
color: white;
&:hover {
background: linear-gradient(145deg, $primary-700, $primary-800);
}
}
&.small {
padding: 0.5rem 0.75rem;
font-size: $text-sm;
}
&.full-width {
width: 100%;
justify-content: center;
}
}
.neumorphic-inset {
box-shadow: $shadow-inset-sm;
background: linear-gradient(145deg, #e6e6e6, #ffffff);
}
// Transitions
.morph-enter-active,
.morph-leave-active {
transition: all 0.3s $spring-smooth;
}
.morph-enter-from {
opacity: 0;
transform: scale(0.95) translateY(-10px);
}
.morph-leave-to {
opacity: 0;
transform: scale(0.95) translateY(-10px);
}
</style>

View File

@ -1,804 +0,0 @@
<template>
<div class="glass-dashboard">
<!-- Ultra-Modern Hero Header -->
<div class="hero-header">
<!-- Animated background -->
<div class="hero-gradient"></div>
<div class="hero-overlay"></div>
<!-- Floating elements -->
<div class="floating-orb orb-1"></div>
<div class="floating-orb orb-2"></div>
<div class="floating-orb orb-3"></div>
<!-- Mesh gradient overlay -->
<div class="mesh-overlay"></div>
<div class="hero-content">
<div class="hero-inner">
<!-- User Info -->
<div class="user-section">
<div class="avatar-wrapper">
<!-- Avatar glow -->
<div class="avatar-glow"></div>
<div class="avatar-container">
<div class="avatar">
<span class="avatar-text">JD</span>
</div>
<!-- Status indicator -->
<div class="status-indicator">
<div class="status-dot"></div>
</div>
<!-- Crown icon -->
<div class="crown-badge">
👑
</div>
</div>
</div>
<div class="user-info">
<h1 class="welcome-title">
Welcome back,
<span class="name-gradient">Board Member!</span>
</h1>
<div class="user-meta">
<div class="role-badge">
<span class="sparkle"></span>
<span class="role-text">Administrator</span>
</div>
<div class="divider"></div>
<span class="org-name">MonacoUSA Association</span>
</div>
</div>
</div>
<!-- Date & Quick Actions -->
<div class="actions-section">
<!-- Date Card -->
<div class="date-card">
<div class="date-header">
<span class="calendar-icon">📅</span>
<p class="date-label">Today</p>
</div>
<p class="date-text">
{{ currentDate }}
</p>
<div class="date-progress">
<div class="progress-fill"></div>
</div>
</div>
<!-- Quick Actions -->
<div class="quick-actions">
<button class="action-btn action-alerts">
<span class="action-icon">🔔</span>
<span class="action-text">Alerts</span>
</button>
<button class="action-btn action-primary">
<span class="action-icon"></span>
<span class="action-text">Quick Add</span>
</button>
</div>
</div>
</div>
<!-- Decorative line -->
<div class="hero-divider"></div>
</div>
</div>
<!-- Statistics Grid -->
<div class="stats-section">
<div class="stats-grid">
<div v-for="stat in stats" :key="stat.label" class="stat-card">
<div class="stat-icon">{{ stat.icon }}</div>
<div class="stat-content">
<p class="stat-label">{{ stat.label }}</p>
<p class="stat-value">
{{ stat.prefix }}{{ stat.value.toLocaleString() }}{{ stat.suffix }}
</p>
<p v-if="stat.change" class="stat-change">
{{ stat.change }}
</p>
</div>
</div>
</div>
</div>
<!-- Dues Management Section -->
<div class="dues-section">
<h2 class="section-title">Member Dues Overview</h2>
<div class="dues-grid">
<div v-for="member in visibleDuesMembers" :key="member.id" class="dues-card">
<div class="member-header">
<div class="member-avatar">
<span>{{ member.initials }}</span>
</div>
<div class="member-details">
<h3 class="member-name">{{ member.name }}</h3>
<p class="member-id">Member #{{ member.id }}</p>
</div>
</div>
<div class="dues-info">
<span class="dues-label">Amount Due</span>
<span class="dues-amount">${{ member.dueAmount }}</span>
</div>
<button class="btn-pay">
Mark Paid
</button>
</div>
</div>
<div class="view-all-container">
<button class="btn-view-all">
View All Members
</button>
</div>
</div>
</div>
</template>
<script>
export default {
layout: 'board',
middleware: 'board-auth'
}
</script>
<script setup>
import { ref, computed } from 'vue'
// Current date
const currentDate = computed(() => {
return new Date().toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})
})
// Statistics data
const stats = ref([
{
icon: '👥',
label: 'TOTAL MEMBERS',
value: 1234,
change: '+12%',
},
{
icon: '💰',
label: 'DUES COLLECTED',
value: 45678,
prefix: '$',
change: '+8%',
},
{
icon: '📅',
label: 'UPCOMING EVENTS',
value: 5,
change: '2 this week',
},
{
icon: '📈',
label: 'GROWTH RATE',
value: 23,
suffix: '%',
change: '+3%',
}
])
// Sample dues members data
const visibleDuesMembers = ref([
{
id: 1,
name: 'John Smith',
initials: 'JS',
dueAmount: 250,
},
{
id: 2,
name: 'Marie Dubois',
initials: 'MD',
dueAmount: 250,
},
{
id: 3,
name: 'Alessandro Rossi',
initials: 'AR',
dueAmount: 250,
},
{
id: 4,
name: 'Emma Wilson',
initials: 'EW',
dueAmount: 250,
}
])
</script>
<style scoped>
/* Simplified Animations */
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slide-up {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slide-in {
from { opacity: 0; transform: translateX(-20px); }
to { opacity: 1; transform: translateX(0); }
}
/* Base Styles */
.glass-dashboard {
min-height: 100vh;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
}
/* Hero Header */
.hero-header {
position: relative;
overflow: hidden;
margin-bottom: 2rem;
}
.hero-gradient {
position: absolute;
inset: 0;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
}
.hero-overlay {
position: absolute;
inset: 0;
background: linear-gradient(to top, rgba(0,0,0,0.3), transparent);
}
/* Floating Orbs */
.floating-orb {
position: absolute;
border-radius: 50%;
opacity: 0.3;
filter: blur(20px);
}
.orb-1 {
top: 40px;
right: 80px;
width: 200px;
height: 200px;
background: rgba(220, 38, 38, 0.05);
}
.orb-2 {
bottom: 40px;
left: 80px;
width: 150px;
height: 150px;
background: rgba(220, 38, 38, 0.03);
}
.orb-3 {
top: 50%;
left: 50%;
width: 100px;
height: 100px;
background: rgba(220, 38, 38, 0.02);
}
.mesh-overlay {
position: absolute;
inset: 0;
opacity: 0.2;
background: radial-gradient(circle at 20% 50%, transparent 0%, rgba(255,255,255,0.1) 50%, transparent 100%);
mix-blend-mode: overlay;
}
.hero-content {
position: relative;
padding: 3rem;
border-radius: 1.5rem;
}
.hero-inner {
display: flex;
flex-direction: column;
gap: 2rem;
}
@media (min-width: 1024px) {
.hero-inner {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
}
/* User Section */
.user-section {
display: flex;
align-items: center;
gap: 1.5rem;
animation: fade-in 0.3s ease-out;
}
.avatar-wrapper {
position: relative;
group: true;
}
.avatar-glow {
position: absolute;
inset: -4px;
background: rgba(220, 38, 38, 0.1);
border-radius: 50%;
opacity: 0.5;
transition: opacity 0.3s;
}
.avatar-wrapper:hover .avatar-glow {
opacity: 0.8;
}
.avatar-container {
position: relative;
}
.avatar {
width: 80px;
height: 80px;
background: linear-gradient(135deg, #ffffff, #f8f9fa);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow:
0 0 0 4px rgba(255,255,255,0.4),
0 10px 40px rgba(0,0,0,0.2);
border: 2px solid rgba(255,255,255,0.2);
transition: transform 0.3s;
}
.avatar:hover {
transform: scale(1.1);
}
.avatar-text {
font-size: 1.5rem;
font-weight: bold;
color: #dc2626;
}
.status-indicator {
position: absolute;
bottom: -8px;
right: -8px;
width: 32px;
height: 32px;
background: linear-gradient(135deg, #10b981, #059669);
border-radius: 50%;
border: 4px solid white;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
display: flex;
align-items: center;
justify-content: center;
}
.status-dot {
width: 8px;
height: 8px;
background: white;
border-radius: 50%;
animation: pulse-slow 2s infinite;
}
.crown-badge {
position: absolute;
top: -12px;
right: -4px;
width: 32px;
height: 32px;
background: linear-gradient(135deg, #fbbf24, #f59e0b);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
animation: bounce-slow 3s infinite;
}
/* User Info */
.user-info {
flex: 1;
}
.welcome-title {
font-size: 2.5rem;
font-weight: 700;
color: #27272a;
margin-bottom: 0.5rem;
line-height: 1.2;
letter-spacing: -0.02em;
}
.name-gradient {
display: block;
color: #dc2626;
font-weight: 800;
}
.user-meta {
display: flex;
align-items: center;
gap: 1rem;
}
.role-badge {
background: rgba(255,255,255,0.6);
backdrop-filter: blur(4px);
border-radius: 9999px;
padding: 0.75rem 1.5rem;
border: 1px solid rgba(255,255,255,0.2);
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
display: flex;
align-items: center;
gap: 0.5rem;
}
.sparkle {
font-size: 1.25rem;
}
.role-text {
color: #27272a;
font-weight: 600;
}
.divider {
height: 1.5rem;
width: 1px;
background: rgba(255,255,255,0.3);
}
.org-name {
color: #6b7280;
font-weight: 500;
}
/* Actions Section */
.actions-section {
animation: fade-in 0.3s ease-out;
animation-delay: 100ms;
animation-fill-mode: both;
}
.date-card {
background: rgba(255,255,255,0.6);
backdrop-filter: blur(2px);
border-radius: 1rem;
padding: 1.5rem;
border: 1px solid rgba(0,0,0,0.05);
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
transition: all 0.3s;
margin-bottom: 1rem;
}
.date-card:hover {
background: rgba(255,255,255,0.8);
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
}
.date-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.calendar-icon {
font-size: 1.5rem;
}
.date-label {
color: #6b7280;
font-weight: 600;
}
.date-text {
font-size: 1.25rem;
font-weight: 600;
color: #27272a;
line-height: 1.2;
}
.date-progress {
margin-top: 0.75rem;
height: 4px;
background: rgba(255,255,255,0.2);
border-radius: 9999px;
overflow: hidden;
}
.progress-fill {
height: 100%;
width: 75%;
background: linear-gradient(90deg, rgba(255,255,255,0.6), rgba(255,255,255,0.8));
border-radius: 9999px;
animation: pulse-slow 2s infinite;
}
.quick-actions {
display: flex;
gap: 0.75rem;
}
.action-btn {
flex: 1;
padding: 1rem;
border-radius: 0.75rem;
border: 1px solid rgba(255,255,255,0.2);
transition: all 0.3s;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.action-alerts {
background: rgba(255,255,255,0.6);
backdrop-filter: blur(2px);
color: #27272a;
border: 1px solid rgba(0,0,0,0.05);
}
.action-alerts:hover {
background: rgba(255,255,255,0.8);
transform: translateY(-2px);
}
.action-primary {
background: #dc2626;
color: white;
box-shadow: 0 4px 12px rgba(220, 38, 38, 0.2);
}
.action-primary:hover {
background: #b91c1c;
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(220, 38, 38, 0.3);
}
.action-icon {
font-size: 1.25rem;
}
.action-text {
font-size: 0.875rem;
font-weight: 500;
}
.hero-divider {
margin-top: 2rem;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
}
/* Stats Section */
.stats-section {
padding: 0 1.5rem;
margin-bottom: 2rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.stat-card {
background: rgba(255, 255, 255, 0.8);
border-radius: 1rem;
padding: 1.5rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
border: 1px solid rgba(0,0,0,0.05);
transition: all 0.2s;
display: flex;
align-items: flex-start;
gap: 1rem;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
background: rgba(255, 255, 255, 0.95);
}
.stat-icon {
font-size: 1.5rem;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border: 1px solid rgba(220, 38, 38, 0.1);
border-radius: 0.75rem;
}
.stat-content {
flex: 1;
}
.stat-label {
font-size: 0.75rem;
font-weight: 600;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.25rem;
}
.stat-value {
font-size: 1.875rem;
font-weight: 900;
color: #1f2937;
line-height: 1;
margin-bottom: 0.5rem;
}
.stat-change {
font-size: 0.875rem;
color: #10b981;
font-weight: 500;
}
/* Dues Section */
.dues-section {
padding: 0 1.5rem;
margin-bottom: 2rem;
}
.section-title {
font-size: 1.5rem;
font-weight: 700;
color: #1f2937;
margin-bottom: 1.5rem;
}
.dues-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.dues-card {
background: rgba(255, 255, 255, 0.8);
border-radius: 1rem;
padding: 1.5rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
border: 1px solid rgba(0,0,0,0.05);
transition: all 0.2s;
}
.dues-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
background: rgba(255, 255, 255, 0.95);
}
.member-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
.member-avatar {
width: 48px;
height: 48px;
background: linear-gradient(135deg, #e5e7eb, #d1d5db);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
color: #6b7280;
}
.member-details {
flex: 1;
}
.member-name {
font-weight: 600;
color: #1f2937;
margin-bottom: 0.25rem;
}
.member-id {
font-size: 0.875rem;
color: #6b7280;
}
.dues-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding: 0.75rem;
background: #f9fafb;
border-radius: 0.75rem;
}
.dues-label {
font-size: 0.875rem;
color: #6b7280;
}
.dues-amount {
font-size: 1.25rem;
font-weight: 700;
color: #dc2626;
}
.btn-pay {
width: 100%;
padding: 0.75rem;
background: rgba(255, 255, 255, 0.6);
color: #dc2626;
font-weight: 600;
border: 1px solid rgba(220, 38, 38, 0.2);
border-radius: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.btn-pay:hover {
background: #dc2626;
color: white;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(220, 38, 38, 0.3);
}
.view-all-container {
text-align: center;
}
.btn-view-all {
padding: 0.875rem 2rem;
background: white;
color: #dc2626;
font-weight: 600;
border: 2px solid #dc2626;
border-radius: 0.75rem;
cursor: pointer;
transition: all 0.3s;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-view-all:hover {
background: #dc2626;
color: white;
transform: translateX(4px);
}
</style>

View File

@ -1,419 +0,0 @@
<template>
<div class="min-h-screen" style="background: linear-gradient(135deg, #f5f5f5 0%, #ffffff 100%)">
<!-- Glass Sidebar -->
<GlassSidebar
:is-open="sidebarOpen"
:is-mobile="isMobile"
:user-name="firstName"
:user-role="userRole"
:user-avatar="userAvatar"
@close="sidebarOpen = false"
@logout="handleLogout"
/>
<!-- Main Content Area -->
<div :class="['transition-all duration-300', isMobile ? 'ml-0' : 'ml-72']">
<!-- Glass Navigation Bar -->
<header class="glass-navbar sticky top-0 z-30 px-6 py-4">
<div class="flex items-center justify-between">
<!-- Mobile Menu Toggle -->
<button
v-if="isMobile"
@click="sidebarOpen = !sidebarOpen"
class="p-2 rounded-lg hover:bg-glass-monaco-soft transition-colors lg:hidden"
>
<Menu class="w-6 h-6 text-gray-700" />
</button>
<!-- Page Title -->
<div class="flex items-center gap-4">
<h1 class="text-2xl font-bold text-gradient-monaco">Board Dashboard</h1>
<span class="px-3 py-1 rounded-full bg-glass-monaco-soft text-monaco-600 text-sm font-medium">
{{ currentDate }}
</span>
</div>
<!-- Header Actions -->
<div class="flex items-center gap-3">
<!-- Notifications -->
<button class="relative p-2 rounded-lg hover:bg-glass-monaco-soft transition-colors">
<Bell class="w-5 h-5 text-gray-700" />
<span class="absolute top-1 right-1 w-2 h-2 bg-monaco-600 rounded-full"></span>
</button>
<!-- Quick Actions -->
<button
@click="showQuickActions = !showQuickActions"
class="btn-glass-primary flex items-center gap-2"
>
<Plus class="w-4 h-4" />
Quick Action
</button>
</div>
</div>
</header>
<!-- Dashboard Content -->
<main class="p-6 space-y-6">
<!-- Welcome Section -->
<div class="glass-ultra rounded-glass p-8 text-center animate-fade-in">
<h2 class="text-3xl font-bold text-gray-800 mb-2">
Welcome back, {{ firstName }}!
</h2>
<p class="text-gray-600">
Here's an overview of MonacoUSA's current status and activities.
</p>
</div>
<!-- Statistics Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<GlassStatCard
v-for="stat in stats"
:key="stat.label"
:icon="stat.icon"
:label="stat.label"
:value="stat.value"
:prefix="stat.prefix"
:suffix="stat.suffix"
:change="stat.change"
:change-type="stat.changeType"
:icon-color="stat.color"
:action-label="stat.actionLabel"
@click="handleStatClick(stat)"
/>
</div>
<!-- Dues Management Section - LIMITED TO 4 CARDS -->
<div class="space-y-4">
<div class="flex items-center justify-between">
<h3 class="text-xl font-semibold text-gray-800">Member Dues Overview</h3>
<div class="flex items-center gap-3">
<!-- Filter Tabs -->
<div class="glass rounded-full p-1 flex gap-1">
<button
v-for="tab in duesTabs"
:key="tab.id"
@click="activeDuesTab = tab.id"
:class="[
'px-4 py-2 rounded-full text-sm font-medium transition-all',
activeDuesTab === tab.id
? 'bg-gradient-monaco text-white shadow-monaco-sm'
: 'text-gray-600 hover:text-monaco-600 hover:bg-glass-monaco-soft'
]"
>
{{ tab.label }}
<span v-if="tab.count" class="ml-1">({{ tab.count }})</span>
</button>
</div>
</div>
</div>
<!-- Dues Cards Grid - MAX 4 VISIBLE -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<GlassDuesCard
v-for="member in visibleDuesMembers"
:key="member.id"
:member="member"
:status="activeDuesTab"
@mark-paid="handleMarkPaid"
@send-reminder="handleSendReminder"
@view-details="handleViewDetails"
/>
</div>
<!-- View All Button -->
<div v-if="totalDuesMembers > 4" class="text-center">
<button
@click="navigateToFullDuesList"
class="btn-glass-secondary inline-flex items-center gap-2"
>
View All {{ totalDuesMembers }} Members
<ArrowRight class="w-4 h-4" />
</button>
</div>
</div>
<!-- Quick Actions Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Upcoming Events Card -->
<div class="glass-card-bright">
<div class="flex items-center justify-between mb-4">
<h3 class="font-semibold text-gray-800">Upcoming Events</h3>
<Calendar class="w-5 h-5 text-monaco-600" />
</div>
<div v-if="nextEvent" class="space-y-3">
<div class="p-4 bg-glass-monaco-soft rounded-xl">
<h4 class="font-medium text-gray-800">{{ nextEvent.title }}</h4>
<p class="text-sm text-gray-600 mt-1">{{ nextEvent.date }}</p>
<p class="text-xs text-gray-500 mt-2">{{ nextEvent.attendees }} attendees</p>
</div>
<button class="w-full btn-glass text-sm">
View All Events
</button>
</div>
<div v-else class="text-center py-8 text-gray-500">
No upcoming events
</div>
</div>
<!-- Recent Activity Card -->
<div class="glass-card-bright">
<div class="flex items-center justify-between mb-4">
<h3 class="font-semibold text-gray-800">Recent Activity</h3>
<Activity class="w-5 h-5 text-monaco-600" />
</div>
<div class="space-y-3">
<div
v-for="activity in recentActivity"
:key="activity.id"
class="flex items-start gap-3 p-3 rounded-lg hover:bg-glass-monaco-soft transition-colors"
>
<div class="w-2 h-2 bg-monaco-600 rounded-full mt-1.5"></div>
<div class="flex-1">
<p class="text-sm text-gray-700">{{ activity.description }}</p>
<p class="text-xs text-gray-500 mt-1">{{ activity.time }}</p>
</div>
</div>
</div>
</div>
<!-- Quick Links Card -->
<div class="glass-card-bright">
<div class="flex items-center justify-between mb-4">
<h3 class="font-semibold text-gray-800">Quick Actions</h3>
<Zap class="w-5 h-5 text-monaco-600" />
</div>
<div class="grid grid-cols-2 gap-3">
<button
v-for="action in quickActions"
:key="action.label"
@click="action.handler"
class="p-3 rounded-xl bg-white/50 hover:bg-glass-monaco-soft
transition-all hover:scale-105 group text-center"
>
<component
:is="action.icon"
class="w-6 h-6 text-gray-600 group-hover:text-monaco-600 mx-auto mb-2"
/>
<span class="text-xs text-gray-700 group-hover:text-monaco-600 font-medium">
{{ action.label }}
</span>
</button>
</div>
</div>
</div>
</main>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router'
import {
Menu, Bell, Plus, ArrowRight, Calendar, Activity, Zap,
Users, DollarSign, FileText, Mail, TrendingUp, Settings,
LayoutDashboard, UserPlus, Send, Download
} from 'lucide-vue-next'
// Import our glass components
import GlassSidebar from '~/components/GlassSidebar.vue'
import GlassStatCard from '~/components/GlassStatCard.vue'
import GlassDuesCard from '~/components/GlassDuesCard.vue'
const router = useRouter()
// Reactive state
const sidebarOpen = ref(false)
const isMobile = ref(false)
const showQuickActions = ref(false)
const activeDuesTab = ref('overdue')
const isLoading = ref(false)
// User data
const firstName = ref('Board Member')
const userRole = ref('Administrator')
const userAvatar = ref('/default-avatar.png')
// Current date
const currentDate = computed(() => {
return new Date().toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})
})
// Statistics data with Lucide icons
const stats = ref([
{
icon: Users,
label: 'Total Members',
value: 1234,
change: '+12%',
changeType: 'increase',
color: 'monaco',
actionLabel: 'View all members'
},
{
icon: DollarSign,
label: 'Dues Collected',
value: 45678,
prefix: '$',
change: '+8%',
changeType: 'increase',
color: 'green',
actionLabel: 'View payments'
},
{
icon: Calendar,
label: 'Upcoming Events',
value: 5,
change: '2 this week',
changeType: 'neutral',
color: 'blue',
actionLabel: 'View calendar'
},
{
icon: TrendingUp,
label: 'Growth Rate',
value: 23,
suffix: '%',
change: '+3%',
changeType: 'increase',
color: 'purple',
actionLabel: 'View report'
}
])
// Dues tabs
const duesTabs = ref([
{ id: 'overdue', label: 'Overdue', count: 12 },
{ id: 'upcoming', label: 'Upcoming', count: 24 },
{ id: 'paid', label: 'Recently Paid', count: 8 }
])
// Sample dues members data - LIMITED TO 4
const duesMembers = ref([
{
id: 1,
name: 'John Smith',
avatar: '/avatar1.jpg',
countryCode: 'US',
dueAmount: 250,
dueDate: '2024-01-15'
},
{
id: 2,
name: 'Marie Dubois',
avatar: '/avatar2.jpg',
countryCode: 'MC',
dueAmount: 250,
dueDate: '2024-01-20'
},
{
id: 3,
name: 'Alessandro Rossi',
avatar: '/avatar3.jpg',
countryCode: 'IT',
dueAmount: 250,
dueDate: '2024-01-25'
},
{
id: 4,
name: 'Emma Wilson',
avatar: '/avatar4.jpg',
countryCode: 'GB',
dueAmount: 250,
dueDate: '2024-01-30'
}
])
// Computed: visible dues members (max 4)
const visibleDuesMembers = computed(() => {
return duesMembers.value.slice(0, 4)
})
const totalDuesMembers = computed(() => {
const tab = duesTabs.value.find(t => t.id === activeDuesTab.value)
return tab ? tab.count : 0
})
// Next event
const nextEvent = ref({
title: 'Annual Gala Dinner',
date: 'January 28, 2024',
attendees: 150
})
// Recent activity
const recentActivity = ref([
{ id: 1, description: 'New member John Doe joined', time: '2 hours ago' },
{ id: 2, description: 'Payment received from Jane Smith', time: '5 hours ago' },
{ id: 3, description: 'Event "Wine Tasting" created', time: '1 day ago' }
])
// Quick actions
const quickActions = [
{ icon: UserPlus, label: 'Add Member', handler: () => router.push('/board/members/new') },
{ icon: Calendar, label: 'New Event', handler: () => router.push('/board/events/new') },
{ icon: Send, label: 'Send Email', handler: () => router.push('/board/communications') },
{ icon: Download, label: 'Export Data', handler: () => generateReport() }
]
// Event handlers
const handleStatClick = (stat) => {
console.log('Stat clicked:', stat.label)
}
const handleMarkPaid = (member) => {
console.log('Mark paid:', member.name)
// Implement payment marking logic
}
const handleSendReminder = (member) => {
console.log('Send reminder:', member.name)
// Implement reminder logic
}
const handleViewDetails = (member) => {
router.push(`/board/members/${member.id}`)
}
const navigateToFullDuesList = () => {
router.push('/board/dues')
}
const handleLogout = () => {
// Implement logout logic
router.push('/logout')
}
const generateReport = () => {
console.log('Generating report...')
// Implement report generation
}
// Check if mobile
const checkMobile = () => {
isMobile.value = window.innerWidth < 1024
if (!isMobile.value) {
sidebarOpen.value = false
}
}
// Lifecycle
onMounted(() => {
checkMobile()
window.addEventListener('resize', checkMobile)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', checkMobile)
})
</script>
<style scoped>
/* Any additional custom styles */
</style>

View File

@ -1,889 +0,0 @@
<template>
<div>
<v-container fluid>
<v-row>
<v-col cols="12">
<h1 class="text-h4 font-weight-bold mb-4">
<v-icon left>mdi-account</v-icon>
Welcome Back, {{ firstName }}
</h1>
<p class="text-body-1 mb-6">
Manage users and portal settings for the MonacoUSA Portal.
</p>
</v-col>
</v-row>
<!-- Portal Status -->
<v-row class="mb-6">
<v-col cols="12" md="6">
<v-card elevation="2">
<v-card-text>
<div class="d-flex justify-space-between align-center">
<div>
<p class="text-caption text-medium-emphasis mb-1">Portal Status</p>
<p class="text-h5 font-weight-bold text-success">Online</p>
</div>
<v-icon color="success" size="40">mdi-check-circle</v-icon>
</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card elevation="2">
<v-card-text>
<div class="d-flex justify-space-between align-center">
<div>
<p class="text-caption text-medium-emphasis mb-1">Total Users</p>
<p class="text-h5 font-weight-bold">{{ userCount }}</p>
</div>
<v-icon color="primary" size="40">mdi-account-multiple</v-icon>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- User Management -->
<v-row class="mb-6">
<v-col cols="12">
<v-card elevation="2">
<v-card-title>
<v-icon left>mdi-account-group</v-icon>
User Management
</v-card-title>
<v-card-text>
<p class="mb-4">Manage user accounts, roles, and permissions for the MonacoUSA Portal.</p>
<v-row>
<v-col cols="12" md="3">
<v-btn
color="primary"
block
size="large"
@click="navigateTo('/dashboard/member-list')"
>
<v-icon start>mdi-account-cog</v-icon>
Manage Users
</v-btn>
</v-col>
<v-col cols="12" md="3">
<v-btn
color="success"
variant="outlined"
block
size="large"
@click="showCreateUserDialog = true"
>
<v-icon start>mdi-account-plus</v-icon>
Create User Account
</v-btn>
</v-col>
<v-col cols="12" md="3">
<v-btn
color="secondary"
variant="outlined"
block
size="large"
@click="viewAuditLogs"
>
<v-icon start>mdi-file-document-outline</v-icon>
View Audit Logs
</v-btn>
</v-col>
<v-col cols="12" md="3">
<v-btn
color="secondary"
variant="outlined"
block
size="large"
@click="showAdminConfig = true"
>
<v-icon start>mdi-cog</v-icon>
Portal Settings
</v-btn>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- Dues Management -->
<v-row class="mb-6">
<v-col cols="12">
<BoardDuesManagement
:refresh-trigger="duesRefreshTrigger"
@view-member="handleViewMember"
@view-all-members="navigateToMembers"
@member-updated="handleMemberUpdated"
/>
</v-col>
</v-row>
<!-- Portal Configuration -->
<v-row class="mb-6">
<v-col cols="12">
<v-card elevation="2">
<v-card-title>
<v-icon left>mdi-cog</v-icon>
Portal Configuration
</v-card-title>
<v-card-text>
<p class="mb-4">Configure all portal settings including database, email, reCAPTCHA, and membership fees in one centralized location.</p>
<v-row>
<v-col cols="12" md="4">
<v-btn
color="primary"
block
size="large"
@click="showAdminConfig = true"
>
<v-icon start>mdi-cog</v-icon>
Portal Settings
</v-btn>
</v-col>
<v-col cols="12" md="8">
<v-row dense>
<v-col cols="6" sm="3">
<v-chip color="success" variant="tonal" size="small" block>
<v-icon start size="14">mdi-database</v-icon>
NocoDB
</v-chip>
</v-col>
<v-col cols="6" sm="3">
<v-chip color="info" variant="tonal" size="small" block>
<v-icon start size="14">mdi-email</v-icon>
Email
</v-chip>
</v-col>
<v-col cols="6" sm="3">
<v-chip color="warning" variant="tonal" size="small" block>
<v-icon start size="14">mdi-shield</v-icon>
reCAPTCHA
</v-chip>
</v-col>
<v-col cols="6" sm="3">
<v-chip color="primary" variant="tonal" size="small" block>
<v-icon start size="14">mdi-bank</v-icon>
Membership
</v-chip>
</v-col>
</v-row>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- Data Management -->
<v-row class="mb-6">
<v-col cols="12">
<v-card elevation="2">
<v-card-title>
<v-icon left>mdi-database-cog</v-icon>
Data Management
</v-card-title>
<v-card-text>
<p class="mb-4">Manage data integrity and perform maintenance operations on the portal database.</p>
<v-row>
<v-col cols="12" md="6">
<v-btn
@click="assignMemberIds"
color="warning"
variant="outlined"
prepend-icon="mdi-account-multiple-plus"
block
size="large"
:loading="assigningMemberIds"
>
Assign Member IDs
</v-btn>
<div class="text-caption mt-2 text-medium-emphasis">
Assign unique member IDs (MUSA-0001, MUSA-0002, etc.) to members who don't have them
</div>
</v-col>
<v-col cols="12" md="6">
<v-btn
@click="backfillEventIds"
color="primary"
variant="outlined"
prepend-icon="mdi-calendar-sync"
block
size="large"
:loading="backfillLoading"
>
Backfill Event IDs
</v-btn>
<div class="text-caption mt-2 text-medium-emphasis">
Assign business IDs to events that don't have them
</div>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
<!-- NocoDB Settings Dialog -->
<NocoDBSettingsDialog
v-model="showNocoDBSettings"
@settings-saved="handleSettingsSaved"
/>
<!-- Admin Configuration Dialog -->
<AdminConfigurationDialog
v-model="showAdminConfig"
@settings-saved="handleAdminConfigSaved"
/>
<!-- reCAPTCHA Configuration Dialog -->
<v-dialog v-model="showRecaptchaConfig" max-width="600">
<v-card>
<v-card-title class="text-h5">
<v-icon left>mdi-shield-account</v-icon>
reCAPTCHA Configuration
</v-card-title>
<v-card-text>
<v-alert type="info" variant="tonal" class="mb-4">
<v-alert-title>Security Configuration</v-alert-title>
Configure Google reCAPTCHA settings for form protection on the registration page.
</v-alert>
<v-form ref="recaptchaForm" v-model="recaptchaValid">
<v-text-field
v-model="recaptchaConfig.siteKey"
label="Site Key"
placeholder="6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy"
:rules="[v => !!v || 'Site key is required']"
variant="outlined"
required
/>
<v-text-field
v-model="recaptchaConfig.secretKey"
label="Secret Key"
placeholder="6Lc6BAAAAAAAAKN3DRm6VA_xxxxxxxxxxxxxxxxx"
:rules="[v => !!v || 'Secret key is required']"
variant="outlined"
type="password"
required
/>
<v-alert type="warning" variant="tonal" class="mt-4">
<v-alert-title>Important</v-alert-title>
Keep your secret key confidential. You can get these keys from the Google reCAPTCHA admin console.
</v-alert>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn text @click="showRecaptchaConfig = false">Cancel</v-btn>
<v-btn
color="primary"
:loading="savingRecaptcha"
:disabled="!recaptchaValid"
@click="saveRecaptchaConfig"
>
Save Configuration
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Membership Configuration Dialog -->
<v-dialog v-model="showMembershipConfig" max-width="600">
<v-card>
<v-card-title class="text-h5">
<v-icon left>mdi-bank</v-icon>
Membership Configuration
</v-card-title>
<v-card-text>
<v-alert type="info" variant="tonal" class="mb-4">
<v-alert-title>Payment Configuration</v-alert-title>
Configure membership fees and payment details displayed on the registration page.
</v-alert>
<v-form ref="membershipForm" v-model="membershipValid">
<v-text-field
v-model="membershipConfig.membershipFee"
label="Annual Membership Fee (€)"
type="number"
:rules="[
v => !!v || 'Membership fee is required',
v => v > 0 || 'Fee must be greater than 0'
]"
variant="outlined"
required
/>
<v-text-field
v-model="membershipConfig.iban"
label="IBAN"
placeholder="DE89 3704 0044 0532 0130 00"
:rules="[v => !!v || 'IBAN is required']"
variant="outlined"
required
/>
<v-text-field
v-model="membershipConfig.accountHolder"
label="Account Holder Name"
placeholder="MonacoUSA Association"
:rules="[v => !!v || 'Account holder is required']"
variant="outlined"
required
/>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn text @click="showMembershipConfig = false">Cancel</v-btn>
<v-btn
color="primary"
:loading="savingMembership"
:disabled="!membershipValid"
@click="saveMembershipConfig"
>
Save Configuration
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- View Member Dialog -->
<ViewMemberDialog
v-model="showViewDialog"
:member="selectedMember"
@edit="handleEditMember"
/>
<!-- Edit Member Dialog -->
<EditMemberDialog
v-model="showEditDialog"
:member="selectedMember"
@member-updated="handleMemberUpdated"
/>
<!-- Create User Dialog -->
<v-dialog v-model="showCreateUserDialog" max-width="600">
<v-card>
<v-card-title class="text-h5">
<v-icon left>mdi-account-plus</v-icon>
Create User Account
</v-card-title>
<v-card-text>
<v-alert type="info" variant="tonal" class="mb-4">
<v-alert-title>Create Portal Account</v-alert-title>
This will create a new user account in the MonacoUSA Portal with email verification.
</v-alert>
<v-form ref="createUserForm" v-model="createUserValid">
<v-row>
<v-col cols="6">
<v-text-field
v-model="newUser.firstName"
label="First Name"
:rules="[v => !!v || 'First name is required']"
variant="outlined"
required
/>
</v-col>
<v-col cols="6">
<v-text-field
v-model="newUser.lastName"
label="Last Name"
:rules="[v => !!v || 'Last name is required']"
variant="outlined"
required
/>
</v-col>
</v-row>
<v-text-field
v-model="newUser.email"
label="Email Address"
type="email"
:rules="[
v => !!v || 'Email is required',
v => /.+@.+\..+/.test(v) || 'Email must be valid'
]"
variant="outlined"
required
/>
<v-select
v-model="newUser.role"
label="User Role"
:items="roleOptions"
item-title="title"
item-value="value"
variant="outlined"
required
/>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn text @click="showCreateUserDialog = false">Cancel</v-btn>
<v-btn
color="primary"
:loading="creatingUser"
:disabled="!createUserValid"
@click="createUserAccount"
>
Create Account
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'dashboard',
middleware: 'auth-admin'
});
const { firstName } = useAuth();
// Reactive data
const userCount = ref(0);
const loading = ref(false);
const showCreateUserDialog = ref(false);
const showAdminConfig = ref(false);
const showRecaptchaConfig = ref(false);
const showMembershipConfig = ref(false);
const showEmailConfig = ref(false);
// Dues management
const overdueCount = ref(0);
const overdueRefreshTrigger = ref(0);
const duesRefreshTrigger = ref(0);
// Data management
const assigningMemberIds = ref(false);
const backfillLoading = ref(false);
// Member dialog state
const showViewDialog = ref(false);
const showEditDialog = ref(false);
const selectedMember = ref(null);
// Create user dialog data
const createUserValid = ref(false);
const creatingUser = ref(false);
const newUser = ref({
firstName: '',
lastName: '',
email: '',
role: 'user'
});
const roleOptions = [
{ title: 'User', value: 'user' },
{ title: 'Board Member', value: 'board' },
{ title: 'Administrator', value: 'admin' }
];
// reCAPTCHA configuration data
const recaptchaValid = ref(false);
const savingRecaptcha = ref(false);
const recaptchaConfig = ref({
siteKey: '',
secretKey: ''
});
// Membership configuration data
const membershipValid = ref(false);
const savingMembership = ref(false);
const membershipConfig = ref({
membershipFee: 50,
iban: '',
accountHolder: ''
});
const recentActivity = ref([
{
id: 1,
title: 'User Account Created',
description: 'New user account created for john.doe@monacousa.org',
time: '2 hours ago',
icon: 'mdi-account-plus',
color: 'success'
},
{
id: 2,
title: 'Role Updated',
description: 'User role updated from User to Board Member',
time: '4 hours ago',
icon: 'mdi-shield-account',
color: 'warning'
},
{
id: 3,
title: 'System Backup',
description: 'Automated system backup completed successfully',
time: '1 day ago',
icon: 'mdi-backup-restore',
color: 'info'
},
{
id: 4,
title: 'Password Reset',
description: 'Password reset requested for jane.smith@monacousa.org',
time: '2 days ago',
icon: 'mdi-key-change',
color: 'primary'
}
]);
// Load simplified admin stats (without system metrics)
const loadStats = async () => {
try {
loading.value = true;
// Simple user count without complex system metrics
const response = await $fetch<{ userCount: number }>('/api/admin/stats');
userCount.value = response.userCount || 0;
console.log('✅ Admin stats loaded:', { userCount: userCount.value });
} catch (error) {
console.error('❌ Failed to load admin stats:', error);
// Use fallback data
userCount.value = 25;
} finally {
loading.value = false;
}
};
// Action methods (placeholders for now)
const manageUsers = () => {
window.open('https://auth.monacousa.org', '_blank');
};
const viewAuditLogs = () => {
console.log('Navigate to audit logs');
// TODO: Implement audit logs navigation
};
const showNocoDBSettings = ref(false);
const portalSettings = () => {
showNocoDBSettings.value = true;
};
const handleSettingsSaved = () => {
console.log('NocoDB settings saved successfully');
};
const handleAdminConfigSaved = () => {
console.log('Admin configuration saved successfully');
showAdminConfig.value = false;
};
// Handle opening email configuration directly
const openEmailConfig = () => {
// Set the activeTab to email when opening the admin config dialog
showEmailConfig.value = true;
showAdminConfig.value = true;
};
// Watch for showEmailConfig to set the initial tab
watch(showEmailConfig, (newValue) => {
if (newValue) {
// This will be handled by the AdminConfigurationDialog to set initial tab
showEmailConfig.value = false; // Reset the flag
}
});
const saveRecaptchaConfig = async () => {
if (!recaptchaValid.value) return;
savingRecaptcha.value = true;
try {
const response = await $fetch('/api/admin/recaptcha-config', {
method: 'POST',
body: {
siteKey: recaptchaConfig.value.siteKey,
secretKey: recaptchaConfig.value.secretKey
}
}) as any;
if (response?.success) {
showRecaptchaConfig.value = false;
console.log('reCAPTCHA configuration saved successfully');
// TODO: Show success notification
}
} catch (error) {
console.error('Failed to save reCAPTCHA configuration:', error);
// TODO: Show error notification
} finally {
savingRecaptcha.value = false;
}
};
const saveMembershipConfig = async () => {
if (!membershipValid.value) return;
savingMembership.value = true;
try {
const response = await $fetch('/api/admin/registration-config', {
method: 'POST',
body: {
membershipFee: membershipConfig.value.membershipFee,
iban: membershipConfig.value.iban,
accountHolder: membershipConfig.value.accountHolder
}
}) as any;
if (response?.success) {
showMembershipConfig.value = false;
console.log('Membership configuration saved successfully');
// TODO: Show success notification
}
} catch (error) {
console.error('Failed to save membership configuration:', error);
// TODO: Show error notification
} finally {
savingMembership.value = false;
}
};
const createUserAccount = async () => {
if (!createUserValid.value) return;
creatingUser.value = true;
try {
console.log('Creating user account:', newUser.value);
// TODO: Implement actual user creation using enhanced Keycloak API
// For now, just show success
await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate API call
// Reset form
newUser.value = {
firstName: '',
lastName: '',
email: '',
role: 'user'
};
showCreateUserDialog.value = false;
console.log('User account created successfully');
// TODO: Show success notification
// TODO: Refresh user list
} catch (error) {
console.error('Failed to create user account:', error);
// TODO: Show error notification
} finally {
creatingUser.value = false;
}
};
const createUser = () => {
console.log('Create new user');
// TODO: Implement create user dialog/form
};
const generateReport = () => {
console.log('Generate user report');
// TODO: Implement report generation
};
const manageRoles = () => {
console.log('Manage user roles');
// TODO: Implement role management
};
const systemMaintenance = () => {
console.log('System maintenance');
// TODO: Implement maintenance mode
};
// Dues management handlers
const loadOverdueCount = async () => {
try {
const response = await $fetch<{ success: boolean; data: { count: number } }>('/api/members/overdue-count');
if (response.success) {
overdueCount.value = response.data.count;
}
} catch (error: any) {
console.error('Error loading overdue count:', error);
}
};
const viewOverdueMembers = () => {
// Navigate to member list with overdue filter applied
navigateTo('/dashboard/member-list');
};
const sendDuesReminders = () => {
// Placeholder for dues reminder functionality
console.log('Send dues reminders - feature to be implemented');
};
const handleStatusesUpdated = async (updatedCount: number) => {
console.log(`Successfully updated ${updatedCount} member${updatedCount !== 1 ? 's' : ''} to inactive status`);
// Refresh overdue count
await loadOverdueCount();
// Trigger banner refresh
overdueRefreshTrigger.value += 1;
};
const handleViewMember = (member: any) => {
// Open the view dialog instead of navigating away
selectedMember.value = member;
showViewDialog.value = true;
};
const handleEditMember = (member: any) => {
// Close the view dialog and open the edit dialog
showViewDialog.value = false;
selectedMember.value = member;
showEditDialog.value = true;
};
const navigateToMembers = () => {
// Navigate to member list page
navigateTo('/dashboard/member-list');
};
const handleMemberUpdated = (member: any) => {
console.log('Member updated:', member.FullName || `${member.first_name} ${member.last_name}`);
// Close edit dialog
showEditDialog.value = false;
// Trigger dues refresh
duesRefreshTrigger.value += 1;
};
// Data management functions
const assignMemberIds = async () => {
assigningMemberIds.value = true;
try {
console.log('Starting member ID assignment...');
const response = await $fetch<{
success: boolean;
message: string;
data: {
totalMembers: number;
membersUpdated: number;
updatedMembers: Array<{
id: string;
name: string;
email: string;
memberId: string;
}>;
startingId: string | null;
endingId: string | null;
};
}>('/api/admin/assign-member-ids', {
method: 'POST'
});
if (response.success) {
console.log('✅ Member ID assignment completed:', {
totalMembers: response.data.totalMembers,
membersUpdated: response.data.membersUpdated,
startingId: response.data.startingId,
endingId: response.data.endingId
});
// Show success message
alert(`Success! Assigned member IDs to ${response.data.membersUpdated} members.\nRange: ${response.data.startingId} to ${response.data.endingId}`);
// Refresh dues management data
duesRefreshTrigger.value += 1;
}
} catch (error: any) {
console.error('❌ Failed to assign member IDs:', error);
alert(`Error: ${error.statusMessage || error.message || 'Failed to assign member IDs'}`);
} finally {
assigningMemberIds.value = false;
}
};
const backfillEventIds = async () => {
backfillLoading.value = true;
try {
console.log('Starting event ID backfill...');
const response = await $fetch<{
success: boolean;
message: string;
data: {
totalEvents: number;
eventsUpdated: number;
};
}>('/api/admin/backfill-event-ids', {
method: 'POST'
});
if (response.success) {
console.log('✅ Event ID backfill completed:', {
totalEvents: response.data.totalEvents,
eventsUpdated: response.data.eventsUpdated
});
// Show success message
alert(`Success! Assigned event IDs to ${response.data.eventsUpdated} events.`);
}
} catch (error: any) {
console.error('❌ Failed to backfill event IDs:', error);
alert(`Error: ${error.statusMessage || error.message || 'Failed to backfill event IDs'}`);
} finally {
backfillLoading.value = false;
}
};
// Load stats and overdue count on component mount
onMounted(async () => {
await loadStats();
await loadOverdueCount();
});
</script>
<style scoped>
.v-card {
border-radius: 12px !important;
}
.v-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15) !important;
transition: all 0.3s ease;
}
.v-btn {
text-transform: none !important;
font-weight: 600;
}
.v-list-item {
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.v-list-item:last-child {
border-bottom: none;
}
</style>

View File

@ -1,350 +0,0 @@
<template>
<v-container>
<!-- Dues Payment Banner -->
<DuesPaymentBanner />
<!-- Welcome Header -->
<v-row class="mb-6">
<v-col>
<h1 class="text-h3 font-weight-bold" style="color: #a31515;">
Welcome Back, {{ firstName }}!
</h1>
<p class="text-h6 text-medium-emphasis">
MonacoUSA Board Portal
</p>
<div class="text-center">
<v-chip color="primary" variant="elevated" class="mt-2">
<v-icon start>mdi-shield-account</v-icon>
Board Member
</v-chip>
</div>
</v-col>
</v-row>
<!-- Board Tools -->
<v-row class="mb-6">
<v-col cols="12" md="6">
<v-card class="pa-4 text-center" elevation="2" hover>
<v-icon size="48" color="primary" class="mb-2">mdi-calendar</v-icon>
<h3 class="mb-2">Events</h3>
<p class="text-body-2 mb-4">View and manage association events</p>
<v-btn
color="primary"
variant="outlined"
style="border-color: #a31515; color: #a31515;"
@click="navigateToEvents"
>
View Events
</v-btn>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card class="pa-4 text-center" elevation="2" hover>
<v-icon size="48" color="primary" class="mb-2">mdi-account-group</v-icon>
<h3 class="mb-2">Members</h3>
<p class="text-body-2 mb-4">View and manage association members</p>
<v-btn
color="primary"
variant="outlined"
style="border-color: #a31515; color: #a31515;"
@click="navigateToMembers"
>
View Members
</v-btn>
</v-card>
</v-col>
</v-row>
<!-- Board Statistics -->
<v-row class="mb-6">
<v-col cols="12" md="8">
<v-card elevation="2">
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
<v-icon class="mr-2" color="primary">mdi-chart-box-outline</v-icon>
Board Overview
</v-card-title>
<v-card-text class="pa-4">
<v-row>
<v-col cols="6" md="3" class="text-center">
<div class="text-h4 font-weight-bold" style="color: #a31515;">{{ stats.totalMembers }}</div>
<div class="text-body-2">Total Members</div>
</v-col>
<v-col cols="6" md="3" class="text-center">
<div class="text-h4 font-weight-bold" style="color: #a31515;">{{ stats.activeMembers }}</div>
<div class="text-body-2">Active Members</div>
</v-col>
<v-col cols="6" md="6" class="text-center">
<div class="text-h4 font-weight-bold" style="color: #a31515;">{{ stats.upcomingEvents }}</div>
<div class="text-body-2">Upcoming Events</div>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card elevation="2">
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
<v-icon class="mr-2" color="primary">mdi-calendar-today</v-icon>
Next Event
</v-card-title>
<v-card-text class="pa-4">
<div class="text-h6 mb-2">{{ nextEvent.title }}</div>
<div class="text-body-2 mb-2">
<v-icon size="small" class="mr-1">mdi-calendar</v-icon>
{{ nextEvent.date }}
</div>
<div class="text-body-2 mb-4">
<v-icon size="small" class="mr-1">mdi-clock</v-icon>
{{ nextEvent.time }}
</div>
<v-btn
color="primary"
variant="outlined"
size="small"
style="border-color: #a31515; color: #a31515;"
@click="viewEventDetails"
>
View Details
</v-btn>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- Dues Management Section -->
<v-row class="mb-6">
<v-col cols="12">
<BoardDuesManagement
:refresh-trigger="duesRefreshTrigger"
@view-member="handleViewMember"
@view-all-members="navigateToMembers"
@member-updated="handleMemberUpdated"
/>
</v-col>
</v-row>
<!-- View Member Dialog -->
<ViewMemberDialog
v-model="showViewDialog"
:member="selectedMember"
@edit="handleEditMember"
/>
<!-- Edit Member Dialog -->
<EditMemberDialog
v-model="showEditDialog"
:member="selectedMember"
@member-updated="handleMemberUpdated"
/>
</v-container>
</template>
<script setup lang="ts">
import type { Member } from '~/utils/types';
definePageMeta({
layout: 'dashboard',
middleware: 'auth'
});
const { firstName, isBoard, isAdmin } = useAuth();
// Check board access on mount
onMounted(() => {
if (!isBoard.value && !isAdmin.value) {
throw createError({
statusCode: 403,
statusMessage: 'Access denied. Board membership required.'
});
}
});
// Dues management state
const duesRefreshTrigger = ref(0);
// Member dialog state
const showViewDialog = ref(false);
const showEditDialog = ref(false);
const selectedMember = ref<Member | null>(null);
// Real data for board dashboard
const stats = ref({
totalMembers: 0,
activeMembers: 0,
upcomingEvents: 0
});
const nextEvent = ref({
id: null,
title: 'Next Event',
date: 'Loading...',
time: 'Loading...',
location: 'TBD',
description: 'Upcoming association event'
});
const isLoading = ref(true);
// Load real data on component mount
onMounted(async () => {
await loadBoardData();
});
const loadBoardData = async () => {
try {
isLoading.value = true;
// Load board statistics
const [statsResponse, meetingResponse] = await Promise.allSettled([
$fetch('/api/board/stats'),
$fetch('/api/board/next-meeting')
]);
// Handle stats response
if (statsResponse.status === 'fulfilled') {
const statsData = statsResponse.value as any;
if (statsData?.success) {
stats.value = {
totalMembers: statsData.data.totalMembers || 0,
activeMembers: statsData.data.activeMembers || 0,
upcomingEvents: statsData.data.upcomingEvents || 0
};
}
}
// Handle next meeting response
if (meetingResponse.status === 'fulfilled') {
const meetingData = meetingResponse.value as any;
if (meetingData?.success) {
nextEvent.value = {
id: meetingData.data.id,
title: meetingData.data.title || 'Next Event',
date: meetingData.data.date || 'TBD',
time: meetingData.data.time || 'TBD',
location: meetingData.data.location || 'TBD',
description: meetingData.data.description || 'Upcoming association event'
};
}
}
} catch (error) {
console.error('Error loading board data:', error);
// Keep fallback values
} finally {
isLoading.value = false;
}
};
const recentActivity = ref([
{
id: 1,
title: 'Monthly Board Meeting',
description: 'Meeting minutes approved and distributed',
type: 'success',
status: 'Completed'
},
{
id: 2,
title: 'Budget Review',
description: 'Q4 financial report under review',
type: 'warning',
status: 'In Progress'
},
{
id: 3,
title: 'Member Application',
description: 'New member application pending approval',
type: 'info',
status: 'Pending'
}
]);
// Dues management handlers
const handleViewMember = (member: Member) => {
// Open the view dialog instead of navigating away
selectedMember.value = member;
showViewDialog.value = true;
};
const handleEditMember = (member: Member) => {
// Close the view dialog and open the edit dialog
showViewDialog.value = false;
selectedMember.value = member;
showEditDialog.value = true;
};
const handleMemberUpdated = (member: Member) => {
console.log('Member updated:', member.FullName || `${member.first_name} ${member.last_name}`);
// Close edit dialog
showEditDialog.value = false;
// Trigger dues refresh to update the lists
duesRefreshTrigger.value += 1;
// You could also update stats here if needed
// stats.value = await fetchUpdatedStats();
};
// Navigation methods
const navigateToEvents = () => {
// Navigate to events page
navigateTo('/dashboard/events');
};
const navigateToMembers = () => {
// Navigate to member list page
navigateTo('/dashboard/member-list');
};
const viewEventDetails = () => {
console.log('View event details');
};
const scheduleNewMeeting = () => {
console.log('Schedule new meeting');
};
const createAnnouncement = () => {
console.log('Create announcement');
};
const generateReport = () => {
console.log('Generate report');
};
</script>
<style scoped>
.v-card {
border-radius: 12px !important;
}
.v-card:hover {
transform: translateY(-2px);
transition: transform 0.2s ease-in-out;
}
.v-btn {
text-transform: none !important;
}
.v-icon {
color: #a31515 !important;
}
h3 {
color: #333;
font-weight: 600;
}
.text-body-2 {
color: #666;
}
.v-chip {
font-weight: 600;
}
</style>

View File

@ -1,613 +0,0 @@
<template>
<div class="dashboard-mockup">
<!-- Header -->
<header class="dashboard-header">
<div class="dashboard-header__content">
<h1
v-motion
:initial="{ opacity: 0, x: -20 }"
:enter="{ opacity: 1, x: 0 }"
class="dashboard-header__title"
>
Welcome back, {{ userName }}
</h1>
<p
v-motion
:initial="{ opacity: 0, x: -20 }"
:enter="{ opacity: 1, x: 0, transition: { delay: 100 } }"
class="dashboard-header__subtitle"
>
Here's what's happening with MonacoUSA today
</p>
</div>
<div class="dashboard-header__actions">
<MonacoButton variant="glass" icon="bell">
Notifications
</MonacoButton>
<MonacoButton variant="primary" icon="plus">
New Event
</MonacoButton>
</div>
</header>
<!-- Stats Grid -->
<section class="dashboard-stats">
<StatsCard
v-for="(stat, index) in stats"
:key="stat.label"
:label="stat.label"
:value="stat.value"
:icon="stat.icon"
:prefix="stat.prefix"
:suffix="stat.suffix"
:trend="stat.trend"
:progress="stat.progress"
:sparkline="stat.sparkline"
:delay="index"
variant="glass"
/>
</section>
<!-- Main Content Grid -->
<div class="dashboard-grid">
<!-- Recent Activity -->
<GlassCard
title="Recent Activity"
variant="glass"
:delay="400"
class="dashboard-activity"
>
<div class="activity-list">
<div
v-for="(activity, index) in recentActivities"
:key="index"
v-motion
:initial="{ opacity: 0, x: -20 }"
:enter="{
opacity: 1,
x: 0,
transition: { delay: 500 + (index * 50) }
}"
class="activity-item"
>
<div class="activity-item__icon">
<span :class="`activity-icon activity-icon--${activity.type}`">
{{ activity.icon }}
</span>
</div>
<div class="activity-item__content">
<p class="activity-item__text">{{ activity.text }}</p>
<span class="activity-item__time">{{ activity.time }}</span>
</div>
</div>
</div>
</GlassCard>
<!-- Upcoming Events -->
<GlassCard
title="Upcoming Events"
variant="glass"
:delay="450"
class="dashboard-events"
>
<div class="events-list">
<div
v-for="(event, index) in upcomingEvents"
:key="index"
v-motion
:initial="{ opacity: 0, y: 20 }"
:enter="{
opacity: 1,
y: 0,
transition: { delay: 550 + (index * 50) }
}"
class="event-card"
>
<div class="event-card__date">
<span class="event-card__day">{{ event.day }}</span>
<span class="event-card__month">{{ event.month }}</span>
</div>
<div class="event-card__content">
<h4 class="event-card__title">{{ event.title }}</h4>
<p class="event-card__location">{{ event.location }}</p>
<div class="event-card__attendees">
<span class="event-card__count">{{ event.attendees }} attending</span>
<MonacoButton variant="ghost" size="sm">
View Details
</MonacoButton>
</div>
</div>
</div>
</div>
</GlassCard>
<!-- Member Status -->
<GlassCard
title="Member Status"
variant="gradient"
:delay="500"
class="dashboard-member-status"
>
<div class="member-status">
<div class="member-status__badge">
<span class="badge badge--active">Active Member</span>
</div>
<div class="member-status__info">
<div class="status-item">
<span class="status-item__label">Dues Status</span>
<span class="status-item__value status-item__value--success">Paid</span>
</div>
<div class="status-item">
<span class="status-item__label">Next Payment</span>
<span class="status-item__value">January 2025</span>
</div>
<div class="status-item">
<span class="status-item__label">Member Since</span>
<span class="status-item__value">March 2023</span>
</div>
</div>
<MonacoButton variant="primary" block>
Manage Membership
</MonacoButton>
</div>
</GlassCard>
<!-- Quick Actions -->
<GlassCard
title="Quick Actions"
variant="glass"
:delay="550"
class="dashboard-actions"
>
<div class="quick-actions">
<button
v-for="(action, index) in quickActions"
:key="action.label"
v-motion
:initial="{ opacity: 0, scale: 0.8 }"
:enter="{
opacity: 1,
scale: 1,
transition: { delay: 600 + (index * 50) }
}"
class="action-button"
>
<span class="action-button__icon">{{ action.icon }}</span>
<span class="action-button__label">{{ action.label }}</span>
</button>
</div>
</GlassCard>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import GlassCard from '~/components/ui/GlassCard.vue'
import MonacoButton from '~/components/ui/MonacoButton.vue'
import StatsCard from '~/components/ui/StatsCard.vue'
const userName = ref('John')
const stats = ref([
{
label: 'Total Members',
value: 1234,
icon: 'users',
trend: { type: 'up', value: 12 },
sparkline: [30, 40, 35, 50, 49, 60, 70, 91, 95]
},
{
label: 'Events This Month',
value: 8,
icon: 'calendar',
suffix: ' events',
trend: { type: 'up', value: 33 }
},
{
label: 'Dues Collected',
value: 45670,
icon: 'dollar',
prefix: '$',
trend: { type: 'up', value: 5 },
progress: 78
},
{
label: 'Active Projects',
value: 12,
icon: 'briefcase',
trend: { type: 'neutral', value: 0 }
}
])
const recentActivities = ref([
{
icon: '👤',
type: 'member',
text: 'New member John Doe joined',
time: '2 hours ago'
},
{
icon: '📅',
type: 'event',
text: 'Summer Gala event created',
time: '5 hours ago'
},
{
icon: '💳',
type: 'payment',
text: 'Sarah Smith paid dues',
time: '1 day ago'
},
{
icon: '📝',
type: 'update',
text: 'Board meeting minutes posted',
time: '2 days ago'
}
])
const upcomingEvents = ref([
{
day: '15',
month: 'DEC',
title: 'Monaco Winter Gala',
location: 'Grand Ballroom',
attendees: 120
},
{
day: '22',
month: 'DEC',
title: 'Board Meeting',
location: 'Conference Room A',
attendees: 15
},
{
day: '31',
month: 'DEC',
title: 'New Year Celebration',
location: 'Monaco Club',
attendees: 200
}
])
const quickActions = ref([
{ icon: '📝', label: 'Register for Event' },
{ icon: '💳', label: 'Pay Dues' },
{ icon: '📊', label: 'View Reports' },
{ icon: '👥', label: 'Member Directory' },
{ icon: '📧', label: 'Send Newsletter' },
{ icon: '⚙️', label: 'Settings' }
])
</script>
<style scoped lang="scss">
.dashboard-mockup {
padding: 2rem;
max-width: 1400px;
margin: 0 auto;
background: linear-gradient(135deg, #fef2f2 0%, #ffffff 100%);
min-height: 100vh;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
&__content {
flex: 1;
}
&__title {
font-size: 2rem;
font-weight: 700;
color: #27272a;
margin: 0 0 0.5rem;
}
&__subtitle {
font-size: 1rem;
color: #6b7280;
margin: 0;
}
&__actions {
display: flex;
gap: 1rem;
}
}
.dashboard-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 1.5rem;
.dashboard-activity {
grid-column: span 8;
}
.dashboard-events {
grid-column: span 4;
}
.dashboard-member-status {
grid-column: span 4;
}
.dashboard-actions {
grid-column: span 8;
}
}
.activity-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.activity-item {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 0.75rem;
border-radius: 8px;
transition: background 0.2s;
&:hover {
background: rgba(220, 38, 38, 0.05);
}
&__icon {
flex-shrink: 0;
}
&__content {
flex: 1;
}
&__text {
margin: 0 0 0.25rem;
color: #27272a;
font-size: 0.875rem;
}
&__time {
font-size: 0.75rem;
color: #6b7280;
}
}
.activity-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: 8px;
font-size: 1rem;
&--member {
background: rgba(59, 130, 246, 0.1);
}
&--event {
background: rgba(168, 85, 247, 0.1);
}
&--payment {
background: rgba(16, 185, 129, 0.1);
}
&--update {
background: rgba(251, 146, 60, 0.1);
}
}
.events-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.event-card {
display: flex;
gap: 1rem;
padding: 1rem;
background: rgba(255, 255, 255, 0.5);
border-radius: 12px;
transition: all 0.2s;
&:hover {
background: rgba(255, 255, 255, 0.7);
transform: translateX(4px);
}
&__date {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 3.5rem;
height: 3.5rem;
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
color: white;
border-radius: 10px;
flex-shrink: 0;
}
&__day {
font-size: 1.25rem;
font-weight: 700;
line-height: 1;
}
&__month {
font-size: 0.75rem;
text-transform: uppercase;
}
&__content {
flex: 1;
}
&__title {
margin: 0 0 0.25rem;
font-size: 1rem;
font-weight: 600;
color: #27272a;
}
&__location {
margin: 0 0 0.5rem;
font-size: 0.875rem;
color: #6b7280;
}
&__attendees {
display: flex;
align-items: center;
justify-content: space-between;
}
&__count {
font-size: 0.75rem;
color: #dc2626;
font-weight: 500;
}
}
.member-status {
display: flex;
flex-direction: column;
gap: 1.5rem;
&__badge {
text-align: center;
}
&__info {
display: flex;
flex-direction: column;
gap: 1rem;
}
}
.badge {
display: inline-block;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 600;
&--active {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
}
}
.status-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background: rgba(255, 255, 255, 0.5);
border-radius: 8px;
&__label {
font-size: 0.875rem;
color: #6b7280;
}
&__value {
font-size: 0.875rem;
font-weight: 600;
color: #27272a;
&--success {
color: #10b981;
}
}
}
.quick-actions {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
.action-button {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 1.5rem 1rem;
background: rgba(255, 255, 255, 0.5);
border: 2px solid transparent;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: rgba(255, 255, 255, 0.8);
border-color: rgba(220, 38, 38, 0.2);
transform: translateY(-2px);
}
&__icon {
font-size: 1.5rem;
}
&__label {
font-size: 0.875rem;
font-weight: 500;
color: #27272a;
}
}
// Responsive
@media (max-width: 1024px) {
.dashboard-grid {
.dashboard-activity,
.dashboard-events,
.dashboard-member-status,
.dashboard-actions {
grid-column: span 12;
}
}
.quick-actions {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.dashboard-header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
&__actions {
width: 100%;
button {
flex: 1;
}
}
}
.dashboard-stats {
grid-template-columns: 1fr;
}
}
</style>

View File

@ -1,304 +0,0 @@
<template>
<v-container>
<!-- Dues Payment Banner -->
<DuesPaymentBanner />
<!-- Welcome Header -->
<v-row class="mb-6">
<v-col>
<h1 class="text-h3 font-weight-bold" style="color: #a31515;">
Welcome Back, {{ firstName }}!
</h1>
<p class="text-h6 text-medium-emphasis">
MonacoUSA Member Portal
</p>
</v-col>
</v-row>
<!-- Quick Actions -->
<v-row class="mb-6">
<v-col cols="12" md="4">
<v-card class="pa-4 text-center" elevation="2" hover>
<v-icon size="48" color="primary" class="mb-2">mdi-account</v-icon>
<h3 class="mb-2">My Profile</h3>
<p class="text-body-2 mb-4">View and update your information</p>
<v-btn
color="primary"
variant="outlined"
style="border-color: #a31515; color: #a31515;"
@click="navigateToProfile"
>
View Profile
</v-btn>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card class="pa-4 text-center" elevation="2" hover>
<v-icon size="48" color="primary" class="mb-2">mdi-calendar</v-icon>
<h3 class="mb-2">Events</h3>
<p class="text-body-2 mb-4">View upcoming association events</p>
<v-btn
color="primary"
variant="outlined"
style="border-color: #a31515; color: #a31515;"
@click="navigateToEvents"
>
View Events
</v-btn>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card class="pa-4 text-center" elevation="2" hover>
<v-icon size="48" color="primary" class="mb-2">mdi-file-document</v-icon>
<h3 class="mb-2">Resources</h3>
<p class="text-body-2 mb-4">Access member resources</p>
<v-btn
color="primary"
variant="outlined"
style="border-color: #a31515; color: #a31515;"
@click="navigateToResources"
>
View Resources
</v-btn>
</v-card>
</v-col>
</v-row>
<!-- Recent Activity Section -->
<v-row class="mb-6">
<v-col cols="12">
<v-card elevation="2">
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
<v-icon class="mr-2" color="primary">mdi-clock-outline</v-icon>
Recent Activity
</v-card-title>
<v-card-text class="pa-4">
<v-list>
<v-list-item>
<v-list-item-content>
<v-list-item-title>Welcome to MonacoUSA Portal!</v-list-item-title>
<v-list-item-subtitle>
You've successfully logged in to your member dashboard.
</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action>
<v-chip color="success" size="small">New</v-chip>
</v-list-item-action>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- Member Information -->
<v-row>
<v-col cols="12" md="6">
<v-card elevation="2">
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
<v-icon class="mr-2" color="primary">mdi-information-outline</v-icon>
Member Information
</v-card-title>
<v-card-text class="pa-4">
<v-skeleton-loader v-if="pending" type="list-item-three-line"></v-skeleton-loader>
<div v-else-if="error" class="text-error text-center pa-4">
<v-icon size="48" class="mb-2">mdi-alert-circle</v-icon>
<div>Failed to load member information</div>
</div>
<v-list v-else>
<v-list-item>
<v-list-item-content>
<v-list-item-title>Member ID</v-list-item-title>
<v-list-item-subtitle>
<v-chip color="primary" size="small" variant="outlined">
{{ memberInfo?.memberId || 'Not assigned' }}
</v-chip>
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item-content>
<v-list-item-title>Name</v-list-item-title>
<v-list-item-subtitle>{{ memberInfo?.fullName || 'Not provided' }}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item-content>
<v-list-item-title>Email</v-list-item-title>
<v-list-item-subtitle>{{ memberInfo?.email || 'Not provided' }}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item-content>
<v-list-item-title>Phone</v-list-item-title>
<v-list-item-subtitle>{{ memberInfo?.phone || 'Not provided' }}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item-content>
<v-list-item-title>Nationality</v-list-item-title>
<v-list-item-subtitle>{{ memberInfo?.nationality || 'Not provided' }}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item-content>
<v-list-item-title>Member Since</v-list-item-title>
<v-list-item-subtitle>{{ memberInfo?.memberSince || 'Not provided' }}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item-content>
<v-list-item-title>Dues Status</v-list-item-title>
<v-list-item-subtitle>
<v-chip
:color="memberInfo?.duesStatus === 'Paid' ? 'success' : 'warning'"
size="small"
>
{{ memberInfo?.duesStatus || 'Unknown' }}
</v-chip>
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item-content>
<v-list-item-title>Last Payment</v-list-item-title>
<v-list-item-subtitle>{{ memberInfo?.lastPayment || 'No payment recorded' }}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item-content>
<v-list-item-title>Member Type</v-list-item-title>
<v-list-item-subtitle>
<v-chip color="primary" size="small">{{ userTier.toUpperCase() }}</v-chip>
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card elevation="2">
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
<v-icon class="mr-2" color="primary">mdi-help-circle-outline</v-icon>
Need Help?
</v-card-title>
<v-card-text class="pa-4">
<p class="mb-4">
If you need assistance or have questions about your membership,
please don't hesitate to contact our support team.
</p>
<v-btn
color="primary"
variant="outlined"
style="border-color: #a31515; color: #a31515;"
@click="contactSupport"
>
<v-icon start>mdi-email</v-icon>
Contact Support
</v-btn>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script setup lang="ts">
import type { Member } from '~/utils/types';
definePageMeta({
layout: 'dashboard',
middleware: 'auth'
});
const { firstName, user, userTier } = useAuth();
// Fetch complete member data
const { data: memberData, pending, error } = await useFetch<{ success: boolean; member: Member }>('/api/auth/session', {
server: false
});
const member = computed(() => memberData.value?.member);
// Format member information for display
const memberInfo = computed(() => {
if (!member.value) return null;
return {
memberId: member.value.member_id || 'Not assigned',
fullName: member.value.FullName || `${member.value.first_name || ''} ${member.value.last_name || ''}`.trim(),
email: member.value.email || 'Not provided',
phone: member.value.FormattedPhone || member.value.phone || 'Not provided',
nationality: member.value.nationality || 'Not provided',
memberSince: member.value.member_since ? new Date(member.value.member_since).toLocaleDateString() : 'Not provided',
duesStatus: member.value.current_year_dues_paid === 'true' ? 'Paid' : 'Outstanding',
membershipStatus: member.value.membership_status || 'Active',
lastPayment: member.value.membership_date_paid ? new Date(member.value.membership_date_paid).toLocaleDateString() : 'No payment recorded',
dueDate: member.value.payment_due_date ? new Date(member.value.payment_due_date).toLocaleDateString() : 'N/A'
};
});
// Navigation methods (placeholder implementations)
const navigateToProfile = () => {
navigateTo('/dashboard/profile');
};
const navigateToEvents = () => {
// TODO: Implement events navigation
console.log('Navigate to events');
};
const navigateToResources = () => {
// TODO: Implement resources navigation
console.log('Navigate to resources');
};
const contactSupport = () => {
const subject = encodeURIComponent('MonacoUSA Portal Support Request');
const body = encodeURIComponent(`Hello,
I need assistance with:
[Please describe your issue]
Member ID: ${memberInfo.value?.memberId || 'Not provided'}
Name: ${memberInfo.value?.fullName || 'Not provided'}
Email: ${memberInfo.value?.email || 'Not provided'}
Thank you!`);
window.open(`mailto:support@monacousa.org?subject=${subject}&body=${body}`, '_self');
};
</script>
<style scoped>
.v-card {
border-radius: 12px !important;
}
.v-card:hover {
transform: translateY(-2px);
transition: transform 0.2s ease-in-out;
}
.v-btn {
text-transform: none !important;
}
.v-icon {
color: #a31515 !important;
}
h3 {
color: #333;
font-weight: 600;
}
.text-body-2 {
color: #666;
}
</style>

View File

@ -1,710 +0,0 @@
<template>
<div class="events-mockup">
<!-- Header -->
<header class="events-header">
<div
v-motion
:initial="{ opacity: 0, y: -20 }"
:enter="{ opacity: 1, y: 0 }"
class="events-header__content"
>
<h1 class="events-header__title">Events</h1>
<p class="events-header__subtitle">Discover and join MonacoUSA events</p>
</div>
<div
v-motion
:initial="{ opacity: 0, y: -20 }"
:enter="{ opacity: 1, y: 0, transition: { delay: 100 } }"
class="events-header__actions"
>
<FloatingInput
v-model="searchQuery"
label="Search events..."
leftIcon="search"
variant="glass"
clearable
/>
<MonacoButton variant="primary" icon="plus">
Create Event
</MonacoButton>
</div>
</header>
<!-- Filter Bar -->
<div
v-motion
:initial="{ opacity: 0, y: 20 }"
:enter="{ opacity: 1, y: 0, transition: { delay: 200 } }"
class="events-filters"
>
<div class="filter-chips">
<button
v-for="filter in filters"
:key="filter.value"
class="filter-chip"
:class="{ 'filter-chip--active': selectedFilter === filter.value }"
@click="selectedFilter = filter.value"
>
{{ filter.label }}
<span v-if="filter.count" class="filter-chip__count">{{ filter.count }}</span>
</button>
</div>
<div class="view-toggles">
<button
class="view-toggle"
:class="{ 'view-toggle--active': viewMode === 'grid' }"
@click="viewMode = 'grid'"
>
<span></span> Grid
</button>
<button
class="view-toggle"
:class="{ 'view-toggle--active': viewMode === 'list' }"
@click="viewMode = 'list'"
>
<span></span> List
</button>
</div>
</div>
<!-- Events Grid/List -->
<div
class="events-container"
:class="`events-container--${viewMode}`"
>
<div
v-for="(event, index) in events"
:key="event.id"
v-motion
:initial="{ opacity: 0, y: 30 }"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 300 + (index * 50),
type: 'spring',
stiffness: 200,
damping: 20
}
}"
class="event-card-full"
:class="{ 'event-card-full--featured': event.featured }"
>
<div class="event-card-full__image">
<img :src="event.image" :alt="event.title" />
<div v-if="event.featured" class="event-card-full__badge">Featured</div>
<div class="event-card-full__date-overlay">
<span class="date-day">{{ event.date.day }}</span>
<span class="date-month">{{ event.date.month }}</span>
</div>
</div>
<div class="event-card-full__content">
<div class="event-card-full__header">
<h3 class="event-card-full__title">{{ event.title }}</h3>
<span class="event-card-full__category">{{ event.category }}</span>
</div>
<p class="event-card-full__description">{{ event.description }}</p>
<div class="event-card-full__meta">
<div class="meta-item">
<span class="meta-icon">📍</span>
<span class="meta-text">{{ event.location }}</span>
</div>
<div class="meta-item">
<span class="meta-icon">🕐</span>
<span class="meta-text">{{ event.time }}</span>
</div>
<div class="meta-item">
<span class="meta-icon">👥</span>
<span class="meta-text">{{ event.attendees }} attending</span>
</div>
</div>
<div class="event-card-full__footer">
<div class="event-card-full__price">
<span v-if="event.price === 0" class="price-free">Free</span>
<span v-else class="price-amount">${{ event.price }}</span>
</div>
<div class="event-card-full__actions">
<MonacoButton variant="ghost" size="sm" icon="heart">
Save
</MonacoButton>
<MonacoButton variant="primary" size="sm">
Register
</MonacoButton>
</div>
</div>
</div>
</div>
</div>
<!-- Load More -->
<div
v-motion
:initial="{ opacity: 0 }"
:enter="{ opacity: 1, transition: { delay: 800 } }"
class="load-more"
>
<MonacoButton variant="glass" icon="refresh" block>
Load More Events
</MonacoButton>
</div>
<!-- Floating Calendar Widget -->
<GlassCard
variant="glass"
class="calendar-widget"
:animated="true"
:delay="900"
>
<h4 class="calendar-widget__title">Quick Calendar</h4>
<div class="calendar-mini">
<div class="calendar-mini__header">
<button class="calendar-nav"></button>
<span class="calendar-month">December 2024</span>
<button class="calendar-nav"></button>
</div>
<div class="calendar-mini__grid">
<div
v-for="day in 31"
:key="day"
class="calendar-day"
:class="{
'calendar-day--event': [5, 12, 15, 22, 31].includes(day),
'calendar-day--today': day === 10
}"
>
{{ day }}
</div>
</div>
</div>
</GlassCard>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import GlassCard from '~/components/ui/GlassCard.vue'
import MonacoButton from '~/components/ui/MonacoButton.vue'
import FloatingInput from '~/components/ui/FloatingInput.vue'
const searchQuery = ref('')
const selectedFilter = ref('all')
const viewMode = ref('grid')
const filters = ref([
{ label: 'All Events', value: 'all', count: 24 },
{ label: 'Upcoming', value: 'upcoming', count: 12 },
{ label: 'This Week', value: 'week', count: 5 },
{ label: 'This Month', value: 'month', count: 8 },
{ label: 'Free', value: 'free', count: 7 },
{ label: 'Members Only', value: 'members', count: 10 }
])
const events = ref([
{
id: 1,
title: 'Monaco Winter Gala 2024',
category: 'Social',
description: 'Join us for an elegant evening celebrating the Monaco-US friendship with fine dining, live entertainment, and networking.',
image: '/api/placeholder/400/250',
date: { day: '15', month: 'DEC' },
time: '7:00 PM - 11:00 PM',
location: 'Grand Ballroom, Downtown',
attendees: 120,
price: 150,
featured: true
},
{
id: 2,
title: 'Business Networking Lunch',
category: 'Networking',
description: 'Connect with fellow Monaco-US business professionals over lunch and expand your network.',
image: '/api/placeholder/400/250',
date: { day: '18', month: 'DEC' },
time: '12:00 PM - 2:00 PM',
location: 'Monaco Club',
attendees: 45,
price: 35,
featured: false
},
{
id: 3,
title: 'Cultural Exchange Workshop',
category: 'Education',
description: 'Learn about Monaco culture, history, and traditions in this interactive workshop.',
image: '/api/placeholder/400/250',
date: { day: '20', month: 'DEC' },
time: '3:00 PM - 5:00 PM',
location: 'Community Center',
attendees: 30,
price: 0,
featured: false
},
{
id: 4,
title: 'New Year Celebration',
category: 'Social',
description: 'Ring in the new year with the MonacoUSA community! Champagne toast, live music, and dancing.',
image: '/api/placeholder/400/250',
date: { day: '31', month: 'DEC' },
time: '9:00 PM - 2:00 AM',
location: 'Monaco Club Rooftop',
attendees: 200,
price: 200,
featured: true
},
{
id: 5,
title: 'Wine Tasting Evening',
category: 'Social',
description: 'Discover exceptional wines from Monaco and France guided by our sommelier.',
image: '/api/placeholder/400/250',
date: { day: '22', month: 'DEC' },
time: '6:00 PM - 9:00 PM',
location: 'Wine Gallery',
attendees: 60,
price: 75,
featured: false
},
{
id: 6,
title: 'Board Meeting',
category: 'Meeting',
description: 'Monthly board meeting to discuss club activities and initiatives.',
image: '/api/placeholder/400/250',
date: { day: '28', month: 'DEC' },
time: '5:00 PM - 7:00 PM',
location: 'Conference Room A',
attendees: 15,
price: 0,
featured: false
}
])
</script>
<style scoped lang="scss">
.events-mockup {
padding: 2rem;
max-width: 1400px;
margin: 0 auto;
background: linear-gradient(135deg, #fef2f2 0%, #ffffff 100%);
min-height: 100vh;
position: relative;
}
.events-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
flex-wrap: wrap;
gap: 1.5rem;
&__content {
flex: 1;
}
&__title {
font-size: 2.5rem;
font-weight: 700;
color: #27272a;
margin: 0 0 0.5rem;
}
&__subtitle {
font-size: 1.125rem;
color: #6b7280;
margin: 0;
}
&__actions {
display: flex;
gap: 1rem;
align-items: center;
.floating-input {
width: 300px;
}
}
}
.events-filters {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding: 1rem;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(20px);
border-radius: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.05);
flex-wrap: wrap;
gap: 1rem;
}
.filter-chips {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.filter-chip {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(255, 255, 255, 0.8);
border: 2px solid transparent;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 500;
color: #6b7280;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: rgba(220, 38, 38, 0.05);
border-color: rgba(220, 38, 38, 0.2);
}
&--active {
background: #dc2626;
color: white;
border-color: #dc2626;
.filter-chip__count {
background: rgba(255, 255, 255, 0.2);
}
}
&__count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.25rem;
height: 1.25rem;
padding: 0 0.25rem;
background: rgba(220, 38, 38, 0.1);
border-radius: 10px;
font-size: 0.75rem;
font-weight: 600;
}
}
.view-toggles {
display: flex;
gap: 0.5rem;
}
.view-toggle {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(255, 255, 255, 0.8);
border: 2px solid transparent;
border-radius: 10px;
font-size: 0.875rem;
font-weight: 500;
color: #6b7280;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: rgba(220, 38, 38, 0.05);
border-color: rgba(220, 38, 38, 0.2);
}
&--active {
background: white;
color: #dc2626;
border-color: #dc2626;
}
}
.events-container {
display: grid;
gap: 1.5rem;
margin-bottom: 2rem;
&--grid {
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
}
&--list {
grid-template-columns: 1fr;
}
}
.event-card-full {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(20px);
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
transform: translateY(-4px);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);
}
&--featured {
border: 2px solid #dc2626;
box-shadow: 0 4px 16px rgba(220, 38, 38, 0.15);
}
&__image {
position: relative;
height: 200px;
overflow: hidden;
background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
&__badge {
position: absolute;
top: 1rem;
left: 1rem;
padding: 0.25rem 0.75rem;
background: #dc2626;
color: white;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
&__date-overlay {
position: absolute;
top: 1rem;
right: 1rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 3.5rem;
height: 3.5rem;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
.date-day {
font-size: 1.25rem;
font-weight: 700;
color: #dc2626;
line-height: 1;
}
.date-month {
font-size: 0.75rem;
color: #6b7280;
text-transform: uppercase;
}
}
&__content {
padding: 1.5rem;
}
&__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.75rem;
}
&__title {
flex: 1;
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: #27272a;
}
&__category {
padding: 0.25rem 0.75rem;
background: rgba(220, 38, 38, 0.1);
color: #dc2626;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 500;
}
&__description {
margin: 0 0 1rem;
font-size: 0.875rem;
color: #6b7280;
line-height: 1.5;
}
&__meta {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 1rem;
}
&__footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 1rem;
border-top: 1px solid rgba(220, 38, 38, 0.1);
}
&__price {
.price-free {
padding: 0.25rem 0.75rem;
background: rgba(16, 185, 129, 0.1);
color: #10b981;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 600;
}
.price-amount {
font-size: 1.25rem;
font-weight: 700;
color: #dc2626;
}
}
&__actions {
display: flex;
gap: 0.5rem;
}
}
.meta-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
color: #6b7280;
.meta-icon {
font-size: 1rem;
}
}
.load-more {
max-width: 400px;
margin: 2rem auto;
}
.calendar-widget {
position: fixed;
bottom: 2rem;
right: 2rem;
width: 280px;
z-index: 10;
&__title {
margin: 0 0 1rem;
font-size: 1rem;
font-weight: 600;
color: #dc2626;
}
}
.calendar-mini {
&__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
&__grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 0.25rem;
}
}
.calendar-nav {
width: 1.5rem;
height: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
color: #dc2626;
cursor: pointer;
border-radius: 4px;
transition: background 0.2s;
&:hover {
background: rgba(220, 38, 38, 0.1);
}
}
.calendar-month {
font-size: 0.875rem;
font-weight: 600;
color: #27272a;
}
.calendar-day {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
color: #6b7280;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: rgba(220, 38, 38, 0.05);
}
&--event {
background: rgba(220, 38, 38, 0.1);
color: #dc2626;
font-weight: 600;
}
&--today {
background: #dc2626;
color: white;
font-weight: 600;
}
}
// Responsive
@media (max-width: 768px) {
.events-header {
&__actions {
width: 100%;
.floating-input {
flex: 1;
}
}
}
.events-container--grid {
grid-template-columns: 1fr;
}
.calendar-widget {
display: none;
}
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff