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>
|
|
@ -97,7 +97,10 @@
|
|||
"Bash(git checkout:*)",
|
||||
"Bash(git branch:*)",
|
||||
"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": [],
|
||||
"ask": []
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 814 KiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 669 KiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 666 KiB |
|
After Width: | Height: | Size: 593 KiB |
|
After Width: | Height: | Size: 829 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 814 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 261 KiB |
|
After Width: | Height: | Size: 335 KiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 953 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"template": "bolt-vite-react-ts"
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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 },
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
|
@ -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: [],
|
||||
};
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -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'],
|
||||
},
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||