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 checkout:*)",
|
||||||
"Bash(git branch:*)",
|
"Bash(git branch:*)",
|
||||||
"mcp__zen__consensus",
|
"mcp__zen__consensus",
|
||||||
"mcp___21st-dev_magic__21st_magic_component_refiner"
|
"mcp___21st-dev_magic__21st_magic_component_refiner",
|
||||||
|
"Bash(timeout:*)",
|
||||||
|
"Bash(Copy-Item:*)",
|
||||||
|
"Bash(Remove-Item -Path \"Z:\\Repos\\monacousa-portal\\pages\\glass-bolt-perfect.vue\" -Force)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|
|
||||||
|
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>
|
|
||||||