Compare commits
1 Commits
main
...
visual-aud
| Author | SHA1 | Date |
|---|---|---|
|
|
e54ef9e596 |
|
|
@ -93,14 +93,7 @@
|
|||
"Read(/Z:\\Repos\\monacousa-portal\\components/**)",
|
||||
"Read(/Z:\\Repos\\monacousa-portal\\server\\api/**)",
|
||||
"Read(/Z:\\Repos\\monacousa-portal\\pages\\admin\\members/**)",
|
||||
"Bash(git pull:*)",
|
||||
"Bash(git checkout:*)",
|
||||
"Bash(git branch:*)",
|
||||
"mcp__zen__consensus",
|
||||
"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)"
|
||||
"Bash(git pull:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 814 KiB |
|
Before Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 669 KiB |
|
Before Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 666 KiB |
|
Before Width: | Height: | Size: 593 KiB |
|
Before Width: | Height: | Size: 829 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 814 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 261 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 330 KiB |
|
Before Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 335 KiB |
|
Before Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 953 KiB |
|
Before Width: | Height: | Size: 426 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"template": "bolt-vite-react-ts"
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
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 },
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,283 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
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>
|
||||
);
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
/** @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: [],
|
||||
};
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
{
|
||||
"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"]
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
{
|
||||
"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"]
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
optimizeDeps: {
|
||||
exclude: ['lucide-react'],
|
||||
},
|
||||
});
|
||||
433
DESIGN-SYSTEM.md
|
|
@ -1,433 +0,0 @@
|
|||
# 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 │
|
||||
│ 280px │ Flexible │
|
||||
│ │ ┌───────────────────────┐ │
|
||||
│ │ │ Header Section │ │
|
||||
│ │ ├───────────────────────┤ │
|
||||
│ │ │ Stats Grid │ │
|
||||
│ │ ├───────────────────────┤ │
|
||||
│ │ │ Content Cards │ │
|
||||
│ │ └───────────────────────┘ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### Enhanced Sidebar Design
|
||||
```scss
|
||||
.enhanced-glass-sidebar {
|
||||
// Subtle glass background with gradient
|
||||
background: linear-gradient(135deg,
|
||||
rgba(255, 255, 255, 0.6) 0%,
|
||||
rgba(248, 249, 250, 0.8) 100%);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.05);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
// Decorative floating orbs (non-interactive)
|
||||
.floating-orb {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(30px);
|
||||
pointer-events: none;
|
||||
animation: float 8s ease-in-out infinite;
|
||||
|
||||
&.orb-1 {
|
||||
top: 20%;
|
||||
right: -50px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: radial-gradient(circle,
|
||||
rgba(220, 38, 38, 0.05), transparent);
|
||||
}
|
||||
|
||||
&.orb-2 {
|
||||
bottom: 30%;
|
||||
left: -30px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: radial-gradient(circle,
|
||||
rgba(220, 38, 38, 0.03), transparent);
|
||||
animation-delay: 3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation Items with Premium Feel
|
||||
.nav-item {
|
||||
border-radius: 12px;
|
||||
margin: 6px 16px;
|
||||
padding: 12px 16px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
// Shimmer effect on hover
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.1),
|
||||
transparent);
|
||||
pointer-events: none;
|
||||
transition: left 0.5s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(220, 38, 38, 0.05);
|
||||
transform: translateX(2px);
|
||||
|
||||
&::after {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
border-left: 3px solid $monaco-red-600;
|
||||
transform: scale(1.02);
|
||||
|
||||
.icon {
|
||||
animation: subtle-pulse 3s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// System Status Indicator
|
||||
.system-status {
|
||||
margin-top: auto;
|
||||
padding: 1rem;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: rgba(16, 185, 129, 0.05);
|
||||
border-radius: 8px;
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #10b981;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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,292 +0,0 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Base layer overrides */
|
||||
@layer base {
|
||||
html {
|
||||
@apply scroll-smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply min-h-screen antialiased;
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background: linear-gradient(135deg, #f5f5f5 0%, #ffffff 100%);
|
||||
}
|
||||
|
||||
/* Typography defaults */
|
||||
h1 {
|
||||
@apply text-4xl font-bold text-gray-800;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-3xl font-bold text-gray-800;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-2xl font-semibold text-gray-700;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply text-xl font-semibold text-gray-700;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply text-gray-600;
|
||||
}
|
||||
}
|
||||
|
||||
/* Component layer */
|
||||
@layer components {
|
||||
/* Bright Glass Card Variants */
|
||||
.glass-card-bright {
|
||||
@apply glass rounded-glass p-6 transition-all duration-300;
|
||||
}
|
||||
|
||||
.glass-card-ultra {
|
||||
@apply glass-ultra rounded-glass p-6 transition-all duration-300;
|
||||
}
|
||||
|
||||
.glass-card-monaco {
|
||||
@apply glass-monaco rounded-glass p-6 transition-all duration-300;
|
||||
}
|
||||
|
||||
/* Glass Stat Card */
|
||||
.glass-stat-card {
|
||||
@apply glass-light rounded-glass p-6 hover:glass-ultra transition-all duration-300 hover:-translate-y-1 hover:shadow-glass-lg;
|
||||
}
|
||||
|
||||
.glass-stat-icon {
|
||||
@apply w-12 h-12 rounded-xl bg-glass-monaco-soft flex items-center justify-center mb-4;
|
||||
}
|
||||
|
||||
.glass-stat-value {
|
||||
@apply text-3xl font-bold text-gray-800 mb-1;
|
||||
}
|
||||
|
||||
.glass-stat-label {
|
||||
@apply text-sm text-gray-600 uppercase tracking-wide;
|
||||
}
|
||||
|
||||
/* Glass Dues Card */
|
||||
.glass-dues-card {
|
||||
@apply glass rounded-glass p-4 hover:shadow-glass-lg transition-all duration-300;
|
||||
}
|
||||
|
||||
.glass-dues-header {
|
||||
@apply flex items-center justify-between mb-3;
|
||||
}
|
||||
|
||||
.glass-dues-avatar {
|
||||
@apply w-10 h-10 rounded-full ring-2 ring-white/60;
|
||||
}
|
||||
|
||||
.glass-dues-name {
|
||||
@apply font-semibold text-gray-800;
|
||||
}
|
||||
|
||||
.glass-dues-amount {
|
||||
@apply text-lg font-bold text-monaco-600;
|
||||
}
|
||||
|
||||
.glass-dues-status {
|
||||
@apply px-3 py-1 rounded-full text-xs font-medium;
|
||||
}
|
||||
|
||||
.glass-dues-status-overdue {
|
||||
@apply bg-red-100 text-red-700;
|
||||
}
|
||||
|
||||
.glass-dues-status-upcoming {
|
||||
@apply bg-amber-100 text-amber-700;
|
||||
}
|
||||
|
||||
/* Glass Button Variants */
|
||||
.btn-glass {
|
||||
@apply glass-button px-6 py-2.5 rounded-xl font-medium text-gray-700 hover:text-monaco-600;
|
||||
}
|
||||
|
||||
.btn-glass-primary {
|
||||
@apply bg-gradient-monaco text-white px-6 py-2.5 rounded-xl font-medium shadow-monaco-sm hover:shadow-monaco transition-all duration-300 hover:-translate-y-0.5;
|
||||
}
|
||||
|
||||
.btn-glass-secondary {
|
||||
@apply glass px-6 py-2.5 rounded-xl font-medium text-monaco-600 border-monaco-200 hover:bg-glass-monaco-soft;
|
||||
}
|
||||
|
||||
.btn-glass-ghost {
|
||||
@apply bg-transparent border-2 border-monaco-600 text-monaco-600 px-6 py-2.5 rounded-xl font-medium hover:bg-glass-monaco-soft transition-all duration-300;
|
||||
}
|
||||
|
||||
/* Glass Navigation Items */
|
||||
.nav-item-glass {
|
||||
@apply flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-200 text-gray-700 hover:bg-glass-monaco-soft hover:text-monaco-600 hover:translate-x-0.5;
|
||||
}
|
||||
|
||||
.nav-item-glass-active {
|
||||
@apply flex items-center gap-3 px-4 py-3 rounded-xl bg-glass-monaco text-monaco-600 font-medium relative;
|
||||
}
|
||||
|
||||
.nav-item-indicator {
|
||||
@apply absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 bg-gradient-monaco rounded-r-full;
|
||||
}
|
||||
|
||||
/* Glass Input Fields */
|
||||
.input-glass {
|
||||
@apply glass-input px-4 py-3 rounded-xl w-full;
|
||||
}
|
||||
|
||||
.select-glass {
|
||||
@apply glass-input px-4 py-3 rounded-xl w-full appearance-none cursor-pointer;
|
||||
}
|
||||
|
||||
.textarea-glass {
|
||||
@apply glass-input px-4 py-3 rounded-xl w-full resize-none;
|
||||
}
|
||||
|
||||
/* Glass Table */
|
||||
.table-glass {
|
||||
@apply glass-ultra rounded-glass overflow-hidden;
|
||||
}
|
||||
|
||||
.table-glass thead {
|
||||
@apply bg-glass-monaco-soft border-b border-white/40;
|
||||
}
|
||||
|
||||
.table-glass th {
|
||||
@apply px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider;
|
||||
}
|
||||
|
||||
.table-glass td {
|
||||
@apply px-6 py-4 text-gray-600;
|
||||
}
|
||||
|
||||
.table-glass tbody tr {
|
||||
@apply border-b border-white/20 hover:bg-glass-monaco-soft transition-colors duration-150;
|
||||
}
|
||||
|
||||
/* Glass Modal/Dialog */
|
||||
.modal-glass {
|
||||
@apply glass-ultra rounded-glass shadow-2xl;
|
||||
}
|
||||
|
||||
.modal-glass-header {
|
||||
@apply px-6 py-4 border-b border-white/40 bg-gradient-glass;
|
||||
}
|
||||
|
||||
.modal-glass-body {
|
||||
@apply px-6 py-4;
|
||||
}
|
||||
|
||||
.modal-glass-footer {
|
||||
@apply px-6 py-4 border-t border-white/40 bg-glass-monaco-soft flex justify-end gap-3;
|
||||
}
|
||||
|
||||
/* Glass Badge */
|
||||
.badge-glass {
|
||||
@apply inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-glass border border-white/60;
|
||||
}
|
||||
|
||||
.badge-glass-monaco {
|
||||
@apply inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-glass-monaco text-monaco-700 border-monaco-200;
|
||||
}
|
||||
|
||||
/* Glass Alert */
|
||||
.alert-glass {
|
||||
@apply glass-light rounded-glass p-4 border-l-4;
|
||||
}
|
||||
|
||||
.alert-glass-success {
|
||||
@apply alert-glass border-green-500 text-green-800;
|
||||
}
|
||||
|
||||
.alert-glass-warning {
|
||||
@apply alert-glass border-amber-500 text-amber-800;
|
||||
}
|
||||
|
||||
.alert-glass-error {
|
||||
@apply alert-glass border-red-500 text-red-800;
|
||||
}
|
||||
|
||||
.alert-glass-info {
|
||||
@apply alert-glass border-blue-500 text-blue-800;
|
||||
}
|
||||
}
|
||||
|
||||
/* Utility layer */
|
||||
@layer utilities {
|
||||
/* Text gradient utilities */
|
||||
.text-gradient-monaco {
|
||||
@apply bg-gradient-monaco bg-clip-text text-transparent;
|
||||
}
|
||||
|
||||
/* Hover lift effect */
|
||||
.hover-lift {
|
||||
@apply transition-transform duration-300 hover:-translate-y-1;
|
||||
}
|
||||
|
||||
/* Hover scale effect */
|
||||
.hover-scale {
|
||||
@apply transition-transform duration-300 hover:scale-105;
|
||||
}
|
||||
|
||||
/* Focus ring Monaco */
|
||||
.focus-monaco {
|
||||
@apply focus:ring-2 focus:ring-monaco-400 focus:ring-offset-2 focus:outline-none;
|
||||
}
|
||||
|
||||
/* Loading skeleton */
|
||||
.skeleton {
|
||||
@apply animate-pulse bg-gradient-to-r from-gray-200 via-gray-300 to-gray-200 rounded-lg;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom animations */
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -1000px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 1000px 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Glass morphism scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(135deg, #b91c1c 0%, #991b1b 100%);
|
||||
}
|
||||
|
||||
/* Reduced motion preferences */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,193 +0,0 @@
|
|||
// Design Tokens for Bolt.ai Style System
|
||||
// Based on consensus from multiple AI models for optimal UI transformation
|
||||
|
||||
// ============================================
|
||||
// Color Palette
|
||||
// ============================================
|
||||
:root {
|
||||
// Primary Colors - Monaco branding
|
||||
--color-primary: #dc2626; // Monaco red for branding
|
||||
--color-primary-light: #fef2f2; // Very light red for accents only
|
||||
--color-primary-soft: rgba(220, 38, 38, 0.05); // Very soft red for hover states
|
||||
|
||||
// Background Colors - Clean white/gray
|
||||
--color-bg-primary: #ffffff;
|
||||
--color-bg-secondary: #f8f9fa;
|
||||
--color-bg-gradient: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); // White to light gray
|
||||
|
||||
// Glass Effects - Subtle transparency
|
||||
--color-glass-white: rgba(255, 255, 255, 0.6);
|
||||
--color-glass-light: rgba(255, 255, 255, 0.8);
|
||||
--color-glass-dark: rgba(0, 0, 0, 0.05);
|
||||
|
||||
// Text Colors
|
||||
--color-text-primary: #27272a;
|
||||
--color-text-secondary: #6b7280;
|
||||
--color-text-muted: #9ca3af;
|
||||
--color-text-white: #ffffff;
|
||||
|
||||
// Status Colors
|
||||
--color-success: #10b981;
|
||||
--color-warning: #f59e0b;
|
||||
--color-error: #ef4444;
|
||||
--color-info: #3b82f6;
|
||||
|
||||
// Border Colors
|
||||
--color-border-light: rgba(0, 0, 0, 0.05);
|
||||
--color-border-medium: rgba(0, 0, 0, 0.1);
|
||||
--color-border-white: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Spacing System (8-point grid)
|
||||
// ============================================
|
||||
$spacing-unit: 8px;
|
||||
|
||||
$space-0: 0;
|
||||
$space-1: $spacing-unit; // 8px
|
||||
$space-2: $spacing-unit * 2; // 16px
|
||||
$space-3: $spacing-unit * 3; // 24px
|
||||
$space-4: $spacing-unit * 4; // 32px
|
||||
$space-5: $spacing-unit * 5; // 40px
|
||||
$space-6: $spacing-unit * 6; // 48px
|
||||
$space-7: $spacing-unit * 7; // 56px
|
||||
$space-8: $spacing-unit * 8; // 64px
|
||||
$space-10: $spacing-unit * 10; // 80px
|
||||
$space-12: $spacing-unit * 12; // 96px
|
||||
|
||||
// ============================================
|
||||
// Typography Scale
|
||||
// ============================================
|
||||
:root {
|
||||
// Font Families
|
||||
--font-primary: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--font-mono: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', monospace;
|
||||
|
||||
// Font Sizes
|
||||
--text-xs: 0.75rem; // 12px
|
||||
--text-sm: 0.875rem; // 14px
|
||||
--text-base: 1rem; // 16px
|
||||
--text-lg: 1.125rem; // 18px
|
||||
--text-xl: 1.25rem; // 20px
|
||||
--text-2xl: 1.5rem; // 24px
|
||||
--text-3xl: 1.875rem; // 30px
|
||||
--text-4xl: 2.25rem; // 36px
|
||||
--text-5xl: 3rem; // 48px
|
||||
|
||||
// Font Weights
|
||||
--font-normal: 400;
|
||||
--font-medium: 500;
|
||||
--font-semibold: 600;
|
||||
--font-bold: 700;
|
||||
--font-extrabold: 800;
|
||||
|
||||
// Line Heights
|
||||
--leading-tight: 1.25;
|
||||
--leading-normal: 1.5;
|
||||
--leading-relaxed: 1.75;
|
||||
|
||||
// Letter Spacing
|
||||
--tracking-tight: -0.02em;
|
||||
--tracking-normal: 0;
|
||||
--tracking-wide: 0.05em;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Border Radius
|
||||
// ============================================
|
||||
:root {
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-xl: 16px;
|
||||
--radius-2xl: 24px;
|
||||
--radius-full: 9999px;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Shadows (Subtle, layered approach)
|
||||
// ============================================
|
||||
:root {
|
||||
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
--shadow-lg: 0 10px 40px rgba(0, 0, 0, 0.08);
|
||||
--shadow-xl: 0 20px 60px rgba(0, 0, 0, 0.12);
|
||||
--shadow-glass: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
--shadow-monaco: 0 8px 24px rgba(220, 38, 38, 0.15);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Glass Effects (Reduced blur for performance)
|
||||
// ============================================
|
||||
:root {
|
||||
--blur-none: 0;
|
||||
--blur-sm: 2px;
|
||||
--blur-md: 4px;
|
||||
--blur-lg: 8px; // Maximum blur, use sparingly
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Transitions (Consistent timing)
|
||||
// ============================================
|
||||
:root {
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-base: 200ms ease;
|
||||
--transition-slow: 300ms ease;
|
||||
--transition-spring: cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Z-Index Scale
|
||||
// ============================================
|
||||
:root {
|
||||
--z-base: 0;
|
||||
--z-dropdown: 10;
|
||||
--z-sticky: 20;
|
||||
--z-overlay: 30;
|
||||
--z-modal: 40;
|
||||
--z-popover: 50;
|
||||
--z-tooltip: 60;
|
||||
--z-max: 9999;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Breakpoints
|
||||
// ============================================
|
||||
$breakpoint-sm: 640px;
|
||||
$breakpoint-md: 768px;
|
||||
$breakpoint-lg: 1024px;
|
||||
$breakpoint-xl: 1280px;
|
||||
$breakpoint-2xl: 1536px;
|
||||
|
||||
// ============================================
|
||||
// Utility Mixins
|
||||
// ============================================
|
||||
@mixin glass-effect($blur: var(--blur-sm), $bg: var(--color-glass-white)) {
|
||||
background: $bg;
|
||||
backdrop-filter: blur($blur);
|
||||
-webkit-backdrop-filter: blur($blur);
|
||||
border: 1px solid var(--color-border-white);
|
||||
}
|
||||
|
||||
@mixin hover-lift() {
|
||||
transition: transform var(--transition-base), box-shadow var(--transition-base);
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin text-gradient() {
|
||||
background: var(--color-bg-gradient);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
@mixin focus-ring() {
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,268 +0,0 @@
|
|||
// Bolt.ai Glass Style Overrides
|
||||
// Ensures all glass components use the subtle bolt.ai design system
|
||||
|
||||
// Import design tokens
|
||||
@import 'design-tokens';
|
||||
|
||||
// ============================================
|
||||
// GLOBAL GLASS STYLES - Bolt.ai Pattern
|
||||
// ============================================
|
||||
|
||||
// All glass elements get the subtle treatment
|
||||
.glass,
|
||||
.glass-light,
|
||||
.glass-ultra,
|
||||
.glass-card,
|
||||
.glass-stat-card,
|
||||
.glass-dues-card,
|
||||
.glass-navbar,
|
||||
.glass-sidebar,
|
||||
.glass-app-bar,
|
||||
.glass-card-bright {
|
||||
// Subtle glass effect like bolt.ai
|
||||
background: rgba(255, 255, 255, 0.6) !important;
|
||||
backdrop-filter: blur(4px) !important;
|
||||
-webkit-backdrop-filter: blur(4px) !important;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05) !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
|
||||
|
||||
// Hover state
|
||||
&:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12) !important;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
// Ultra glass variant (slightly more opaque)
|
||||
.glass-ultra {
|
||||
background: rgba(255, 255, 255, 0.8) !important;
|
||||
backdrop-filter: blur(2px) !important;
|
||||
-webkit-backdrop-filter: blur(2px) !important;
|
||||
}
|
||||
|
||||
// Glass Monaco soft colors - very subtle
|
||||
.bg-glass-monaco-soft,
|
||||
.glass-monaco-soft {
|
||||
background: rgba(220, 38, 38, 0.03) !important; // Very subtle red tint
|
||||
|
||||
&:hover {
|
||||
background: rgba(220, 38, 38, 0.05) !important; // Slightly more on hover
|
||||
}
|
||||
}
|
||||
|
||||
// Border radius consistency
|
||||
.rounded-glass {
|
||||
border-radius: 20px !important;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TEXT COLORS - Dark on Light
|
||||
// ============================================
|
||||
|
||||
// Ensure text is always readable on light backgrounds
|
||||
.glass,
|
||||
.glass-light,
|
||||
.glass-ultra,
|
||||
.glass-card,
|
||||
.glass-stat-card,
|
||||
.glass-dues-card,
|
||||
.glass-navbar,
|
||||
.glass-sidebar,
|
||||
.glass-card-bright {
|
||||
color: var(--color-text-primary, #27272a) !important;
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: var(--color-text-primary, #27272a) !important;
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--color-text-secondary, #6b7280) !important;
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
color: var(--color-text-muted, #9ca3af) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// GRADIENT OVERRIDES - Subtle Bolt.ai Style
|
||||
// ============================================
|
||||
|
||||
// Hero gradients - Clean white/gray
|
||||
.hero-gradient,
|
||||
.bg-gradient-monaco,
|
||||
.gradient-monaco {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%) !important; // White to light gray
|
||||
}
|
||||
|
||||
// Monaco gradient for text
|
||||
.text-gradient-monaco {
|
||||
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%) !important;
|
||||
-webkit-background-clip: text !important;
|
||||
background-clip: text !important;
|
||||
-webkit-text-fill-color: transparent !important;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// BUTTON STYLES - Bolt.ai Pattern
|
||||
// ============================================
|
||||
|
||||
.btn-glass,
|
||||
.btn-glass-primary,
|
||||
.btn-glass-secondary {
|
||||
background: rgba(255, 255, 255, 0.6) !important;
|
||||
backdrop-filter: blur(4px) !important;
|
||||
-webkit-backdrop-filter: blur(4px) !important;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05) !important;
|
||||
color: var(--color-text-primary, #27272a) !important;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08) !important;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.8) !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12) !important;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-glass-primary {
|
||||
border-color: rgba(220, 38, 38, 0.1) !important;
|
||||
color: #dc2626 !important;
|
||||
|
||||
&:hover {
|
||||
background: rgba(254, 242, 242, 0.8) !important;
|
||||
border-color: rgba(220, 38, 38, 0.2) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ANIMATION OVERRIDES - Simple and Subtle
|
||||
// ============================================
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.3s ease-out !important;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove complex animations
|
||||
.animate-float,
|
||||
.animate-bounce,
|
||||
.animate-pulse-glow,
|
||||
.animate-shimmer-slow {
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SHADOW OVERRIDES - Subtle Shadows
|
||||
// ============================================
|
||||
|
||||
.shadow-glass,
|
||||
.shadow-glass-hover,
|
||||
.shadow-glass-lg,
|
||||
.shadow-monaco,
|
||||
.shadow-monaco-sm,
|
||||
.shadow-monaco-intense {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
|
||||
.shadow-glass-hover:hover,
|
||||
.shadow-glass-lg:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12) !important;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SIDEBAR SPECIFIC
|
||||
// ============================================
|
||||
|
||||
.glass-sidebar {
|
||||
background: rgba(255, 255, 255, 0.6) !important;
|
||||
backdrop-filter: blur(4px) !important;
|
||||
-webkit-backdrop-filter: blur(4px) !important;
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.05) !important;
|
||||
box-shadow: 4px 0 12px rgba(0, 0, 0, 0.08) !important;
|
||||
|
||||
.bg-glass-light {
|
||||
background: rgba(255, 255, 255, 0.4) !important;
|
||||
backdrop-filter: blur(2px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// GLOBAL BLUR REDUCTION
|
||||
// ============================================
|
||||
|
||||
// Override any high blur values
|
||||
* {
|
||||
&[style*="blur(60px)"],
|
||||
&[style*="blur(50px)"],
|
||||
&[style*="blur(40px)"],
|
||||
&[style*="blur(30px)"],
|
||||
&[style*="blur(20px)"],
|
||||
&[style*="blur(10px)"] {
|
||||
backdrop-filter: blur(4px) !important;
|
||||
-webkit-backdrop-filter: blur(4px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure maximum blur is 8px
|
||||
.backdrop-blur-3xl,
|
||||
.backdrop-blur-2xl,
|
||||
.backdrop-blur-xl,
|
||||
.backdrop-blur-lg {
|
||||
backdrop-filter: blur(4px) !important;
|
||||
-webkit-backdrop-filter: blur(4px) !important;
|
||||
}
|
||||
|
||||
.backdrop-blur-md {
|
||||
backdrop-filter: blur(4px) !important;
|
||||
-webkit-backdrop-filter: blur(4px) !important;
|
||||
}
|
||||
|
||||
.backdrop-blur-sm {
|
||||
backdrop-filter: blur(2px) !important;
|
||||
-webkit-backdrop-filter: blur(2px) !important;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// RESPONSIVE ADJUSTMENTS
|
||||
// ============================================
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.glass,
|
||||
.glass-light,
|
||||
.glass-ultra,
|
||||
.glass-card {
|
||||
backdrop-filter: blur(2px) !important;
|
||||
-webkit-backdrop-filter: blur(2px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// PERFORMANCE OPTIMIZATIONS
|
||||
// ============================================
|
||||
|
||||
// Reduce motion for better performance
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
// GPU acceleration for transforms
|
||||
.glass,
|
||||
.glass-card,
|
||||
.glass-stat-card {
|
||||
will-change: transform;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
|
@ -1,12 +1,6 @@
|
|||
// MonacoUSA Portal - Main Stylesheet
|
||||
// Based on design-system.md specifications
|
||||
|
||||
// Import design tokens for bolt.ai style
|
||||
@import 'design-tokens';
|
||||
|
||||
// Import bolt.ai glass style overrides
|
||||
@import 'glass-bolt-style';
|
||||
|
||||
// Import component styles
|
||||
@import 'components/dashboards';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,70 +0,0 @@
|
|||
<template>
|
||||
<span>{{ prefix }}{{ displayValue }}{{ suffix }}</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
endVal: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
prefix: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
suffix: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
decimals: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const displayValue = ref(0)
|
||||
|
||||
const countUp = () => {
|
||||
const startTime = Date.now()
|
||||
const startVal = 0
|
||||
const endVal = props.endVal
|
||||
const duration = props.duration * 1000
|
||||
|
||||
const updateCount = () => {
|
||||
const now = Date.now()
|
||||
const progress = Math.min((now - startTime) / duration, 1)
|
||||
|
||||
// Easing function for smooth animation
|
||||
const easeOutQuart = 1 - Math.pow(1 - progress, 4)
|
||||
|
||||
const currentVal = startVal + (endVal - startVal) * easeOutQuart
|
||||
displayValue.value = props.decimals > 0
|
||||
? currentVal.toFixed(props.decimals)
|
||||
: Math.round(currentVal).toLocaleString()
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(updateCount)
|
||||
} else {
|
||||
displayValue.value = props.decimals > 0
|
||||
? endVal.toFixed(props.decimals)
|
||||
: endVal.toLocaleString()
|
||||
}
|
||||
}
|
||||
|
||||
updateCount()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
countUp()
|
||||
})
|
||||
|
||||
watch(() => props.endVal, () => {
|
||||
countUp()
|
||||
})
|
||||
</script>
|
||||
|
|
@ -1,229 +0,0 @@
|
|||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'glass rounded-glass p-5 transition-all duration-300',
|
||||
'hover:shadow-glass-lg hover:-translate-y-0.5 group'
|
||||
]"
|
||||
>
|
||||
<!-- Header Section -->
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<!-- Member Info -->
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="relative">
|
||||
<img
|
||||
:src="member.avatar || '/default-avatar.png'"
|
||||
:alt="member.name"
|
||||
class="w-12 h-12 rounded-full object-cover ring-2 ring-white/60"
|
||||
>
|
||||
<!-- Country Flag Badge -->
|
||||
<div
|
||||
v-if="member.countryCode"
|
||||
class="absolute -bottom-1 -right-1 w-6 h-6 rounded-full overflow-hidden ring-2 ring-white"
|
||||
>
|
||||
<CountryFlag :country="member.countryCode" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-800">{{ member.name }}</h4>
|
||||
<p class="text-sm text-gray-500">Member #{{ member.id }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Badge -->
|
||||
<span
|
||||
:class="[
|
||||
'px-3 py-1 rounded-full text-xs font-medium',
|
||||
status === 'overdue'
|
||||
? 'bg-red-50 text-red-700 border border-red-200'
|
||||
: status === 'upcoming'
|
||||
? 'bg-amber-50 text-amber-700 border border-amber-200'
|
||||
: 'bg-green-50 text-green-700 border border-green-200'
|
||||
]"
|
||||
>
|
||||
{{ statusLabel }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Dues Information -->
|
||||
<div class="space-y-3 mb-4">
|
||||
<!-- Amount -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">Amount Due</span>
|
||||
<span class="text-lg font-bold text-monaco-600">
|
||||
${{ member.dueAmount }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Due Date -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">Due Date</span>
|
||||
<span class="text-sm font-medium text-gray-800">
|
||||
{{ formatDate(member.dueDate) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Days Status -->
|
||||
<div
|
||||
v-if="daysUntilDue !== null"
|
||||
class="flex items-center justify-between"
|
||||
>
|
||||
<span class="text-sm text-gray-600">
|
||||
{{ daysUntilDue > 0 ? 'Days Until Due' : 'Days Overdue' }}
|
||||
</span>
|
||||
<span
|
||||
:class="[
|
||||
'text-sm font-medium',
|
||||
daysUntilDue > 7
|
||||
? 'text-gray-800'
|
||||
: daysUntilDue > 0
|
||||
? 'text-amber-600'
|
||||
: 'text-red-600'
|
||||
]"
|
||||
>
|
||||
{{ Math.abs(daysUntilDue) }} {{ Math.abs(daysUntilDue) === 1 ? 'day' : 'days' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions Section -->
|
||||
<div class="flex gap-2">
|
||||
<!-- Mark as Paid - Subtle Design -->
|
||||
<button
|
||||
@click="$emit('mark-paid', member)"
|
||||
class="flex-1 px-3 py-2 rounded-lg bg-white/50 border border-monaco-200
|
||||
text-monaco-600 text-sm font-medium hover:bg-glass-monaco-soft
|
||||
hover:border-monaco-300 transition-all duration-200
|
||||
flex items-center justify-center gap-2"
|
||||
>
|
||||
<Check class="w-4 h-4" />
|
||||
Mark Paid
|
||||
</button>
|
||||
|
||||
<!-- More Options Dropdown -->
|
||||
<div class="relative">
|
||||
<button
|
||||
@click="showDropdown = !showDropdown"
|
||||
class="p-2 rounded-lg bg-white/50 border border-gray-200
|
||||
text-gray-600 hover:bg-gray-50 hover:border-gray-300
|
||||
transition-all duration-200"
|
||||
>
|
||||
<MoreVertical class="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<!-- Dropdown Menu -->
|
||||
<transition name="dropdown">
|
||||
<div
|
||||
v-if="showDropdown"
|
||||
class="absolute right-0 mt-2 w-48 glass-ultra rounded-xl shadow-lg
|
||||
border border-white/60 overflow-hidden z-50"
|
||||
>
|
||||
<button
|
||||
v-for="action in dropdownActions"
|
||||
:key="action.label"
|
||||
@click="handleAction(action)"
|
||||
class="w-full px-4 py-3 text-left text-sm text-gray-700
|
||||
hover:bg-glass-monaco-soft hover:text-monaco-600
|
||||
transition-colors flex items-center gap-3"
|
||||
>
|
||||
<component :is="action.icon" class="w-4 h-4" />
|
||||
{{ action.label }}
|
||||
</button>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import {
|
||||
Check,
|
||||
MoreVertical,
|
||||
Mail,
|
||||
Phone,
|
||||
FileText,
|
||||
Calendar,
|
||||
AlertCircle
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps({
|
||||
member: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
default: 'upcoming',
|
||||
validator: (value) => ['overdue', 'upcoming', 'paid'].includes(value)
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['mark-paid', 'send-reminder', 'view-details', 'schedule-payment'])
|
||||
|
||||
const showDropdown = ref(false)
|
||||
|
||||
const statusLabel = computed(() => {
|
||||
const labels = {
|
||||
overdue: 'Overdue',
|
||||
upcoming: 'Upcoming',
|
||||
paid: 'Paid'
|
||||
}
|
||||
return labels[props.status] || 'Upcoming'
|
||||
})
|
||||
|
||||
const daysUntilDue = computed(() => {
|
||||
if (!props.member.dueDate) return null
|
||||
|
||||
const today = new Date()
|
||||
const dueDate = new Date(props.member.dueDate)
|
||||
const diffTime = dueDate - today
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
||||
|
||||
return diffDays
|
||||
})
|
||||
|
||||
const dropdownActions = [
|
||||
{ icon: Mail, label: 'Send Reminder', action: 'send-reminder' },
|
||||
{ icon: Calendar, label: 'Schedule Payment', action: 'schedule-payment' },
|
||||
{ icon: FileText, label: 'View Details', action: 'view-details' },
|
||||
{ icon: Phone, label: 'Contact Member', action: 'contact' },
|
||||
{ icon: AlertCircle, label: 'Report Issue', action: 'report' }
|
||||
]
|
||||
|
||||
const handleAction = (action) => {
|
||||
showDropdown.value = false
|
||||
emit(action.action, props.member)
|
||||
}
|
||||
|
||||
const formatDate = (date) => {
|
||||
if (!date) return 'Not set'
|
||||
return new Date(date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
// Placeholder for CountryFlag component
|
||||
const CountryFlag = {
|
||||
name: 'CountryFlag',
|
||||
props: ['country', 'size'],
|
||||
template: '<span></span>'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Dropdown animation */
|
||||
.dropdown-enter-active,
|
||||
.dropdown-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.dropdown-enter-from,
|
||||
.dropdown-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,312 +0,0 @@
|
|||
<template>
|
||||
<aside
|
||||
:class="[
|
||||
'fixed left-0 top-0 h-full z-40 transition-all duration-300 glass-sidebar',
|
||||
isCollapsed ? 'w-20' : 'w-72',
|
||||
isMobile && !isOpen ? '-translate-x-full' : 'translate-x-0'
|
||||
]"
|
||||
>
|
||||
<!-- Logo Section -->
|
||||
<div class="flex items-center justify-between p-6 border-b border-white/20">
|
||||
<div class="flex items-center gap-3">
|
||||
<img
|
||||
src="/MONACOUSA-Flags_376x376.png"
|
||||
alt="MonacoUSA"
|
||||
class="w-10 h-10 rounded-xl shadow-soft"
|
||||
>
|
||||
<transition name="fade">
|
||||
<div v-if="!isCollapsed" class="flex flex-col">
|
||||
<span class="text-lg font-bold text-gradient-monaco">MonacoUSA</span>
|
||||
<span class="text-xs text-gray-500">Board Portal</span>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<!-- Collapse Button (Desktop) -->
|
||||
<button
|
||||
v-if="!isMobile"
|
||||
@click="toggleCollapse"
|
||||
class="p-2 rounded-lg hover:bg-glass-monaco-soft transition-colors"
|
||||
>
|
||||
<ChevronLeft v-if="!isCollapsed" class="w-5 h-5 text-gray-600" />
|
||||
<ChevronRight v-else class="w-5 h-5 text-gray-600" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Section -->
|
||||
<nav class="px-4 py-6 space-y-2 overflow-y-auto scrollbar-thin h-[calc(100%-200px)]">
|
||||
<!-- Main Navigation -->
|
||||
<div class="space-y-1">
|
||||
<SidebarLink
|
||||
v-for="item in mainNavItems"
|
||||
:key="item.path"
|
||||
:to="item.path"
|
||||
:icon="item.icon"
|
||||
:label="item.label"
|
||||
:badge="item.badge"
|
||||
:collapsed="isCollapsed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="my-4 border-t border-white/20"></div>
|
||||
|
||||
<!-- Secondary Navigation -->
|
||||
<div class="space-y-1">
|
||||
<div v-if="!isCollapsed" class="px-3 py-2">
|
||||
<span class="text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
||||
Management
|
||||
</span>
|
||||
</div>
|
||||
<SidebarLink
|
||||
v-for="item in managementItems"
|
||||
:key="item.path"
|
||||
:to="item.path"
|
||||
:icon="item.icon"
|
||||
:label="item.label"
|
||||
:badge="item.badge"
|
||||
:collapsed="isCollapsed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Settings Section -->
|
||||
<div class="mt-6 pt-6 border-t border-white/20 space-y-1">
|
||||
<SidebarLink
|
||||
v-for="item in settingsItems"
|
||||
:key="item.path"
|
||||
:to="item.path"
|
||||
:icon="item.icon"
|
||||
:label="item.label"
|
||||
:collapsed="isCollapsed"
|
||||
/>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- User Profile Section -->
|
||||
<div class="absolute bottom-0 left-0 right-0 p-4 border-t border-white/20 bg-glass-light">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="relative">
|
||||
<img
|
||||
:src="userAvatar"
|
||||
alt="Profile"
|
||||
class="w-10 h-10 rounded-full ring-2 ring-white/60"
|
||||
>
|
||||
<div class="absolute bottom-0 right-0 w-3 h-3 bg-green-500 rounded-full ring-2 ring-white"></div>
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<div v-if="!isCollapsed" class="flex-1">
|
||||
<p class="text-sm font-semibold text-gray-800">{{ userName }}</p>
|
||||
<p class="text-xs text-gray-500">{{ userRole }}</p>
|
||||
</div>
|
||||
</transition>
|
||||
<transition name="fade">
|
||||
<button
|
||||
v-if="!isCollapsed"
|
||||
@click="$emit('logout')"
|
||||
class="p-2 rounded-lg hover:bg-glass-monaco-soft transition-colors"
|
||||
>
|
||||
<LogOut class="w-4 h-4 text-gray-600" />
|
||||
</button>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Close Overlay -->
|
||||
<div
|
||||
v-if="isMobile && isOpen"
|
||||
@click="$emit('close')"
|
||||
class="fixed inset-0 bg-black/20 backdrop-blur-sm -z-10"
|
||||
></div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Users,
|
||||
Calendar,
|
||||
DollarSign,
|
||||
FileText,
|
||||
Mail,
|
||||
BarChart3,
|
||||
Settings,
|
||||
HelpCircle,
|
||||
LogOut,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Bell,
|
||||
Shield,
|
||||
Globe,
|
||||
Briefcase
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
// Component for individual sidebar links
|
||||
import SidebarLink from './SidebarLink.vue'
|
||||
|
||||
const props = defineProps({
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
isMobile: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
userName: {
|
||||
type: String,
|
||||
default: 'Board Member'
|
||||
},
|
||||
userRole: {
|
||||
type: String,
|
||||
default: 'Administrator'
|
||||
},
|
||||
userAvatar: {
|
||||
type: String,
|
||||
default: '/default-avatar.png'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'logout'])
|
||||
|
||||
const isCollapsed = ref(false)
|
||||
const route = useRoute()
|
||||
|
||||
const toggleCollapse = () => {
|
||||
isCollapsed.value = !isCollapsed.value
|
||||
}
|
||||
|
||||
// Navigation items with Lucide icons
|
||||
const mainNavItems = [
|
||||
{
|
||||
path: '/board/dashboard',
|
||||
icon: LayoutDashboard,
|
||||
label: 'Dashboard',
|
||||
badge: null
|
||||
},
|
||||
{
|
||||
path: '/board/members',
|
||||
icon: Users,
|
||||
label: 'Members',
|
||||
badge: '1,234'
|
||||
},
|
||||
{
|
||||
path: '/board/events',
|
||||
icon: Calendar,
|
||||
label: 'Events',
|
||||
badge: '3'
|
||||
},
|
||||
{
|
||||
path: '/board/dues',
|
||||
icon: DollarSign,
|
||||
label: 'Dues & Payments',
|
||||
badge: '12'
|
||||
}
|
||||
]
|
||||
|
||||
const managementItems = [
|
||||
{
|
||||
path: '/board/documents',
|
||||
icon: FileText,
|
||||
label: 'Documents',
|
||||
badge: null
|
||||
},
|
||||
{
|
||||
path: '/board/communications',
|
||||
icon: Mail,
|
||||
label: 'Communications',
|
||||
badge: '5'
|
||||
},
|
||||
{
|
||||
path: '/board/reports',
|
||||
icon: BarChart3,
|
||||
label: 'Reports',
|
||||
badge: null
|
||||
},
|
||||
{
|
||||
path: '/board/governance',
|
||||
icon: Shield,
|
||||
label: 'Governance',
|
||||
badge: null
|
||||
}
|
||||
]
|
||||
|
||||
const settingsItems = [
|
||||
{
|
||||
path: '/board/settings',
|
||||
icon: Settings,
|
||||
label: 'Settings',
|
||||
badge: null
|
||||
},
|
||||
{
|
||||
path: '/board/help',
|
||||
icon: HelpCircle,
|
||||
label: 'Help & Support',
|
||||
badge: null
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Glass Sidebar Styles - Bolt.ai Style */
|
||||
.glass-sidebar {
|
||||
background: rgba(255, 255, 255, 0.6) !important;
|
||||
backdrop-filter: blur(4px) !important;
|
||||
-webkit-backdrop-filter: blur(4px) !important;
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.05);
|
||||
box-shadow: 4px 0 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* Glass effects for internal elements */
|
||||
.bg-glass-light {
|
||||
background: rgba(255, 255, 255, 0.4) !important;
|
||||
backdrop-filter: blur(2px) !important;
|
||||
}
|
||||
|
||||
.bg-glass-monaco-soft {
|
||||
background: rgba(254, 242, 242, 0.6) !important;
|
||||
}
|
||||
|
||||
.hover\:bg-glass-monaco-soft:hover {
|
||||
background: rgba(254, 242, 242, 0.8) !important;
|
||||
}
|
||||
|
||||
/* Fade transition for collapsing elements */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for navigation */
|
||||
.scrollbar-thin::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
background: rgba(220, 38, 38, 0.2);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(220, 38, 38, 0.3);
|
||||
}
|
||||
|
||||
/* Text gradient for branding */
|
||||
.text-gradient-monaco {
|
||||
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,192 +0,0 @@
|
|||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'glass-stat-card group',
|
||||
variant === 'ultra' ? 'glass-ultra' : 'glass-light',
|
||||
'rounded-glass p-6 transition-all duration-300',
|
||||
'hover:-translate-y-1 hover:shadow-glass-lg cursor-pointer'
|
||||
]"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<!-- Icon Section -->
|
||||
<div
|
||||
:class="[
|
||||
'glass-stat-icon',
|
||||
iconBgClass,
|
||||
'w-14 h-14 rounded-2xl flex items-center justify-center mb-4',
|
||||
'transition-transform duration-300 group-hover:scale-110'
|
||||
]"
|
||||
>
|
||||
<component
|
||||
:is="icon"
|
||||
:class="[iconColorClass, 'w-7 h-7']"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Content Section -->
|
||||
<div class="space-y-2">
|
||||
<!-- Label -->
|
||||
<p class="text-sm font-medium text-gray-500 uppercase tracking-wider">
|
||||
{{ label }}
|
||||
</p>
|
||||
|
||||
<!-- Value with Animation -->
|
||||
<div class="flex items-baseline gap-2">
|
||||
<CountUp
|
||||
v-if="animated"
|
||||
:end-val="numericValue"
|
||||
:duration="2"
|
||||
:prefix="prefix"
|
||||
:suffix="suffix"
|
||||
class="text-3xl font-bold text-gray-800"
|
||||
/>
|
||||
<p v-else class="text-3xl font-bold text-gray-800">
|
||||
{{ prefix }}{{ value }}{{ suffix }}
|
||||
</p>
|
||||
|
||||
<!-- Change Indicator -->
|
||||
<div
|
||||
v-if="change"
|
||||
:class="[
|
||||
'flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium',
|
||||
changeType === 'increase'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: changeType === 'decrease'
|
||||
? 'bg-red-100 text-red-700'
|
||||
: 'bg-gray-100 text-gray-700'
|
||||
]"
|
||||
>
|
||||
<TrendingUp v-if="changeType === 'increase'" class="w-3 h-3" />
|
||||
<TrendingDown v-else-if="changeType === 'decrease'" class="w-3 h-3" />
|
||||
<span>{{ change }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<p v-if="description" class="text-sm text-gray-600">
|
||||
{{ description }}
|
||||
</p>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div v-if="showProgress" class="mt-3">
|
||||
<div class="flex justify-between text-xs text-gray-500 mb-1">
|
||||
<span>Progress</span>
|
||||
<span>{{ progressValue }}%</span>
|
||||
</div>
|
||||
<div class="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
:style="{ width: `${progressValue}%` }"
|
||||
class="h-full bg-gradient-monaco rounded-full transition-all duration-500"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Link -->
|
||||
<div
|
||||
v-if="actionLabel"
|
||||
class="mt-4 pt-4 border-t border-white/40 flex items-center justify-between group/link"
|
||||
>
|
||||
<span class="text-sm font-medium text-monaco-600 group-hover/link:text-monaco-700">
|
||||
{{ actionLabel }}
|
||||
</span>
|
||||
<ArrowRight class="w-4 h-4 text-monaco-600 transition-transform group-hover/link:translate-x-1" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { TrendingUp, TrendingDown, ArrowRight } from 'lucide-vue-next'
|
||||
import CountUp from './CountUp.vue'
|
||||
|
||||
const props = defineProps({
|
||||
icon: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
value: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
},
|
||||
prefix: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
suffix: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
change: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
changeType: {
|
||||
type: String,
|
||||
default: null,
|
||||
validator: (value) => ['increase', 'decrease', 'neutral'].includes(value)
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
actionLabel: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'light',
|
||||
validator: (value) => ['light', 'ultra'].includes(value)
|
||||
},
|
||||
iconColor: {
|
||||
type: String,
|
||||
default: 'monaco'
|
||||
},
|
||||
animated: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showProgress: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
progressValue: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['click'])
|
||||
|
||||
const numericValue = computed(() => {
|
||||
if (typeof props.value === 'number') return props.value
|
||||
return parseFloat(props.value.replace(/[^0-9.-]/g, '')) || 0
|
||||
})
|
||||
|
||||
const iconBgClass = computed(() => {
|
||||
const colors = {
|
||||
monaco: 'bg-glass-monaco-soft',
|
||||
green: 'bg-green-50',
|
||||
blue: 'bg-blue-50',
|
||||
amber: 'bg-amber-50',
|
||||
purple: 'bg-purple-50'
|
||||
}
|
||||
return colors[props.iconColor] || colors.monaco
|
||||
})
|
||||
|
||||
const iconColorClass = computed(() => {
|
||||
const colors = {
|
||||
monaco: 'text-monaco-600',
|
||||
green: 'text-green-600',
|
||||
blue: 'text-blue-600',
|
||||
amber: 'text-amber-600',
|
||||
purple: 'text-purple-600'
|
||||
}
|
||||
return colors[props.iconColor] || colors.monaco
|
||||
})
|
||||
</script>
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
<template>
|
||||
<NuxtLink
|
||||
:to="to"
|
||||
:class="[
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-200 group relative',
|
||||
isActive
|
||||
? 'bg-glass-monaco text-monaco-600 font-medium shadow-soft'
|
||||
: 'text-gray-700 hover:bg-glass-monaco-soft hover:text-monaco-600 hover:translate-x-0.5'
|
||||
]"
|
||||
>
|
||||
<!-- Active Indicator -->
|
||||
<div
|
||||
v-if="isActive"
|
||||
class="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 bg-gradient-monaco rounded-r-full"
|
||||
></div>
|
||||
|
||||
<!-- Icon -->
|
||||
<component
|
||||
:is="icon"
|
||||
:class="[
|
||||
'flex-shrink-0 transition-colors',
|
||||
collapsed ? 'w-6 h-6' : 'w-5 h-5',
|
||||
isActive ? 'text-monaco-600' : 'text-gray-500 group-hover:text-monaco-600'
|
||||
]"
|
||||
/>
|
||||
|
||||
<!-- Label and Badge -->
|
||||
<transition name="slide">
|
||||
<div v-if="!collapsed" class="flex-1 flex items-center justify-between">
|
||||
<span class="text-sm font-medium">{{ label }}</span>
|
||||
|
||||
<!-- Badge -->
|
||||
<span
|
||||
v-if="badge"
|
||||
:class="[
|
||||
'px-2 py-0.5 text-xs rounded-full font-medium transition-colors',
|
||||
isActive
|
||||
? 'bg-monaco-600 text-white'
|
||||
: 'bg-glass-monaco-soft text-monaco-700 group-hover:bg-monaco-100'
|
||||
]"
|
||||
>
|
||||
{{ badge }}
|
||||
</span>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<!-- Tooltip for collapsed state -->
|
||||
<div
|
||||
v-if="collapsed"
|
||||
class="absolute left-full ml-2 px-3 py-2 bg-gray-900 text-white text-sm rounded-lg
|
||||
opacity-0 invisible group-hover:opacity-100 group-hover:visible
|
||||
transition-all duration-200 whitespace-nowrap z-50"
|
||||
>
|
||||
{{ label }}
|
||||
<div class="absolute left-0 top-1/2 -translate-x-1 -translate-y-1/2
|
||||
w-0 h-0 border-4 border-transparent border-r-gray-900"></div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const props = defineProps({
|
||||
to: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
icon: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
badge: {
|
||||
type: [String, Number],
|
||||
default: null
|
||||
},
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const isActive = computed(() => {
|
||||
return route.path === props.to || route.path.startsWith(props.to + '/')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Slide transition for label */
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.slide-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
|
||||
.slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -91,34 +91,58 @@ const animationConfig = {
|
|||
overflow: hidden;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
// Variants - Updated to bolt.ai style with reduced blur
|
||||
// Glass effect base
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
padding: 1px;
|
||||
background: linear-gradient(135deg,
|
||||
rgba(255, 255, 255, 0.3) 0%,
|
||||
rgba(255, 255, 255, 0.1) 100%);
|
||||
-webkit-mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Variants
|
||||
&--light {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: #27272a;
|
||||
}
|
||||
|
||||
&--dark {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
backdrop-filter: blur(2px);
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
color: #27272a;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&--colored {
|
||||
background: rgba(254, 242, 242, 0.6);
|
||||
backdrop-filter: blur(2px);
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
border: 1px solid rgba(220, 38, 38, 0.1);
|
||||
background: linear-gradient(135deg,
|
||||
rgba(220, 38, 38, 0.1) 0%,
|
||||
rgba(185, 28, 28, 0.05) 100%);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border: 1px solid rgba(220, 38, 38, 0.2);
|
||||
color: #27272a;
|
||||
}
|
||||
|
||||
&--gradient {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
background: linear-gradient(135deg,
|
||||
rgba(255, 255, 255, 0.8) 0%,
|
||||
rgba(255, 255, 255, 0.4) 100%);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
color: #27272a;
|
||||
}
|
||||
|
||||
|
|
@ -181,10 +205,16 @@ const animationConfig = {
|
|||
}
|
||||
|
||||
&--elevated {
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08);
|
||||
box-shadow:
|
||||
0 10px 40px rgba(0, 0, 0, 0.1),
|
||||
0 2px 10px rgba(0, 0, 0, 0.05),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.12);
|
||||
box-shadow:
|
||||
0 20px 60px rgba(0, 0, 0, 0.15),
|
||||
0 4px 15px rgba(0, 0, 0, 0.08),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -201,7 +231,7 @@ const animationConfig = {
|
|||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: #27272a;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
|
|
|
|||
|
|
@ -149,30 +149,33 @@ defineEmits<{
|
|||
}
|
||||
|
||||
&--glass {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: #27272a;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: #dc2626;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-color: rgba(220, 38, 38, 0.1);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
border-color: rgba(220, 38, 38, 0.2);
|
||||
box-shadow:
|
||||
0 12px 40px rgba(0, 0, 0, 0.15),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.6);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
&--gradient {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||
color: #dc2626;
|
||||
border: 1px solid rgba(220, 38, 38, 0.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 14px rgba(220, 38, 38, 0.25);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
box-shadow: 0 6px 20px rgba(220, 38, 38, 0.35);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,19 +265,6 @@
|
|||
</div>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<!-- System Status Indicator -->
|
||||
<div class="system-status pa-4 mt-auto">
|
||||
<v-card class="status-indicator-card" elevation="0">
|
||||
<v-card-text class="d-flex align-center pa-3">
|
||||
<div class="status-dot"></div>
|
||||
<div class="ml-3">
|
||||
<div class="text-caption font-weight-medium">System Status</div>
|
||||
<div class="text-caption monaco-muted-text">All services operational</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
</v-navigation-drawer>
|
||||
|
||||
|
|
@ -467,59 +454,10 @@ watch(width, (newWidth) => {
|
|||
<style scoped lang="scss">
|
||||
@import '~/assets/scss/main.scss';
|
||||
|
||||
// CSS Variables for Glass Bolt Theme
|
||||
:root {
|
||||
--glass-bg: rgba(255, 255, 255, 0.6);
|
||||
--glass-bg-ultra: rgba(255, 255, 255, 0.8);
|
||||
--glass-blur: 4px;
|
||||
--glass-blur-heavy: 10px;
|
||||
--glass-border: rgba(0, 0, 0, 0.05);
|
||||
--glass-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
--monaco-red: #dc2626;
|
||||
--monaco-red-dark: #b91c1c;
|
||||
--monaco-red-light: rgba(220, 38, 38, 0.1);
|
||||
}
|
||||
|
||||
// Enhanced Glass Drawer with Premium Feel
|
||||
// Glass Drawer Styles
|
||||
.glass-drawer {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(255, 255, 255, 0.7) 0%,
|
||||
rgba(248, 249, 250, 0.85) 100%) !important;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-right: 1px solid var(--glass-border) !important;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
// Floating Orbs (decorative)
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(40px);
|
||||
pointer-events: none;
|
||||
animation: float 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: 20%;
|
||||
right: -60px;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
background: radial-gradient(circle,
|
||||
rgba(220, 38, 38, 0.06), transparent);
|
||||
}
|
||||
|
||||
&::after {
|
||||
bottom: 30%;
|
||||
left: -40px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: radial-gradient(circle,
|
||||
rgba(220, 38, 38, 0.04), transparent);
|
||||
animation-delay: 3s;
|
||||
}
|
||||
@include glass-effect(0.95, 30px);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.2) !important;
|
||||
}
|
||||
|
||||
.glass-logo-section {
|
||||
|
|
@ -536,38 +474,10 @@ watch(width, (newWidth) => {
|
|||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0) translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateY(-15px) translateX(5px);
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px) translateX(-5px);
|
||||
}
|
||||
75% {
|
||||
transform: translateY(-5px) translateX(3px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes subtle-pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-dot {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1.2);
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -587,50 +497,35 @@ watch(width, (newWidth) => {
|
|||
|
||||
.glass-nav-item {
|
||||
border-radius: 12px !important;
|
||||
margin: 6px 16px !important;
|
||||
padding: 12px 16px !important;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin: 4px 12px !important;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
|
||||
// Shimmer effect setup
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.15),
|
||||
transparent);
|
||||
pointer-events: none;
|
||||
transition: left 0.5s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(220, 38, 38, 0.05) !important;
|
||||
transform: translateX(3px);
|
||||
box-shadow: 0 2px 8px rgba(220, 38, 38, 0.1);
|
||||
|
||||
&::after {
|
||||
left: 100%;
|
||||
}
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
&.v-list-item--active {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(220, 38, 38, 0.12) 0%,
|
||||
rgba(220, 38, 38, 0.15) 0%,
|
||||
rgba(220, 38, 38, 0.08) 100%) !important;
|
||||
color: var(--monaco-red) !important;
|
||||
transform: scale(1.02);
|
||||
border-left: 3px solid var(--monaco-red);
|
||||
box-shadow: 0 2px 12px rgba(220, 38, 38, 0.15);
|
||||
color: #dc2626 !important;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 70%;
|
||||
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
.v-icon {
|
||||
color: var(--monaco-red) !important;
|
||||
animation: subtle-pulse 3s ease-in-out infinite;
|
||||
color: #dc2626 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -769,65 +664,4 @@ watch(width, (newWidth) => {
|
|||
margin: 2px 8px 2px 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// System Status Indicator Styles
|
||||
.system-status {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(to top,
|
||||
rgba(255, 255, 255, 0.9),
|
||||
rgba(255, 255, 255, 0));
|
||||
padding-top: 2rem !important;
|
||||
}
|
||||
|
||||
.status-indicator-card {
|
||||
background: rgba(16, 185, 129, 0.05) !important;
|
||||
border: 1px solid rgba(16, 185, 129, 0.1);
|
||||
border-radius: 12px !important;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(16, 185, 129, 0.08) !important;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #10b981;
|
||||
border-radius: 50%;
|
||||
animation: pulse-dot 2s infinite;
|
||||
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
|
||||
}
|
||||
|
||||
// Premium Glass Effects on Navigation Items
|
||||
.glass-nav-item,
|
||||
.glass-nav-item-sub {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.05) 50%,
|
||||
transparent 100%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -523,59 +523,10 @@ watch(width, (newWidth) => {
|
|||
<style scoped lang="scss">
|
||||
@import '~/assets/scss/main.scss';
|
||||
|
||||
// CSS Variables for Glass Bolt Theme
|
||||
:root {
|
||||
--glass-bg: rgba(255, 255, 255, 0.6);
|
||||
--glass-bg-ultra: rgba(255, 255, 255, 0.8);
|
||||
--glass-blur: 4px;
|
||||
--glass-blur-heavy: 10px;
|
||||
--glass-border: rgba(0, 0, 0, 0.05);
|
||||
--glass-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
--monaco-red: #dc2626;
|
||||
--monaco-red-dark: #b91c1c;
|
||||
--monaco-red-light: rgba(220, 38, 38, 0.1);
|
||||
}
|
||||
|
||||
// Enhanced Glass Drawer with Premium Feel
|
||||
// Glass Drawer Styles
|
||||
.glass-drawer {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(255, 255, 255, 0.7) 0%,
|
||||
rgba(248, 249, 250, 0.85) 100%) !important;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-right: 1px solid var(--glass-border) !important;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
// Floating Orbs (decorative)
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(40px);
|
||||
pointer-events: none;
|
||||
animation: float 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: 20%;
|
||||
right: -60px;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
background: radial-gradient(circle,
|
||||
rgba(220, 38, 38, 0.06), transparent);
|
||||
}
|
||||
|
||||
&::after {
|
||||
bottom: 30%;
|
||||
left: -40px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: radial-gradient(circle,
|
||||
rgba(220, 38, 38, 0.04), transparent);
|
||||
animation-delay: 3s;
|
||||
}
|
||||
@include glass-effect(0.95, 30px);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.2) !important;
|
||||
}
|
||||
|
||||
.glass-logo-section {
|
||||
|
|
@ -592,38 +543,10 @@ watch(width, (newWidth) => {
|
|||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0) translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateY(-15px) translateX(5px);
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px) translateX(-5px);
|
||||
}
|
||||
75% {
|
||||
transform: translateY(-5px) translateX(3px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes subtle-pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-dot {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1.2);
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -639,36 +562,12 @@ watch(width, (newWidth) => {
|
|||
|
||||
.glass-nav-item {
|
||||
border-radius: 12px !important;
|
||||
margin: 6px 16px !important;
|
||||
padding: 12px 16px !important;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin: 4px 12px !important;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
|
||||
// Shimmer effect setup
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.15),
|
||||
transparent);
|
||||
pointer-events: none;
|
||||
transition: left 0.5s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(220, 38, 38, 0.05) !important;
|
||||
transform: translateX(3px);
|
||||
box-shadow: 0 2px 8px rgba(220, 38, 38, 0.1);
|
||||
|
||||
&::after {
|
||||
left: 100%;
|
||||
}
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
&.v-list-item--active {
|
||||
|
|
|
|||
|
|
@ -321,60 +321,11 @@ watch(width, (newWidth) => {
|
|||
<style scoped lang="scss">
|
||||
@import '~/assets/scss/main.scss';
|
||||
|
||||
// CSS Variables for Glass Bolt Theme
|
||||
:root {
|
||||
--glass-bg: rgba(255, 255, 255, 0.6);
|
||||
--glass-bg-ultra: rgba(255, 255, 255, 0.8);
|
||||
--glass-blur: 4px;
|
||||
--glass-blur-heavy: 10px;
|
||||
--glass-border: rgba(0, 0, 0, 0.05);
|
||||
--glass-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
--monaco-red: #dc2626;
|
||||
--monaco-red-dark: #b91c1c;
|
||||
--monaco-red-light: rgba(220, 38, 38, 0.1);
|
||||
}
|
||||
|
||||
// Enhanced Glass Drawer with Premium Feel
|
||||
// Enhanced Glass Drawer Styles
|
||||
.enhanced-glass-drawer {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(255, 255, 255, 0.7) 0%,
|
||||
rgba(248, 249, 250, 0.85) 100%) !important;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-right: 1px solid var(--glass-border) !important;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@include enhanced-glass(0.95, 30px);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.2) !important;
|
||||
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
// Floating Orbs (decorative)
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(40px);
|
||||
pointer-events: none;
|
||||
animation: float 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: 20%;
|
||||
right: -60px;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
background: radial-gradient(circle,
|
||||
rgba(220, 38, 38, 0.06), transparent);
|
||||
}
|
||||
|
||||
&::after {
|
||||
bottom: 30%;
|
||||
left: -40px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: radial-gradient(circle,
|
||||
rgba(220, 38, 38, 0.04), transparent);
|
||||
animation-delay: 3s;
|
||||
}
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
|
|
@ -488,43 +439,6 @@ watch(width, (newWidth) => {
|
|||
50% { transform: scale(1.1); }
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0) translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateY(-15px) translateX(5px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px) translateX(-5px);
|
||||
}
|
||||
75% {
|
||||
transform: translateY(-5px) translateX(3px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes subtle-pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-dot {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
// Glass Divider
|
||||
.glass-divider {
|
||||
opacity: 0.2;
|
||||
|
|
|
|||
|
|
@ -15,13 +15,7 @@ export default defineNuxtConfig({
|
|||
}
|
||||
},
|
||||
modules: ["vuetify-nuxt-module", "@vueuse/motion/nuxt"],
|
||||
css: ["~/assets/css/tailwind.css", "~/assets/scss/main.scss"],
|
||||
postcss: {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
},
|
||||
css: ["~/assets/scss/main.scss"],
|
||||
app: {
|
||||
head: {
|
||||
titleTemplate: "%s • MonacoUSA Portal",
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@
|
|||
"vuetify-nuxt-module": "^0.18.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.13",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/formidable": "^3.4.5",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
|
|
@ -53,19 +52,6 @@
|
|||
"sass": "^1.91.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
|
|
@ -2678,17 +2664,6 @@
|
|||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/remapping": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
|
|
@ -2709,9 +2684,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
|
||||
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
|
|
@ -5442,282 +5417,6 @@
|
|||
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.13.tgz",
|
||||
"integrity": "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"enhanced-resolve": "^5.18.3",
|
||||
"jiti": "^2.5.1",
|
||||
"lightningcss": "1.30.1",
|
||||
"magic-string": "^0.30.18",
|
||||
"source-map-js": "^1.2.1",
|
||||
"tailwindcss": "4.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.13.tgz",
|
||||
"integrity": "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.4",
|
||||
"tar": "^7.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tailwindcss/oxide-android-arm64": "4.1.13",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.1.13",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.1.13",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.1.13",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.13",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.13",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.13",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.13",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.13",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.13",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.13.tgz",
|
||||
"integrity": "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.13.tgz",
|
||||
"integrity": "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.13.tgz",
|
||||
"integrity": "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.13.tgz",
|
||||
"integrity": "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.13.tgz",
|
||||
"integrity": "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.13.tgz",
|
||||
"integrity": "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.13.tgz",
|
||||
"integrity": "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.13.tgz",
|
||||
"integrity": "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.13.tgz",
|
||||
"integrity": "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.13.tgz",
|
||||
"integrity": "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==",
|
||||
"bundleDependencies": [
|
||||
"@napi-rs/wasm-runtime",
|
||||
"@emnapi/core",
|
||||
"@emnapi/runtime",
|
||||
"@tybys/wasm-util",
|
||||
"@emnapi/wasi-threads",
|
||||
"tslib"
|
||||
],
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.4.5",
|
||||
"@emnapi/runtime": "^1.4.5",
|
||||
"@emnapi/wasi-threads": "^1.0.4",
|
||||
"@napi-rs/wasm-runtime": "^0.2.12",
|
||||
"@tybys/wasm-util": "^0.10.0",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.13.tgz",
|
||||
"integrity": "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.13.tgz",
|
||||
"integrity": "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.13.tgz",
|
||||
"integrity": "sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@tailwindcss/node": "4.1.13",
|
||||
"@tailwindcss/oxide": "4.1.13",
|
||||
"postcss": "^8.4.41",
|
||||
"tailwindcss": "4.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/virtual-core": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
|
||||
|
|
@ -11511,8 +11210,9 @@
|
|||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
||||
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
|
||||
"devOptional": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
|
|
@ -11543,12 +11243,12 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
|
|
@ -11564,12 +11264,12 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
|
|
@ -11585,12 +11285,12 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
|
|
@ -11606,12 +11306,12 @@
|
|||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
|
|
@ -11627,12 +11327,12 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
|
|
@ -11648,12 +11348,12 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
|
|
@ -11669,12 +11369,12 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
|
|
@ -11690,12 +11390,12 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
|
|
@ -11711,12 +11411,12 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
|
|
@ -11732,12 +11432,12 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
|
|
@ -11991,12 +11691,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.18",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",
|
||||
"integrity": "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==",
|
||||
"version": "0.30.17",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string-ast": {
|
||||
|
|
@ -15579,9 +15279,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
|
||||
"integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz",
|
||||
"integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@
|
|||
"vuetify-nuxt-module": "^0.18.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.13",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/formidable": "^3.4.5",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,721 @@
|
|||
<template>
|
||||
<div class="admin-dashboard-v2">
|
||||
<!-- Neumorphic Header -->
|
||||
<div class="dashboard-header">
|
||||
<h1 class="dashboard-title">System Administration</h1>
|
||||
<p class="dashboard-subtitle">Complete platform control and management</p>
|
||||
</div>
|
||||
|
||||
<!-- Stats Grid with Neumorphic Cards -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card neumorphic-card" v-for="stat in stats" :key="stat.id">
|
||||
<div class="stat-content">
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">{{ stat.label }}</div>
|
||||
<div class="stat-value">{{ stat.value }}</div>
|
||||
<div class="stat-change" :class="stat.changeType">
|
||||
<Icon :name="stat.changeIcon" class="change-icon" />
|
||||
<span>{{ stat.changeText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-icon-wrapper neumorphic-inset">
|
||||
<Icon :name="stat.icon" class="stat-icon" :style="{ color: stat.color }" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Management Sections -->
|
||||
<div class="management-grid">
|
||||
<!-- User Management -->
|
||||
<div class="management-card neumorphic-card">
|
||||
<div class="card-header">
|
||||
<Icon name="mdi:account-group" class="header-icon" />
|
||||
<h2>User Management</h2>
|
||||
</div>
|
||||
<p class="card-description">Manage user accounts, roles, and permissions</p>
|
||||
|
||||
<!-- Morphing Dropdown for User Filters -->
|
||||
<div class="morphing-select-wrapper">
|
||||
<div class="select-trigger neumorphic-button" @click="toggleUserFilter">
|
||||
<span>{{ selectedUserFilter }}</span>
|
||||
<Icon name="mdi:chevron-down" class="dropdown-icon" :class="{ 'rotate': showUserFilter }" />
|
||||
</div>
|
||||
<Transition name="morph">
|
||||
<div v-if="showUserFilter" class="morphing-dropdown">
|
||||
<div
|
||||
v-for="option in userFilterOptions"
|
||||
:key="option"
|
||||
class="dropdown-option"
|
||||
@click="selectUserFilter(option)"
|
||||
>
|
||||
{{ option }}
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button class="neumorphic-button primary" @click="showCreateUserDialog = true">
|
||||
<Icon name="mdi:account-plus" />
|
||||
Create User
|
||||
</button>
|
||||
<button class="neumorphic-button" @click="manageRoles">
|
||||
<Icon name="mdi:shield-account" />
|
||||
Manage Roles
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Maintenance -->
|
||||
<div class="management-card neumorphic-card">
|
||||
<div class="card-header">
|
||||
<Icon name="mdi:cog" class="header-icon" />
|
||||
<h2>System Maintenance</h2>
|
||||
</div>
|
||||
<p class="card-description">Backend operations and system health</p>
|
||||
|
||||
<!-- System Status Indicator -->
|
||||
<div class="system-status neumorphic-inset">
|
||||
<div class="status-indicator" :class="systemStatus.type"></div>
|
||||
<span>{{ systemStatus.text }}</span>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button class="neumorphic-button" @click="assignMemberIds">
|
||||
Assign Member IDs
|
||||
</button>
|
||||
<button class="neumorphic-button" @click="backfillEventIds">
|
||||
Backfill Event IDs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reports & Analytics -->
|
||||
<div class="management-card neumorphic-card">
|
||||
<div class="card-header">
|
||||
<Icon name="mdi:chart-line" class="header-icon" />
|
||||
<h2>Reports & Analytics</h2>
|
||||
</div>
|
||||
<p class="card-description">Generate insights and track metrics</p>
|
||||
|
||||
<!-- Report Type Dropdown -->
|
||||
<div class="morphing-select-wrapper">
|
||||
<div class="select-trigger neumorphic-button" @click="toggleReportType">
|
||||
<span>{{ selectedReportType }}</span>
|
||||
<Icon name="mdi:chevron-down" class="dropdown-icon" :class="{ 'rotate': showReportType }" />
|
||||
</div>
|
||||
<Transition name="morph">
|
||||
<div v-if="showReportType" class="morphing-dropdown">
|
||||
<div
|
||||
v-for="type in reportTypes"
|
||||
:key="type"
|
||||
class="dropdown-option"
|
||||
@click="selectReportType(type)"
|
||||
>
|
||||
{{ type }}
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<button class="neumorphic-button primary full-width" @click="generateReport">
|
||||
<Icon name="mdi:file-chart" />
|
||||
Generate Report
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Configuration -->
|
||||
<div class="management-card neumorphic-card">
|
||||
<div class="card-header">
|
||||
<Icon name="mdi:tune" class="header-icon" />
|
||||
<h2>Configuration</h2>
|
||||
</div>
|
||||
<p class="card-description">Portal settings and integrations</p>
|
||||
|
||||
<div class="config-grid">
|
||||
<button class="config-button neumorphic-button" @click="showMembershipConfig = true">
|
||||
<Icon name="mdi:card-account-details" />
|
||||
<span>Membership</span>
|
||||
</button>
|
||||
<button class="config-button neumorphic-button" @click="showRecaptchaConfig = true">
|
||||
<Icon name="mdi:robot" />
|
||||
<span>reCAPTCHA</span>
|
||||
</button>
|
||||
<button class="config-button neumorphic-button" @click="openEmailConfig">
|
||||
<Icon name="mdi:email" />
|
||||
<span>Email</span>
|
||||
</button>
|
||||
<button class="config-button neumorphic-button" @click="showNocoDBSettings = true">
|
||||
<Icon name="mdi:database" />
|
||||
<span>Database</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Activity Feed -->
|
||||
<div class="activity-section neumorphic-card">
|
||||
<div class="card-header">
|
||||
<Icon name="mdi:timeline" class="header-icon" />
|
||||
<h2>Recent Activity</h2>
|
||||
<button class="neumorphic-button small" @click="refreshActivity">
|
||||
<Icon name="mdi:refresh" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="activity-list">
|
||||
<div v-for="activity in recentActivity" :key="activity.id" class="activity-item neumorphic-inset">
|
||||
<Icon :name="activity.icon" class="activity-icon" :style="{ color: activity.color }" />
|
||||
<div class="activity-content">
|
||||
<p class="activity-text">{{ activity.text }}</p>
|
||||
<span class="activity-time">{{ activity.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
// Define page meta
|
||||
definePageMeta({
|
||||
layout: 'admin',
|
||||
middleware: 'auth'
|
||||
})
|
||||
|
||||
// Stats data
|
||||
const stats = ref([
|
||||
{
|
||||
id: 1,
|
||||
label: 'Total Members',
|
||||
value: '1,247',
|
||||
changeType: 'positive',
|
||||
changeIcon: 'mdi:trending-up',
|
||||
changeText: '+12% this month',
|
||||
icon: 'mdi:account-group',
|
||||
color: '#CC0000'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: 'Active Sessions',
|
||||
value: '342',
|
||||
changeType: 'neutral',
|
||||
changeIcon: 'mdi:circle',
|
||||
changeText: 'Live now',
|
||||
icon: 'mdi:monitor-dashboard',
|
||||
color: '#10B981'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: 'Revenue MTD',
|
||||
value: '$48,392',
|
||||
changeType: 'positive',
|
||||
changeIcon: 'mdi:trending-up',
|
||||
changeText: '+8% vs last month',
|
||||
icon: 'mdi:currency-usd',
|
||||
color: '#3B82F6'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
label: 'System Health',
|
||||
value: '98.5%',
|
||||
changeType: 'positive',
|
||||
changeIcon: 'mdi:check-circle',
|
||||
changeText: 'All systems operational',
|
||||
icon: 'mdi:shield-check',
|
||||
color: '#10B981'
|
||||
}
|
||||
])
|
||||
|
||||
// Dropdown states
|
||||
const showUserFilter = ref(false)
|
||||
const selectedUserFilter = ref('All Users')
|
||||
const userFilterOptions = ref(['All Users', 'Active Users', 'Inactive Users', 'Admins', 'Members'])
|
||||
|
||||
const showReportType = ref(false)
|
||||
const selectedReportType = ref('Financial Report')
|
||||
const reportTypes = ref(['Financial Report', 'Member Report', 'Activity Report', 'Usage Report'])
|
||||
|
||||
// System status
|
||||
const systemStatus = ref({
|
||||
type: 'healthy',
|
||||
text: 'All systems operational'
|
||||
})
|
||||
|
||||
// Recent activity
|
||||
const recentActivity = ref([
|
||||
{
|
||||
id: 1,
|
||||
icon: 'mdi:account-plus',
|
||||
text: 'New member registration: John Doe',
|
||||
time: '2 minutes ago',
|
||||
color: '#10B981'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: 'mdi:credit-card',
|
||||
text: 'Payment received from Jane Smith',
|
||||
time: '15 minutes ago',
|
||||
color: '#3B82F6'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: 'mdi:calendar-check',
|
||||
text: 'Event created: Annual Gala 2024',
|
||||
time: '1 hour ago',
|
||||
color: '#F59E0B'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: 'mdi:account-edit',
|
||||
text: 'Profile updated: Mike Johnson',
|
||||
time: '3 hours ago',
|
||||
color: '#6B7280'
|
||||
}
|
||||
])
|
||||
|
||||
// Dialog states
|
||||
const showCreateUserDialog = ref(false)
|
||||
const showMembershipConfig = ref(false)
|
||||
const showRecaptchaConfig = ref(false)
|
||||
const showNocoDBSettings = ref(false)
|
||||
|
||||
// Methods
|
||||
const toggleUserFilter = () => {
|
||||
showUserFilter.value = !showUserFilter.value
|
||||
showReportType.value = false
|
||||
}
|
||||
|
||||
const selectUserFilter = (option) => {
|
||||
selectedUserFilter.value = option
|
||||
showUserFilter.value = false
|
||||
}
|
||||
|
||||
const toggleReportType = () => {
|
||||
showReportType.value = !showReportType.value
|
||||
showUserFilter.value = false
|
||||
}
|
||||
|
||||
const selectReportType = (type) => {
|
||||
selectedReportType.value = type
|
||||
showReportType.value = false
|
||||
}
|
||||
|
||||
const manageRoles = () => {
|
||||
console.log('Managing roles...')
|
||||
}
|
||||
|
||||
const assignMemberIds = () => {
|
||||
console.log('Assigning member IDs...')
|
||||
}
|
||||
|
||||
const backfillEventIds = () => {
|
||||
console.log('Backfilling event IDs...')
|
||||
}
|
||||
|
||||
const generateReport = () => {
|
||||
console.log('Generating report:', selectedReportType.value)
|
||||
}
|
||||
|
||||
const openEmailConfig = () => {
|
||||
console.log('Opening email configuration...')
|
||||
}
|
||||
|
||||
const refreshActivity = () => {
|
||||
console.log('Refreshing activity...')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Close dropdowns when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.morphing-select-wrapper')) {
|
||||
showUserFilter.value = false
|
||||
showReportType.value = false
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/scss/design-system-v2.scss';
|
||||
|
||||
.admin-dashboard-v2 {
|
||||
padding: 2rem;
|
||||
background: linear-gradient(135deg, $neutral-50 0%, $neutral-100 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
// Header
|
||||
.dashboard-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
|
||||
.dashboard-title {
|
||||
font-size: $text-4xl;
|
||||
font-weight: $font-bold;
|
||||
color: $neutral-800;
|
||||
margin-bottom: 0.5rem;
|
||||
background: linear-gradient(135deg, $primary-600, $primary-800);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.dashboard-subtitle {
|
||||
color: $neutral-600;
|
||||
font-size: $text-lg;
|
||||
}
|
||||
}
|
||||
|
||||
// Stats Grid
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
@include neumorphic-card('md');
|
||||
padding: 1.5rem;
|
||||
transition: all $transition-base;
|
||||
|
||||
&:hover {
|
||||
@include neumorphic-card('lg');
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: $neutral-600;
|
||||
font-size: $text-sm;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: $text-3xl;
|
||||
font-weight: $font-bold;
|
||||
color: $neutral-800;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-change {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: $text-sm;
|
||||
|
||||
&.positive {
|
||||
color: $success-500;
|
||||
}
|
||||
|
||||
&.neutral {
|
||||
color: $neutral-600;
|
||||
}
|
||||
|
||||
.change-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-icon-wrapper {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: $radius-xl;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: $shadow-inset-sm;
|
||||
|
||||
.stat-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Management Grid
|
||||
.management-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.management-card {
|
||||
@include neumorphic-card('md');
|
||||
padding: 2rem;
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.header-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: $primary-600;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: $text-xl;
|
||||
font-weight: $font-semibold;
|
||||
color: $neutral-800;
|
||||
}
|
||||
}
|
||||
|
||||
.card-description {
|
||||
color: $neutral-600;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: $text-sm;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.config-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.config-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
font-size: $text-sm;
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Morphing Dropdown
|
||||
.morphing-select-wrapper {
|
||||
position: relative;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
.select-trigger {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
cursor: pointer;
|
||||
|
||||
.dropdown-icon {
|
||||
transition: transform 0.3s $spring-smooth;
|
||||
|
||||
&.rotate {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.morphing-dropdown {
|
||||
@include morphing-dropdown();
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: $z-dropdown;
|
||||
|
||||
.dropdown-option {
|
||||
padding: 0.75rem 1rem;
|
||||
cursor: pointer;
|
||||
transition: all $transition-fast;
|
||||
color: $neutral-700;
|
||||
|
||||
&: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;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.neumorphic-inset {
|
||||
box-shadow: $shadow-inset-sm;
|
||||
background: linear-gradient(145deg, #e6e6e6, #ffffff);
|
||||
}
|
||||
|
||||
// System Status
|
||||
.system-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
border-radius: $radius-lg;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
|
||||
&.healthy {
|
||||
background-color: $success-500;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background-color: $warning-500;
|
||||
}
|
||||
|
||||
&.error {
|
||||
background-color: $error-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Activity Section
|
||||
.activity-section {
|
||||
@include neumorphic-card('lg');
|
||||
padding: 2rem;
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
h2 {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border-radius: $radius-lg;
|
||||
|
||||
.activity-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
flex: 1;
|
||||
|
||||
.activity-text {
|
||||
color: $neutral-800;
|
||||
font-size: $text-sm;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
color: $neutral-500;
|
||||
font-size: $text-xs;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@include responsive($breakpoint-md) {
|
||||
.admin-dashboard-v2 {
|
||||
padding: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
@include responsive($breakpoint-lg) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
.management-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,996 @@
|
|||
<template>
|
||||
<v-container fluid class="pa-6">
|
||||
<!-- Animated Header with Gradient -->
|
||||
<div class="header-section mb-8">
|
||||
<v-row align="center" justify="space-between">
|
||||
<v-col cols="auto">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar size="56" class="gradient-avatar mr-4 elevation-3">
|
||||
<v-icon size="32" color="white">mdi-account-group</v-icon>
|
||||
</v-avatar>
|
||||
<div>
|
||||
<h1 class="text-h3 font-weight-bold gradient-text">Member Directory</h1>
|
||||
<p class="text-body-1 text-medium-emphasis mt-1">
|
||||
<v-icon size="18" class="mr-1">mdi-account-multiple</v-icon>
|
||||
{{ stats.total }} total members • {{ stats.active }} active
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="auto">
|
||||
<v-btn
|
||||
color="primary"
|
||||
size="large"
|
||||
elevation="3"
|
||||
rounded="lg"
|
||||
prepend-icon="mdi-account-plus"
|
||||
@click="showCreateDialog = true"
|
||||
class="pulse-animation"
|
||||
>
|
||||
Add New Member
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
|
||||
<!-- Enhanced Stats Cards with Glassmorphism -->
|
||||
<v-row class="mb-8">
|
||||
<v-col v-for="stat in statsCards" :key="stat.title" cols="12" sm="6" md="3">
|
||||
<v-card
|
||||
class="stat-card glass-card"
|
||||
elevation="0"
|
||||
:style="`border-left: 4px solid ${stat.color}`"
|
||||
>
|
||||
<v-card-text class="pa-5">
|
||||
<div class="d-flex align-center justify-space-between mb-3">
|
||||
<v-avatar :color="stat.color" size="48" class="elevation-2">
|
||||
<v-icon color="white">{{ stat.icon }}</v-icon>
|
||||
</v-avatar>
|
||||
<v-chip
|
||||
v-if="stat.change"
|
||||
:color="stat.changeType === 'increase' ? 'success' : 'error'"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-icon size="14">
|
||||
{{ stat.changeType === 'increase' ? 'mdi-trending-up' : 'mdi-trending-down' }}
|
||||
</v-icon>
|
||||
{{ stat.change }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<div class="text-h3 font-weight-bold mb-1">{{ stat.value }}</div>
|
||||
<div class="text-body-2 text-medium-emphasis">{{ stat.title }}</div>
|
||||
<v-progress-linear
|
||||
v-if="stat.progress"
|
||||
:model-value="stat.progress"
|
||||
:color="stat.color"
|
||||
height="4"
|
||||
rounded
|
||||
class="mt-3"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Enhanced Search & Filters Bar -->
|
||||
<v-card class="filter-card glass-card mb-6" elevation="0">
|
||||
<v-card-text class="pa-5">
|
||||
<v-row align="center">
|
||||
<v-col cols="12" md="5">
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
label="Search members"
|
||||
placeholder="Name, email, or ID..."
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="solo"
|
||||
density="comfortable"
|
||||
clearable
|
||||
hide-details
|
||||
class="search-field"
|
||||
>
|
||||
<template v-slot:append-inner>
|
||||
<v-badge
|
||||
v-if="searchQuery"
|
||||
:content="filteredMembers.length"
|
||||
color="primary"
|
||||
inline
|
||||
/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="7">
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<v-chip-group
|
||||
v-model="quickFilter"
|
||||
selected-class="chip-active"
|
||||
>
|
||||
<v-chip filter variant="outlined" value="all">
|
||||
<v-icon start size="18">mdi-all-inclusive</v-icon>
|
||||
All Members
|
||||
</v-chip>
|
||||
<v-chip filter variant="outlined" value="active">
|
||||
<v-icon start size="18" color="success">mdi-check-circle</v-icon>
|
||||
Active
|
||||
</v-chip>
|
||||
<v-chip filter variant="outlined" value="dues-pending">
|
||||
<v-icon start size="18" color="warning">mdi-clock-alert</v-icon>
|
||||
Dues Pending
|
||||
</v-chip>
|
||||
<v-chip filter variant="outlined" value="new">
|
||||
<v-icon start size="18" color="info">mdi-new-box</v-icon>
|
||||
New This Month
|
||||
</v-chip>
|
||||
</v-chip-group>
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
variant="outlined"
|
||||
@click="showAdvancedFilters = !showAdvancedFilters"
|
||||
>
|
||||
<v-icon>mdi-filter-variant</v-icon>
|
||||
<v-tooltip activator="parent">Advanced Filters</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
variant="outlined"
|
||||
@click="exportMembers"
|
||||
>
|
||||
<v-icon>mdi-download</v-icon>
|
||||
<v-tooltip activator="parent">Export</v-tooltip>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Advanced Filters (Collapsible) -->
|
||||
<v-expand-transition>
|
||||
<v-row v-if="showAdvancedFilters" class="mt-4">
|
||||
<v-col cols="12" md="3">
|
||||
<v-select
|
||||
v-model="statusFilter"
|
||||
label="Status"
|
||||
:items="statusOptions"
|
||||
variant="solo"
|
||||
density="comfortable"
|
||||
clearable
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-select
|
||||
v-model="membershipFilter"
|
||||
label="Membership Type"
|
||||
:items="membershipOptions"
|
||||
variant="solo"
|
||||
density="comfortable"
|
||||
clearable
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-select
|
||||
v-model="nationalityFilter"
|
||||
label="Nationality"
|
||||
:items="countryOptions"
|
||||
item-title="name"
|
||||
item-value="code"
|
||||
variant="solo"
|
||||
density="comfortable"
|
||||
clearable
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-select
|
||||
v-model="duesFilter"
|
||||
label="Dues Status"
|
||||
:items="['Paid', 'Unpaid', 'Overdue']"
|
||||
variant="solo"
|
||||
density="comfortable"
|
||||
clearable
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expand-transition>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- View Mode Toggle -->
|
||||
<div class="d-flex justify-space-between align-center mb-4">
|
||||
<div class="text-body-1">
|
||||
Showing <strong>{{ filteredMembers.length }}</strong> of {{ members.length }} members
|
||||
</div>
|
||||
<v-btn-toggle
|
||||
v-model="viewMode"
|
||||
mandatory
|
||||
density="comfortable"
|
||||
rounded="lg"
|
||||
color="primary"
|
||||
class="elevation-2"
|
||||
>
|
||||
<v-btn value="cards" prepend-icon="mdi-view-grid">
|
||||
Cards
|
||||
</v-btn>
|
||||
<v-btn value="table" prepend-icon="mdi-table">
|
||||
Table
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
|
||||
<!-- Enhanced Card View -->
|
||||
<transition-group
|
||||
v-if="viewMode === 'cards'"
|
||||
name="card-list"
|
||||
tag="div"
|
||||
class="row"
|
||||
>
|
||||
<v-col
|
||||
v-for="member in paginatedMembers"
|
||||
:key="member.member_id"
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
lg="3"
|
||||
>
|
||||
<v-card
|
||||
class="member-card glass-card h-100"
|
||||
elevation="0"
|
||||
@click="viewMember(member)"
|
||||
>
|
||||
<!-- Card Header with Gradient Background -->
|
||||
<div class="card-header gradient-bg pa-4 text-center">
|
||||
<ProfileAvatar
|
||||
:member-id="member.member_id"
|
||||
:first-name="member.first_name"
|
||||
:last-name="member.last_name"
|
||||
size="80"
|
||||
class="mb-3 mx-auto elevation-4 white-border"
|
||||
/>
|
||||
<h3 class="text-h6 font-weight-bold white--text">
|
||||
{{ member.first_name }} {{ member.last_name }}
|
||||
</h3>
|
||||
<div class="text-caption white--text opacity-90">
|
||||
{{ member.member_id || 'Pending ID' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-card-text class="pa-4">
|
||||
<!-- Contact Info -->
|
||||
<div class="info-row mb-3">
|
||||
<v-icon size="18" class="mr-2 text-medium-emphasis">mdi-email</v-icon>
|
||||
<span class="text-body-2 text-truncate">{{ member.email }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Nationality with Flag -->
|
||||
<div class="info-row mb-3">
|
||||
<v-icon size="18" class="mr-2 text-medium-emphasis">mdi-flag</v-icon>
|
||||
<MultipleCountryFlags
|
||||
:nationality="member.nationality"
|
||||
:show-name="true"
|
||||
size="small"
|
||||
fallback-text="Not specified"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Member Since -->
|
||||
<div class="info-row mb-3">
|
||||
<v-icon size="18" class="mr-2 text-medium-emphasis">mdi-calendar</v-icon>
|
||||
<span class="text-body-2">Since {{ formatDate(member.join_date) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Status Badges -->
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<v-chip
|
||||
:color="member.status === 'active' ? 'success' : 'error'"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
label
|
||||
>
|
||||
<v-icon start size="14">
|
||||
{{ member.status === 'active' ? 'mdi-check' : 'mdi-close' }}
|
||||
</v-icon>
|
||||
{{ member.status }}
|
||||
</v-chip>
|
||||
|
||||
<v-chip
|
||||
:color="getDuesChipColor(member)"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
label
|
||||
>
|
||||
<v-icon start size="14">mdi-cash</v-icon>
|
||||
{{ member.dues_paid_this_year ? 'Dues Paid' : 'Dues Pending' }}
|
||||
</v-chip>
|
||||
|
||||
<v-chip
|
||||
v-if="member.membership_type !== 'Standard'"
|
||||
color="purple"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
label
|
||||
>
|
||||
{{ member.membership_type }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<v-card-actions class="pa-3 pt-0">
|
||||
<v-btn
|
||||
v-if="!member.dues_paid_this_year"
|
||||
color="success"
|
||||
variant="flat"
|
||||
size="small"
|
||||
block
|
||||
rounded
|
||||
@click.stop="markDuesPaid(member)"
|
||||
>
|
||||
<v-icon start>mdi-check</v-icon>
|
||||
Mark Dues Paid
|
||||
</v-btn>
|
||||
<v-row v-else dense>
|
||||
<v-col cols="4">
|
||||
<v-btn
|
||||
icon="mdi-eye"
|
||||
size="small"
|
||||
variant="text"
|
||||
block
|
||||
@click.stop="viewMember(member)"
|
||||
>
|
||||
<v-tooltip activator="parent">View</v-tooltip>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
variant="text"
|
||||
block
|
||||
@click.stop="editMember(member)"
|
||||
>
|
||||
<v-tooltip activator="parent">Edit</v-tooltip>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<v-btn
|
||||
icon="mdi-email"
|
||||
size="small"
|
||||
variant="text"
|
||||
block
|
||||
@click.stop="sendEmail(member)"
|
||||
>
|
||||
<v-tooltip activator="parent">Email</v-tooltip>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</transition-group>
|
||||
|
||||
<!-- Enhanced Table View -->
|
||||
<v-card v-else-if="viewMode === 'table'" class="glass-card" elevation="0">
|
||||
<v-data-table
|
||||
:headers="tableHeaders"
|
||||
:items="filteredMembers"
|
||||
:search="searchQuery"
|
||||
:loading="loading"
|
||||
class="modern-table"
|
||||
hover
|
||||
:items-per-page="15"
|
||||
@click:row="(e, { item }) => viewMember(item)"
|
||||
>
|
||||
<template v-slot:item.member="{ item }">
|
||||
<div class="d-flex align-center py-3">
|
||||
<ProfileAvatar
|
||||
:member-id="item.member_id"
|
||||
:first-name="item.first_name"
|
||||
:last-name="item.last_name"
|
||||
size="40"
|
||||
class="mr-3"
|
||||
/>
|
||||
<div>
|
||||
<div class="font-weight-medium">
|
||||
{{ item.first_name }} {{ item.last_name }}
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
{{ item.member_id || 'Pending ID' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.contact="{ item }">
|
||||
<div class="py-2">
|
||||
<div class="d-flex align-center mb-1">
|
||||
<v-icon size="14" class="mr-1">mdi-email</v-icon>
|
||||
<a :href="`mailto:${item.email}`" class="text-primary text-decoration-none" @click.stop>
|
||||
{{ item.email }}
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="item.phone" class="d-flex align-center text-caption">
|
||||
<v-icon size="14" class="mr-1">mdi-phone</v-icon>
|
||||
{{ item.phone }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.nationality="{ item }">
|
||||
<MultipleCountryFlags
|
||||
:nationality="item.nationality"
|
||||
:show-name="true"
|
||||
size="small"
|
||||
fallback-text="—"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.membership="{ item }">
|
||||
<div class="py-2">
|
||||
<v-chip
|
||||
:color="getMembershipColor(item.membership_type)"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
label
|
||||
class="mb-1"
|
||||
>
|
||||
{{ item.membership_type }}
|
||||
</v-chip>
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
Since {{ formatDate(item.join_date) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.status="{ item }">
|
||||
<div class="d-flex gap-2">
|
||||
<v-chip
|
||||
:color="item.status === 'active' ? 'success' : 'error'"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
label
|
||||
>
|
||||
{{ item.status }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
:color="getDuesChipColor(item)"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
label
|
||||
>
|
||||
{{ item.dues_paid_this_year ? 'Paid' : 'Due' }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<div class="d-flex gap-1">
|
||||
<v-btn
|
||||
icon="mdi-eye"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
@click.stop="viewMember(item)"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
@click.stop="editMember(item)"
|
||||
/>
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
icon="mdi-dots-vertical"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
v-bind="props"
|
||||
@click.stop
|
||||
/>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item @click="sendEmail(item)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="small">mdi-email</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Send Email</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-if="!item.dues_paid_this_year"
|
||||
@click="markDuesPaid(item)"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="small" color="success">mdi-check</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Mark Dues Paid</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="viewPaymentHistory(item)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="small">mdi-history</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Payment History</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider />
|
||||
<v-list-item
|
||||
@click="toggleStatus(item)"
|
||||
:class="item.status === 'active' ? 'text-error' : 'text-success'"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="small">
|
||||
{{ item.status === 'active' ? 'mdi-account-off' : 'mdi-account-check' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>
|
||||
{{ item.status === 'active' ? 'Deactivate' : 'Activate' }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
|
||||
<!-- Pagination -->
|
||||
<v-card
|
||||
v-if="viewMode === 'cards' && filteredMembers.length > itemsPerPage"
|
||||
class="mt-6 glass-card"
|
||||
elevation="0"
|
||||
>
|
||||
<v-card-text>
|
||||
<v-pagination
|
||||
v-model="currentPage"
|
||||
:length="Math.ceil(filteredMembers.length / itemsPerPage)"
|
||||
:total-visible="7"
|
||||
rounded="circle"
|
||||
color="primary"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- Dialogs -->
|
||||
<ViewMemberDialog
|
||||
v-model="showViewDialog"
|
||||
:member="selectedMember"
|
||||
@edit="handleEditMember"
|
||||
@mark-dues-paid="handleMarkDuesPaid"
|
||||
/>
|
||||
|
||||
<EditMemberDialog
|
||||
v-model="showEditDialog"
|
||||
:member="selectedMember"
|
||||
@member-updated="handleMemberUpdated"
|
||||
/>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Member } from '~/utils/types';
|
||||
import { countries } from '~/utils/countries';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'admin',
|
||||
middleware: 'admin'
|
||||
});
|
||||
|
||||
// State
|
||||
const loading = ref(false);
|
||||
const members = ref<Member[]>([]);
|
||||
const searchQuery = ref('');
|
||||
const quickFilter = ref('all');
|
||||
const statusFilter = ref(null);
|
||||
const membershipFilter = ref(null);
|
||||
const nationalityFilter = ref(null);
|
||||
const duesFilter = ref(null);
|
||||
const viewMode = ref('cards');
|
||||
const currentPage = ref(1);
|
||||
const itemsPerPage = 12;
|
||||
const showAdvancedFilters = ref(false);
|
||||
const showViewDialog = ref(false);
|
||||
const showEditDialog = ref(false);
|
||||
const showCreateDialog = ref(false);
|
||||
const selectedMember = ref<Member | null>(null);
|
||||
|
||||
// Stats
|
||||
const stats = ref({
|
||||
total: 0,
|
||||
active: 0,
|
||||
paidThisYear: 0,
|
||||
duesOutstanding: 0,
|
||||
newThisMonth: 0
|
||||
});
|
||||
|
||||
// Computed stats cards
|
||||
const statsCards = computed(() => [
|
||||
{
|
||||
title: 'Total Members',
|
||||
value: stats.value.total,
|
||||
icon: 'mdi-account-group',
|
||||
color: '#3b82f6',
|
||||
change: '+12',
|
||||
changeType: 'increase'
|
||||
},
|
||||
{
|
||||
title: 'Active Members',
|
||||
value: stats.value.active,
|
||||
icon: 'mdi-account-check',
|
||||
color: '#10b981',
|
||||
progress: Math.round((stats.value.active / stats.value.total) * 100)
|
||||
},
|
||||
{
|
||||
title: 'Dues Paid',
|
||||
value: stats.value.paidThisYear,
|
||||
icon: 'mdi-cash-check',
|
||||
color: '#8b5cf6',
|
||||
progress: Math.round((stats.value.paidThisYear / stats.value.total) * 100)
|
||||
},
|
||||
{
|
||||
title: 'New This Month',
|
||||
value: stats.value.newThisMonth,
|
||||
icon: 'mdi-account-plus',
|
||||
color: '#f59e0b',
|
||||
change: '+8',
|
||||
changeType: 'increase'
|
||||
}
|
||||
]);
|
||||
|
||||
// Options
|
||||
const statusOptions = ['active', 'inactive'];
|
||||
const membershipOptions = ['Standard', 'Premium', 'VIP', 'Lifetime'];
|
||||
const countryOptions = countries;
|
||||
|
||||
// Table headers
|
||||
const tableHeaders = [
|
||||
{ title: 'Member', key: 'member', sortable: true },
|
||||
{ title: 'Contact', key: 'contact', sortable: true },
|
||||
{ title: 'Nationality', key: 'nationality', sortable: true },
|
||||
{ title: 'Membership', key: 'membership', sortable: true },
|
||||
{ title: 'Status', key: 'status', sortable: true },
|
||||
{ title: '', key: 'actions', sortable: false, align: 'end' }
|
||||
];
|
||||
|
||||
// Computed
|
||||
const filteredMembers = computed(() => {
|
||||
let filtered = [...members.value];
|
||||
|
||||
// Apply quick filter
|
||||
if (quickFilter.value === 'active') {
|
||||
filtered = filtered.filter(m => m.status === 'active');
|
||||
} else if (quickFilter.value === 'dues-pending') {
|
||||
filtered = filtered.filter(m => !m.dues_paid_this_year);
|
||||
} else if (quickFilter.value === 'new') {
|
||||
const thisMonth = new Date().getMonth();
|
||||
filtered = filtered.filter(m => {
|
||||
const joinDate = new Date(m.join_date);
|
||||
return joinDate.getMonth() === thisMonth;
|
||||
});
|
||||
}
|
||||
|
||||
// Apply advanced filters
|
||||
if (statusFilter.value) {
|
||||
filtered = filtered.filter(m => m.status === statusFilter.value);
|
||||
}
|
||||
|
||||
if (membershipFilter.value) {
|
||||
filtered = filtered.filter(m => m.membership_type === membershipFilter.value);
|
||||
}
|
||||
|
||||
if (nationalityFilter.value) {
|
||||
filtered = filtered.filter(m => m.nationality === nationalityFilter.value);
|
||||
}
|
||||
|
||||
if (duesFilter.value) {
|
||||
if (duesFilter.value === 'Paid') {
|
||||
filtered = filtered.filter(m => m.dues_paid_this_year);
|
||||
} else if (duesFilter.value === 'Unpaid' || duesFilter.value === 'Overdue') {
|
||||
filtered = filtered.filter(m => !m.dues_paid_this_year);
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
});
|
||||
|
||||
const paginatedMembers = computed(() => {
|
||||
const start = (currentPage.value - 1) * itemsPerPage;
|
||||
const end = start + itemsPerPage;
|
||||
return filteredMembers.value.slice(start, end);
|
||||
});
|
||||
|
||||
// Methods
|
||||
const formatDate = (date: string) => {
|
||||
if (!date) return 'N/A';
|
||||
const parsedDate = new Date(date);
|
||||
if (isNaN(parsedDate.getTime())) return 'N/A';
|
||||
return parsedDate.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
const getMembershipColor = (type: string) => {
|
||||
switch (type) {
|
||||
case 'VIP': return 'error';
|
||||
case 'Premium': return 'warning';
|
||||
case 'Lifetime': return 'purple';
|
||||
default: return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
const getDuesChipColor = (member: Member) => {
|
||||
return member.dues_paid_this_year ? 'success' : 'warning';
|
||||
};
|
||||
|
||||
const viewMember = (member: Member) => {
|
||||
selectedMember.value = member;
|
||||
showViewDialog.value = true;
|
||||
};
|
||||
|
||||
const editMember = (member: Member) => {
|
||||
selectedMember.value = member;
|
||||
showEditDialog.value = true;
|
||||
};
|
||||
|
||||
const handleEditMember = (member: Member) => {
|
||||
showViewDialog.value = false;
|
||||
selectedMember.value = member;
|
||||
showEditDialog.value = true;
|
||||
};
|
||||
|
||||
const handleMemberUpdated = (member: Member) => {
|
||||
const index = members.value.findIndex(m => m.member_id === member.member_id);
|
||||
if (index > -1) {
|
||||
members.value[index] = member;
|
||||
}
|
||||
showEditDialog.value = false;
|
||||
};
|
||||
|
||||
const markDuesPaid = async (member: Member) => {
|
||||
try {
|
||||
member.dues_paid_this_year = true;
|
||||
member.dues_status = 'Paid';
|
||||
member.last_dues_paid = new Date().toISOString();
|
||||
|
||||
stats.value.paidThisYear++;
|
||||
stats.value.duesOutstanding--;
|
||||
} catch (error) {
|
||||
console.error('Error marking dues as paid:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMarkDuesPaid = (member: Member) => {
|
||||
markDuesPaid(member);
|
||||
};
|
||||
|
||||
const sendEmail = (member: Member) => {
|
||||
window.location.href = `mailto:${member.email}`;
|
||||
};
|
||||
|
||||
const viewPaymentHistory = (member: Member) => {
|
||||
// TODO: Navigate to payment history
|
||||
};
|
||||
|
||||
const toggleStatus = (member: Member) => {
|
||||
member.status = member.status === 'active' ? 'inactive' : 'active';
|
||||
// TODO: Make API call
|
||||
};
|
||||
|
||||
const exportMembers = () => {
|
||||
// TODO: Export to CSV/Excel
|
||||
};
|
||||
|
||||
// Load members data
|
||||
const loadMembers = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await $fetch('/api/members');
|
||||
const membersList = response?.data?.list || response?.data?.members || response?.list || [];
|
||||
|
||||
if (membersList && membersList.length > 0) {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const currentMonth = new Date().getMonth();
|
||||
|
||||
members.value = membersList.map((member: any) => {
|
||||
const lastPaid = member.last_dues_paid ? new Date(member.last_dues_paid) : null;
|
||||
const duesPaidThisYear = lastPaid && lastPaid.getFullYear() === currentYear;
|
||||
const joinDate = member.member_since || member.created_at;
|
||||
const joinMonth = joinDate ? new Date(joinDate).getMonth() : null;
|
||||
|
||||
return {
|
||||
...member,
|
||||
member_id: member.member_id || '',
|
||||
first_name: member.first_name,
|
||||
last_name: member.last_name,
|
||||
name: `${member.last_name || ''}, ${member.first_name || ''}`.trim(),
|
||||
email: member.email,
|
||||
nationality: member.nationality || member.country_code || '',
|
||||
membership_type: member.membership_type || 'Standard',
|
||||
status: member.membership_status === 'Active' ? 'active' : 'inactive',
|
||||
dues_status: member.dues_status || (duesPaidThisYear ? 'Paid' : 'Due'),
|
||||
dues_paid_this_year: duesPaidThisYear,
|
||||
last_dues_paid: member.last_dues_paid,
|
||||
join_date: joinDate,
|
||||
phone: member.phone_number || member.phone || ''
|
||||
};
|
||||
}).sort((a, b) => {
|
||||
const aLastName = (a.last_name || '').toLowerCase();
|
||||
const bLastName = (b.last_name || '').toLowerCase();
|
||||
const aFirstName = (a.first_name || '').toLowerCase();
|
||||
const bFirstName = (b.first_name || '').toLowerCase();
|
||||
|
||||
const lastNameCompare = aLastName.localeCompare(bLastName);
|
||||
if (lastNameCompare !== 0) return lastNameCompare;
|
||||
|
||||
return aFirstName.localeCompare(bFirstName);
|
||||
});
|
||||
|
||||
// Calculate stats
|
||||
const currentYearMembers = members.value.filter(m => m.dues_paid_this_year);
|
||||
const newThisMonth = members.value.filter(m => {
|
||||
const joinDate = new Date(m.join_date);
|
||||
return joinDate.getMonth() === currentMonth && joinDate.getFullYear() === currentYear;
|
||||
});
|
||||
|
||||
stats.value = {
|
||||
total: members.value.length,
|
||||
active: members.value.filter(m => m.status === 'active').length,
|
||||
paidThisYear: currentYearMembers.length,
|
||||
duesOutstanding: members.value.length - currentYearMembers.length,
|
||||
newThisMonth: newThisMonth.length
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading members:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Load on mount
|
||||
onMounted(async () => {
|
||||
await loadMembers();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Glassmorphism effect */
|
||||
.glass-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-radius: 16px !important;
|
||||
}
|
||||
|
||||
/* Gradient text */
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Gradient avatar */
|
||||
.gradient-avatar {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
/* Gradient background for card headers */
|
||||
.gradient-bg {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
/* Stat card hover effect */
|
||||
.stat-card {
|
||||
transition: all 0.3s ease;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Member card effects */
|
||||
.member-card {
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.member-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.member-card .card-header {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.member-card .card-header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.5s;
|
||||
}
|
||||
|
||||
.member-card:hover .card-header::before {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
/* White border for avatar */
|
||||
.white-border {
|
||||
border: 3px solid white;
|
||||
}
|
||||
|
||||
/* Info row styling */
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Search field styling */
|
||||
.search-field :deep(.v-field) {
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
/* Chip active state */
|
||||
.chip-active {
|
||||
background-color: rgba(var(--v-theme-primary), 0.12) !important;
|
||||
border-color: rgb(var(--v-theme-primary)) !important;
|
||||
}
|
||||
|
||||
/* Table styling */
|
||||
.modern-table :deep(tbody tr) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modern-table :deep(tbody tr:hover) {
|
||||
background-color: rgba(var(--v-theme-primary), 0.04);
|
||||
}
|
||||
|
||||
/* Animation for cards */
|
||||
.card-list-move,
|
||||
.card-list-enter-active,
|
||||
.card-list-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.card-list-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
|
||||
.card-list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
|
||||
/* Pulse animation for add button */
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(var(--v-theme-primary), 0.4);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(var(--v-theme-primary), 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(var(--v-theme-primary), 0);
|
||||
}
|
||||
}
|
||||
|
||||
.pulse-animation {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
/* Filter card styling */
|
||||
.filter-card {
|
||||
border-left: 4px solid rgb(var(--v-theme-primary));
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,406 @@
|
|||
<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>
|
||||
|
|
@ -0,0 +1,466 @@
|
|||
<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>
|
||||
|
|
@ -0,0 +1,747 @@
|
|||
<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>
|
||||
|
|
@ -0,0 +1,886 @@
|
|||
<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>
|
||||
|
|
@ -0,0 +1,889 @@
|
|||
<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>
|
||||
|
|
@ -0,0 +1,350 @@
|
|||
<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>
|
||||
|
|
@ -0,0 +1,613 @@
|
|||
<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>
|
||||
|
|
@ -0,0 +1,304 @@
|
|||
<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>
|
||||
|
|
@ -0,0 +1,710 @@
|
|||
<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>
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
export default {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
|
@ -1,249 +0,0 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./components/**/*.{js,vue,ts}",
|
||||
"./layouts/**/*.vue",
|
||||
"./pages/**/*.vue",
|
||||
"./plugins/**/*.{js,ts}",
|
||||
"./app.vue",
|
||||
"./error.vue",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// Monaco Red Spectrum
|
||||
'monaco': {
|
||||
50: '#fef2f2',
|
||||
100: '#fee2e2',
|
||||
200: '#fecaca',
|
||||
300: '#fca5a5',
|
||||
400: '#f87171',
|
||||
500: '#ef4444',
|
||||
600: '#dc2626', // Primary
|
||||
700: '#b91c1c',
|
||||
800: '#991b1b',
|
||||
900: '#7f1d1d',
|
||||
},
|
||||
// Glass Colors
|
||||
'glass': {
|
||||
'white': 'rgba(255, 255, 255, 0.7)',
|
||||
'light': 'rgba(255, 255, 255, 0.85)',
|
||||
'ultra-light': 'rgba(255, 255, 255, 0.95)',
|
||||
'border': 'rgba(255, 255, 255, 0.6)',
|
||||
'monaco': 'rgba(220, 38, 38, 0.1)',
|
||||
'monaco-soft': 'rgba(220, 38, 38, 0.05)',
|
||||
},
|
||||
},
|
||||
backgroundImage: {
|
||||
// Bright gradients
|
||||
'gradient-light': 'linear-gradient(135deg, #f5f5f5 0%, #ffffff 100%)',
|
||||
'gradient-soft': 'linear-gradient(135deg, #fafafa 0%, #f5f5f5 100%)',
|
||||
'gradient-monaco-soft': 'linear-gradient(135deg, #fff5f5 0%, #ffffff 100%)',
|
||||
// Monaco gradients
|
||||
'gradient-monaco': 'linear-gradient(135deg, #dc2626 0%, #b91c1c 100%)',
|
||||
'gradient-monaco-light': 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
|
||||
// Glass gradients
|
||||
'gradient-glass': 'linear-gradient(135deg, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0.6) 100%)',
|
||||
'gradient-glass-soft': 'linear-gradient(135deg, rgba(255,255,255,0.7) 0%, rgba(255,255,255,0.4) 100%)',
|
||||
},
|
||||
backdropBlur: {
|
||||
xs: '2px',
|
||||
sm: '4px',
|
||||
md: '8px',
|
||||
lg: '12px',
|
||||
xl: '16px',
|
||||
'2xl': '20px',
|
||||
'3xl': '30px',
|
||||
},
|
||||
boxShadow: {
|
||||
'glass': '0 8px 32px rgba(31, 38, 135, 0.15)',
|
||||
'glass-sm': '0 4px 16px rgba(31, 38, 135, 0.1)',
|
||||
'glass-lg': '0 12px 48px rgba(31, 38, 135, 0.2)',
|
||||
'glass-inset': 'inset 0 2px 4px rgba(255, 255, 255, 0.6), inset 0 -2px 4px rgba(0, 0, 0, 0.05)',
|
||||
'monaco': '0 10px 40px rgba(220, 38, 38, 0.15)',
|
||||
'monaco-sm': '0 4px 20px rgba(220, 38, 38, 0.1)',
|
||||
'monaco-lg': '0 20px 60px rgba(220, 38, 38, 0.2)',
|
||||
'soft': '0 2px 8px rgba(0, 0, 0, 0.08)',
|
||||
'soft-lg': '0 4px 16px rgba(0, 0, 0, 0.12)',
|
||||
},
|
||||
animation: {
|
||||
'fade-in': 'fadeIn 0.3s ease-in-out',
|
||||
'slide-up': 'slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||||
'slide-down': 'slideDown 0.3s ease-out',
|
||||
'scale-in': 'scaleIn 0.3s ease-out',
|
||||
'float': 'float 3s ease-in-out infinite',
|
||||
'shimmer': 'shimmer 2s infinite',
|
||||
'pulse-soft': 'pulseSoft 2s ease-in-out infinite',
|
||||
'glow': 'glow 2s ease-in-out infinite',
|
||||
},
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
'0%': { opacity: '0' },
|
||||
'100%': { opacity: '1' },
|
||||
},
|
||||
slideUp: {
|
||||
'0%': { opacity: '0', transform: 'translateY(20px)' },
|
||||
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||
},
|
||||
slideDown: {
|
||||
'0%': { opacity: '0', transform: 'translateY(-20px)' },
|
||||
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||
},
|
||||
scaleIn: {
|
||||
'0%': { opacity: '0', transform: 'scale(0.9)' },
|
||||
'100%': { opacity: '1', transform: 'scale(1)' },
|
||||
},
|
||||
float: {
|
||||
'0%, 100%': { transform: 'translateY(0)' },
|
||||
'50%': { transform: 'translateY(-10px)' },
|
||||
},
|
||||
shimmer: {
|
||||
'0%': { backgroundPosition: '-1000px 0' },
|
||||
'100%': { backgroundPosition: '1000px 0' },
|
||||
},
|
||||
pulseSoft: {
|
||||
'0%, 100%': { opacity: '1' },
|
||||
'50%': { opacity: '0.8' },
|
||||
},
|
||||
glow: {
|
||||
'0%, 100%': { boxShadow: '0 0 20px rgba(220, 38, 38, 0.3)' },
|
||||
'50%': { boxShadow: '0 0 30px rgba(220, 38, 38, 0.5)' },
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
'sans': ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
|
||||
},
|
||||
borderRadius: {
|
||||
'glass': '20px',
|
||||
},
|
||||
transitionTimingFunction: {
|
||||
'smooth': 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
'bounce': 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
// Custom glass utilities plugin
|
||||
function({ addUtilities, addComponents, theme }) {
|
||||
// Glass effect utilities
|
||||
addUtilities({
|
||||
'.glass': {
|
||||
background: 'rgba(255, 255, 255, 0.7)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
'-webkit-backdrop-filter': 'blur(20px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.6)',
|
||||
boxShadow: '0 8px 32px rgba(31, 38, 135, 0.15)',
|
||||
},
|
||||
'.glass-light': {
|
||||
background: 'rgba(255, 255, 255, 0.85)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
'-webkit-backdrop-filter': 'blur(20px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.7)',
|
||||
boxShadow: '0 8px 32px rgba(31, 38, 135, 0.12)',
|
||||
},
|
||||
'.glass-ultra': {
|
||||
background: 'rgba(255, 255, 255, 0.95)',
|
||||
backdropFilter: 'blur(30px)',
|
||||
'-webkit-backdrop-filter': 'blur(30px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.8)',
|
||||
boxShadow: '0 8px 32px rgba(31, 38, 135, 0.1), inset 0 2px 4px rgba(255, 255, 255, 0.6)',
|
||||
},
|
||||
'.glass-monaco': {
|
||||
background: 'linear-gradient(135deg, rgba(220, 38, 38, 0.05) 0%, rgba(255, 255, 255, 0.7) 100%)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
'-webkit-backdrop-filter': 'blur(20px)',
|
||||
borderLeft: '3px solid #dc2626',
|
||||
border: '1px solid rgba(220, 38, 38, 0.2)',
|
||||
boxShadow: '0 8px 32px rgba(220, 38, 38, 0.1)',
|
||||
},
|
||||
'.no-scrollbar': {
|
||||
'-ms-overflow-style': 'none',
|
||||
'scrollbar-width': 'none',
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
'.scrollbar-thin': {
|
||||
'scrollbar-width': 'thin',
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
background: 'rgba(0, 0, 0, 0.05)',
|
||||
borderRadius: '9999px',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: 'linear-gradient(135deg, #dc2626 0%, #b91c1c 100%)',
|
||||
borderRadius: '9999px',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Glass component classes
|
||||
addComponents({
|
||||
'.glass-card': {
|
||||
background: 'rgba(255, 255, 255, 0.7)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
'-webkit-backdrop-filter': 'blur(20px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.6)',
|
||||
boxShadow: '0 8px 32px rgba(31, 38, 135, 0.15)',
|
||||
borderRadius: '20px',
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
'&:hover': {
|
||||
background: 'rgba(255, 255, 255, 0.8)',
|
||||
boxShadow: '0 12px 40px rgba(31, 38, 135, 0.2)',
|
||||
transform: 'translateY(-4px)',
|
||||
},
|
||||
},
|
||||
'.glass-button': {
|
||||
background: 'rgba(255, 255, 255, 0.7)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
'-webkit-backdrop-filter': 'blur(10px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.8)',
|
||||
boxShadow: '0 4px 15px rgba(0, 0, 0, 0.08)',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
boxShadow: '0 6px 20px rgba(220, 38, 38, 0.15)',
|
||||
transform: 'translateY(-2px)',
|
||||
},
|
||||
'&:active': {
|
||||
transform: 'translateY(0)',
|
||||
},
|
||||
},
|
||||
'.glass-input': {
|
||||
background: 'rgba(255, 255, 255, 0.7)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
'-webkit-backdrop-filter': 'blur(10px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.6)',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
background: 'rgba(255, 255, 255, 0.8)',
|
||||
borderColor: 'rgba(220, 38, 38, 0.2)',
|
||||
},
|
||||
'&:focus': {
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
borderColor: '#dc2626',
|
||||
boxShadow: '0 0 0 3px rgba(220, 38, 38, 0.1)',
|
||||
outline: 'none',
|
||||
},
|
||||
},
|
||||
'.glass-sidebar': {
|
||||
background: 'rgba(255, 255, 255, 0.85)',
|
||||
backdropFilter: 'blur(30px)',
|
||||
'-webkit-backdrop-filter': 'blur(30px)',
|
||||
borderRight: '1px solid rgba(255, 255, 255, 0.6)',
|
||||
boxShadow: '4px 0 24px rgba(0, 0, 0, 0.08)',
|
||||
},
|
||||
'.glass-navbar': {
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
'-webkit-backdrop-filter': 'blur(20px)',
|
||||
borderBottom: '1px solid rgba(255, 255, 255, 0.6)',
|
||||
boxShadow: '0 4px 24px rgba(0, 0, 0, 0.08)',
|
||||
},
|
||||
});
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
@ -0,0 +1,295 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>MonacoUSA — Visual Preview (Dashboard / Member)</title>
|
||||
<style>
|
||||
/* MonacoUSA Design Tokens (subset) */
|
||||
:root{
|
||||
--monaco-red: #dc2626;
|
||||
--monaco-red-dark: #b91c1c;
|
||||
--monaco-white: #ffffff;
|
||||
--monaco-gold: #ffd700;
|
||||
--neutral-50: #F8FAFC;
|
||||
--neutral-100: #F1F5F9;
|
||||
--neutral-300: #CBD5E1;
|
||||
--neutral-500: #64748B;
|
||||
--neutral-700: #334155;
|
||||
--glass-bg: rgba(255,255,255,0.85);
|
||||
--glass-border: rgba(255,255,255,0.18);
|
||||
--shadow-soft: 0 10px 30px rgba(17,24,39,0.06);
|
||||
--radius-lg: 12px;
|
||||
--radius-xl: 16px;
|
||||
--max-width: 1200px;
|
||||
--page-padding: 20px;
|
||||
font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
|
||||
color: var(--neutral-700);
|
||||
background: linear-gradient(180deg, var(--neutral-50), #ffffff);
|
||||
}
|
||||
|
||||
body { margin: 0; -webkit-font-smoothing:antialiased; -moz-osx-font-smoothing:grayscale; }
|
||||
.app {
|
||||
max-width: var(--max-width);
|
||||
margin: 28px auto;
|
||||
padding: var(--page-padding);
|
||||
}
|
||||
|
||||
/* Top bar */
|
||||
.topbar {
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:12px;
|
||||
padding:12px 16px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.6), rgba(250,250,250,0.9));
|
||||
box-shadow: var(--shadow-soft);
|
||||
border: 1px solid rgba(0,0,0,0.03);
|
||||
}
|
||||
.brand { display:flex; align-items:center; gap:12px; }
|
||||
.brand .logo {
|
||||
width:48px; height:48px; border-radius:10px;
|
||||
background: linear-gradient(135deg,var(--monaco-red),var(--monaco-red-dark));
|
||||
display:flex; align-items:center; justify-content:center; color:white; font-weight:700;
|
||||
box-shadow: 0 8px 20px rgba(185,28,28,0.12);
|
||||
}
|
||||
.brand h1 { margin:0; font-size:18px; color:var(--neutral-700); }
|
||||
.actions { display:flex; gap:8px; align-items:center; }
|
||||
|
||||
/* Layout */
|
||||
.layout { display:grid; grid-template-columns: 260px 1fr; gap: 20px; margin-top:18px; align-items:start; }
|
||||
|
||||
/* Sidebar (glass) */
|
||||
.sidebar {
|
||||
background: var(--glass-bg);
|
||||
border-radius: 14px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--glass-border);
|
||||
box-shadow: 0 8px 32px rgba(31,41,55,0.06);
|
||||
}
|
||||
.sidebar .nav { display:flex; flex-direction:column; gap:8px; }
|
||||
.nav a {
|
||||
display:flex; align-items:center; gap:8px; padding:10px; border-radius:10px; color:var(--neutral-700);
|
||||
text-decoration:none; font-weight:600;
|
||||
}
|
||||
.nav a.active { background: linear-gradient(90deg, rgba(220,38,38,0.08), rgba(220,38,38,0.04)); color:var(--monaco-red); }
|
||||
|
||||
/* Main content cards */
|
||||
.content { min-height:400px; }
|
||||
.row { display:grid; grid-template-columns: repeat(3, 1fr); gap:16px; margin-bottom:16px; }
|
||||
.card {
|
||||
background: var(--glass-bg);
|
||||
border-radius: var(--radius-lg);
|
||||
padding:18px;
|
||||
border: 1px solid var(--glass-border);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
.card h3 { margin:0 0 8px 0; font-size:14px; color:var(--neutral-700); }
|
||||
.stat { font-size:22px; font-weight:700; color:var(--monaco-red); }
|
||||
|
||||
/* Quick actions / buttons */
|
||||
.btn {
|
||||
display:inline-flex; align-items:center; gap:8px; padding:10px 14px; border-radius:10px; font-weight:600; cursor:pointer;
|
||||
border: none; background: var(--monaco-red); color:white;
|
||||
}
|
||||
.btn.ghost { background:transparent; color:var(--neutral-700); border:1px solid var(--neutral-200); }
|
||||
|
||||
/* Member profile mock */
|
||||
.profile {
|
||||
display:grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
gap:18px;
|
||||
align-items:start;
|
||||
}
|
||||
.avatar {
|
||||
width:200px; border-radius:18px; padding:12px; text-align:center; background:linear-gradient(180deg,#fff,#fafafa);
|
||||
border:1px solid rgba(0,0,0,0.03); box-shadow:0 6px 18px rgba(0,0,0,0.04);
|
||||
}
|
||||
.avatar img { width:120px; height:120px; border-radius:16px; object-fit:cover; display:block; margin:8px auto; }
|
||||
.meta h2 { margin:0 0 6px 0; }
|
||||
.meta p { margin:0; color:var(--neutral-500); }
|
||||
|
||||
/* smaller screens */
|
||||
@media (max-width:900px){
|
||||
.layout { grid-template-columns:1fr; }
|
||||
.row { grid-template-columns: repeat(2, 1fr); }
|
||||
.profile { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* simple toggles in demo */
|
||||
.views { display:flex; gap:8px; margin-top:16px; }
|
||||
.views button { padding:8px 12px; border-radius:10px; border:1px solid var(--neutral-200); background:white; cursor:pointer; }
|
||||
.hidden { display:none; }
|
||||
footer { margin-top:28px; text-align:center; color:var(--neutral-300); font-size:13px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<div class="topbar" role="banner" aria-label="MonacoUSA topbar">
|
||||
<div class="brand">
|
||||
<div class="logo" aria-hidden="true">M</div>
|
||||
<div>
|
||||
<h1>MonacoUSA Portal</h1>
|
||||
<div style="font-size:12px;color:var(--neutral-500)">Admin preview — MonacoUSA theme</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions" role="navigation" aria-label="Global actions">
|
||||
<button class="btn ghost" onclick="switchView('dashboard')">Preview Dashboard</button>
|
||||
<button class="btn" onclick="switchView('profile')">Preview Member</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout" role="main">
|
||||
<aside class="sidebar" aria-label="Sidebar">
|
||||
<nav class="nav">
|
||||
<a class="active" href="#" onclick="switchView('dashboard'); return false;">Dashboard</a>
|
||||
<a href="#" onclick="switchView('members'); return false;">Members</a>
|
||||
<a href="#" onclick="switchView('events'); return false;">Events</a>
|
||||
<a href="#" onclick="switchView('payments'); return false;">Payments & Dues</a>
|
||||
<a href="#" onclick="switchView('settings'); return false;">Settings</a>
|
||||
</nav>
|
||||
<div style="margin-top:18px; display:flex; gap:8px;">
|
||||
<button class="btn" onclick="alert('CTA')">Create Event</button>
|
||||
<button class="btn ghost" onclick="alert('Export')">Export</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="content">
|
||||
<!-- Dashboard view -->
|
||||
<div id="dashboard-view">
|
||||
<div class="row" aria-label="Key stats">
|
||||
<div class="card">
|
||||
<h3>Upcoming Events</h3>
|
||||
<div class="stat">12</div>
|
||||
<div style="font-size:13px;color:var(--neutral-500)">Next: Business Luncheon — Sept 24</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Members</h3>
|
||||
<div class="stat">1,562</div>
|
||||
<div style="font-size:13px;color:var(--neutral-500)">Active this month: 52</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Dues Outstanding</h3>
|
||||
<div class="stat">$4,320</div>
|
||||
<div style="font-size:13px;color:var(--neutral-500)">Overdue: 8 members</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom:12px;">
|
||||
<h3>Quick Actions</h3>
|
||||
<div style="display:flex; gap:12px; margin-top:12px;">
|
||||
<button class="btn">Add Member</button>
|
||||
<button class="btn ghost">Send Announcement</button>
|
||||
<button class="btn ghost">Upload Document</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Recent Activity</h3>
|
||||
<ul style="margin:12px 0 0 0; padding-left:18px; color:var(--neutral-500);">
|
||||
<li>Payment received from Jane D. — $120</li>
|
||||
<li>Event RSVP updated — Board Meeting</li>
|
||||
<li>Member profile updated — John Doe</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Members list view (simple mock) -->
|
||||
<div id="members-view" class="hidden">
|
||||
<div class="card">
|
||||
<h3>Members</h3>
|
||||
<div style="display:flex;flex-direction:column;gap:8px;margin-top:10px;">
|
||||
<div style="display:flex;align-items:center;gap:12px;justify-content:space-between;">
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<div style="width:48px;height:48px;border-radius:8px;background:var(--neutral-100);"></div>
|
||||
<div>
|
||||
<div style="font-weight:700">John Doe</div>
|
||||
<div style="color:var(--neutral-500;font-size:13px)">john.doe@example.com</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn ghost" onclick="switchView('profile')">View</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="height:8px;border-bottom:1px dashed rgba(0,0,0,0.04)"></div>
|
||||
<div style="display:flex;align-items:center;gap:12px;justify-content:space-between;">
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<div style="width:48px;height:48px;border-radius:8px;background:var(--neutral-100);"></div>
|
||||
<div>
|
||||
<div style="font-weight:700">Jane Smith</div>
|
||||
<div style="color:var(--neutral-500;font-size:13px)">jane.smith@company.com</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn ghost" onclick="alert('Open')">View</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Member profile view -->
|
||||
<div id="profile-view" class="hidden">
|
||||
<div class="profile card">
|
||||
<div class="avatar">
|
||||
<img src="../public/monaco_high_res.jpg" alt="Member photo" />
|
||||
<div style="font-weight:700;margin-top:12px;">John Doe</div>
|
||||
<div style="color:var(--neutral-500);font-size:13px">Member • Premium</div>
|
||||
<div style="margin-top:10px"><button class="btn">Message</button></div>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<h2>John Doe</h2>
|
||||
<p style="margin-bottom:8px;color:var(--neutral-500)">CEO • Tech Innovations Inc.</p>
|
||||
|
||||
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:12px;margin-top:12px;">
|
||||
<div style="background:var(--neutral-100);padding:12px;border-radius:10px;">
|
||||
<div style="font-size:13px;color:var(--neutral-500)">Email</div>
|
||||
<div style="font-weight:700">john.doe@example.com</div>
|
||||
</div>
|
||||
<div style="background:var(--neutral-100);padding:12px;border-radius:10px;">
|
||||
<div style="font-size:13px;color:var(--neutral-500)">Phone</div>
|
||||
<div style="font-weight:700">+1 234 567 8900</div>
|
||||
</div>
|
||||
<div style="background:var(--neutral-100);padding:12px;border-radius:10px;">
|
||||
<div style="font-size:13px;color:var(--neutral-500)">Location</div>
|
||||
<div style="font-weight:700">Monaco</div>
|
||||
</div>
|
||||
<div style="background:var(--neutral-100);padding:12px;border-radius:10px;">
|
||||
<div style="font-size:13px;color:var(--neutral-500)">Member Since</div>
|
||||
<div style="font-weight:700">2021</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:18px;">
|
||||
<h3 style="margin:0 0 8px 0;">Bio</h3>
|
||||
<p style="color:var(--neutral-500);margin:0;">Passionate about business and innovation. Active member of the Monaco business community. Interested in mentoring and partnerships.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
Live preview (static HTML) — MonacoUSA visual proposal • Colors and spacing follow the design system.
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function switchView(view){
|
||||
document.getElementById('dashboard-view').classList.add('hidden');
|
||||
document.getElementById('members-view').classList.add('hidden');
|
||||
document.getElementById('profile-view').classList.add('hidden');
|
||||
if(view === 'dashboard') document.getElementById('dashboard-view').classList.remove('hidden');
|
||||
if(view === 'members') document.getElementById('members-view').classList.remove('hidden');
|
||||
if(view === 'profile') document.getElementById('profile-view').classList.remove('hidden');
|
||||
window.scrollTo(0,0);
|
||||
}
|
||||
|
||||
// default
|
||||
switchView('dashboard');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||