Compare commits

...

11 Commits

Author SHA1 Message Date
Matt d8453160d3 Enhance sidebars with premium glass-bolt design
Build And Push Image / docker (push) Successful in 1m49s Details
- Added CSS variables for consistent glass-bolt theming
- Enhanced glass drawer with gradient backgrounds (70-85% opacity)
- Added floating orb decorations with smooth animations
- Implemented shimmer effects on navigation item hover
- Added system status indicator to admin sidebar
- Enhanced animations (float, subtle-pulse, pulse-dot)
- Applied premium styling to all three layouts (admin, board, member)
- Preserved all existing functionality while overlaying new design
- Based on BoltAI mockup inspiration for premium aesthetic

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-07 18:04:10 +02:00
Matt 64a12ecd5b Implement glass-bolt design system across platform
Build And Push Image / docker (push) Successful in 1m53s Details
- Removed 19 test/prototype pages while preserving BoltAI-Mockups for reference
- Created comprehensive DESIGN-SYSTEM.md documentation
- Updated and consolidated SCSS structure
- Applied subtle glassmorphic design to admin portal pages
- Updated admin members page with new glass-bolt styling
- Implemented consistent design patterns:
  - Glass cards with 60% white opacity and 4px blur
  - Subtle borders and soft shadows
  - Monaco red accent color (#dc2626)
  - Clean typography without excessive gradients
  - Hover states with gentle lift animations

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-07 17:41:41 +02:00
Matt 9c812d78dd Fix color scheme - use white/gray instead of pink tints
Build And Push Image / docker (push) Successful in 1m51s Details
- Changed all gradients from #fef2f2 (pinkish) to proper white/gray
- Updated background from pink tint to clean white (#ffffff) to light gray (#f8f9fa)
- Fixed glass-monaco-soft to use very subtle red tint (3% opacity)
- Updated all components to follow Monaco brand colors properly
- Maintains Monaco red (#dc2626) for accents and text gradients only

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-06 17:32:32 +02:00
Matt af31781323 Transform glass designs to subtle bolt.ai style
Build And Push Image / docker (push) Successful in 2m0s Details
- Reduced blur effects from 10-60px to 2-4px for better performance
- Changed gradients from heavy red to subtle light (#fef2f2-#ffffff)
- Updated text colors from white to dark (#27272a) for better readability
- Created design tokens system for consistent theming
- Added global glass-bolt-style.scss for unified styling
- Updated GlassCard, MonacoButton, GlassSidebar components
- Transformed glass dashboard to match bolt.ai mockup patterns
- Simplified animations and reduced visual noise
- Improved mobile performance with responsive blur reduction

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-06 17:23:41 +02:00
Matt f735b68fed Implement ultra-modern glassmorphic dashboard with animations
Build And Push Image / docker (push) Successful in 2m0s Details
- Add animated hero header with Monaco red gradient
- Implement floating orbs with blur effects
- Create glowing avatar with status indicator
- Add gradient text animations
- Implement glass-morphic cards with backdrop blur
- Add hover animations and transitions
- Refine dues management cards
- Include comprehensive CSS animations
2025-09-06 16:03:02 +02:00
Matt 245c3571c7 Create properly styled glass dashboard with inline CSS
Build And Push Image / docker (push) Successful in 1m52s Details
2025-09-06 15:52:33 +02:00
Matt a8a12ef12a Fix glassmorphic styling - add inline styles and fix Tailwind CSS
Build And Push Image / docker (push) Successful in 1m51s Details
2025-09-06 15:48:12 +02:00
Matt 5c72aa727c Fix Tailwind build errors - remove style imports and fix gradient classes
Build And Push Image / docker (push) Successful in 1m49s Details
2025-09-06 15:42:15 +02:00
Matt 6223388768 Fix glass dashboard with simplified version
Build And Push Image / docker (push) Failing after 59s Details
- Create simplified glass.vue that works with existing layouts
- Remove complex component dependencies causing 500 error
- Use inline Tailwind classes for glassmorphic effects
- Maintain 4-card limit for dues display

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-06 15:31:40 +02:00
Matt a0d2d2b00f Fix Tailwind CSS v4 build error
Build And Push Image / docker (push) Successful in 1m54s Details
- Install @tailwindcss/postcss package for v4 compatibility
- Update PostCSS configuration to use new package structure
- Fix nuxt.config.ts PostCSS plugin configuration
- Resolves Docker build failures

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-06 15:22:48 +02:00
Matt a0d703e7cb Implement bright glassmorphic UI redesign for Board dashboard
Build And Push Image / docker (push) Failing after 1m11s Details
- Add Tailwind CSS configuration with bright glass utilities
- Create glass components (Sidebar, StatCard, DuesCard) with Lucide icons
- Implement new dashboard with limited dues display (4 cards max vs 30+)
- Use translucent white glass effects with Monaco red accents
- Improve visual hierarchy and reduce UI clutter

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-06 15:17:54 +02:00
90 changed files with 10929 additions and 10724 deletions

View File

@ -93,7 +93,14 @@
"Read(/Z:\\Repos\\monacousa-portal\\components/**)", "Read(/Z:\\Repos\\monacousa-portal\\components/**)",
"Read(/Z:\\Repos\\monacousa-portal\\server\\api/**)", "Read(/Z:\\Repos\\monacousa-portal\\server\\api/**)",
"Read(/Z:\\Repos\\monacousa-portal\\pages\\admin\\members/**)", "Read(/Z:\\Repos\\monacousa-portal\\pages\\admin\\members/**)",
"Bash(git pull:*)" "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)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 953 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

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

View File

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

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

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

View File

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

View File

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

433
DESIGN-SYSTEM.md Normal file
View File

@ -0,0 +1,433 @@
# 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

292
assets/css/tailwind.css Normal file
View File

@ -0,0 +1,292 @@
@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;
}
}

View File

@ -0,0 +1,193 @@
// 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;
}
}

View File

@ -0,0 +1,268 @@
// 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);
}

View File

@ -1,6 +1,12 @@
// MonacoUSA Portal - Main Stylesheet // MonacoUSA Portal - Main Stylesheet
// Based on design-system.md specifications // 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 component styles
@import 'components/dashboards'; @import 'components/dashboards';

70
components/CountUp.vue Normal file
View File

@ -0,0 +1,70 @@
<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>

View File

@ -0,0 +1,229 @@
<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>

312
components/GlassSidebar.vue Normal file
View File

@ -0,0 +1,312 @@
<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>

View File

@ -0,0 +1,192 @@
<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>

111
components/SidebarLink.vue Normal file
View File

@ -0,0 +1,111 @@
<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>

View File

@ -91,58 +91,34 @@ const animationConfig = {
overflow: hidden; overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
// Glass effect base // Variants - Updated to bolt.ai style with reduced blur
&::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 { &--light {
background: rgba(255, 255, 255, 0.7); background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(20px) saturate(180%); backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(4px);
border: 1px solid rgba(255, 255, 255, 0.3); border: 1px solid rgba(255, 255, 255, 0.2);
color: #27272a; color: #27272a;
} }
&--dark { &--dark {
background: rgba(0, 0, 0, 0.7); background: rgba(0, 0, 0, 0.05);
backdrop-filter: blur(20px) saturate(180%); backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(2px);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(0, 0, 0, 0.1);
color: #ffffff; color: #27272a;
} }
&--colored { &--colored {
background: linear-gradient(135deg, background: rgba(254, 242, 242, 0.6);
rgba(220, 38, 38, 0.1) 0%, backdrop-filter: blur(2px);
rgba(185, 28, 28, 0.05) 100%); -webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(20px) saturate(180%); border: 1px solid rgba(220, 38, 38, 0.1);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(220, 38, 38, 0.2);
color: #27272a; color: #27272a;
} }
&--gradient { &--gradient {
background: linear-gradient(135deg, background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
rgba(255, 255, 255, 0.8) 0%, border: 1px solid rgba(0, 0, 0, 0.05);
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; color: #27272a;
} }
@ -205,16 +181,10 @@ const animationConfig = {
} }
&--elevated { &--elevated {
box-shadow: box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08);
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 { &:hover {
box-shadow: box-shadow: 0 20px 60px rgba(0, 0, 0, 0.12);
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);
} }
} }
@ -231,7 +201,7 @@ const animationConfig = {
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 600; font-weight: 600;
margin: 0; margin: 0;
color: #dc2626; color: #27272a;
} }
&__subtitle { &__subtitle {

View File

@ -149,33 +149,30 @@ defineEmits<{
} }
&--glass { &--glass {
background: rgba(255, 255, 255, 0.7); background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(20px); backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(4px);
border: 1px solid rgba(255, 255, 255, 0.3); border: 1px solid rgba(255, 255, 255, 0.2);
color: #dc2626; color: #27272a;
box-shadow: box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
0 8px 32px rgba(0, 0, 0, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.5);
&:hover:not(:disabled) { &:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.8); background: rgba(255, 255, 255, 0.8);
border-color: rgba(220, 38, 38, 0.2); border-color: rgba(220, 38, 38, 0.1);
box-shadow: box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
0 12px 40px rgba(0, 0, 0, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.6);
transform: translateY(-2px); transform: translateY(-2px);
} }
} }
&--gradient { &--gradient {
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%); background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
color: white; color: #dc2626;
box-shadow: 0 4px 14px rgba(220, 38, 38, 0.25); border: 1px solid rgba(220, 38, 38, 0.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
&:hover:not(:disabled) { &:hover:not(:disabled) {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
box-shadow: 0 6px 20px rgba(220, 38, 38, 0.35); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
transform: translateY(-2px); transform: translateY(-2px);
} }
} }

View File

@ -265,6 +265,19 @@
</div> </div>
</v-card> </v-card>
</div> </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> </template>
</v-navigation-drawer> </v-navigation-drawer>
@ -454,10 +467,59 @@ watch(width, (newWidth) => {
<style scoped lang="scss"> <style scoped lang="scss">
@import '~/assets/scss/main.scss'; @import '~/assets/scss/main.scss';
// Glass Drawer Styles // 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 { .glass-drawer {
@include glass-effect(0.95, 30px); background: linear-gradient(135deg,
border-right: 1px solid rgba(255, 255, 255, 0.2) !important; 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;
}
} }
.glass-logo-section { .glass-logo-section {
@ -474,10 +536,38 @@ watch(width, (newWidth) => {
@keyframes float { @keyframes float {
0%, 100% { 0%, 100% {
transform: translateY(0); transform: translateY(0) translateX(0);
}
25% {
transform: translateY(-15px) translateX(5px);
} }
50% { 50% {
transform: translateY(-10px); 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);
} }
} }
@ -497,35 +587,50 @@ watch(width, (newWidth) => {
.glass-nav-item { .glass-nav-item {
border-radius: 12px !important; border-radius: 12px !important;
margin: 4px 12px !important; margin: 6px 16px !important;
padding: 12px 16px !important;
position: relative;
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !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 { &:hover {
background: rgba(220, 38, 38, 0.05) !important; background: rgba(220, 38, 38, 0.05) !important;
transform: translateX(2px); transform: translateX(3px);
box-shadow: 0 2px 8px rgba(220, 38, 38, 0.1);
&::after {
left: 100%;
}
} }
&.v-list-item--active { &.v-list-item--active {
background: linear-gradient(135deg, background: linear-gradient(135deg,
rgba(220, 38, 38, 0.15) 0%, rgba(220, 38, 38, 0.12) 0%,
rgba(220, 38, 38, 0.08) 100%) !important; rgba(220, 38, 38, 0.08) 100%) !important;
color: #dc2626 !important; color: var(--monaco-red) !important;
position: relative; transform: scale(1.02);
border-left: 3px solid var(--monaco-red);
&::before { box-shadow: 0 2px 12px rgba(220, 38, 38, 0.15);
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 { .v-icon {
color: #dc2626 !important; color: var(--monaco-red) !important;
animation: subtle-pulse 3s ease-in-out infinite;
} }
} }
} }
@ -664,4 +769,65 @@ watch(width, (newWidth) => {
margin: 2px 8px 2px 16px !important; 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> </style>

View File

@ -523,10 +523,59 @@ watch(width, (newWidth) => {
<style scoped lang="scss"> <style scoped lang="scss">
@import '~/assets/scss/main.scss'; @import '~/assets/scss/main.scss';
// Glass Drawer Styles // 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 { .glass-drawer {
@include glass-effect(0.95, 30px); background: linear-gradient(135deg,
border-right: 1px solid rgba(255, 255, 255, 0.2) !important; 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;
}
} }
.glass-logo-section { .glass-logo-section {
@ -543,10 +592,38 @@ watch(width, (newWidth) => {
@keyframes float { @keyframes float {
0%, 100% { 0%, 100% {
transform: translateY(0); transform: translateY(0) translateX(0);
}
25% {
transform: translateY(-15px) translateX(5px);
} }
50% { 50% {
transform: translateY(-10px); 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);
} }
} }
@ -562,12 +639,36 @@ watch(width, (newWidth) => {
.glass-nav-item { .glass-nav-item {
border-radius: 12px !important; border-radius: 12px !important;
margin: 4px 12px !important; margin: 6px 16px !important;
padding: 12px 16px !important;
position: relative;
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !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 { &:hover {
background: rgba(220, 38, 38, 0.05) !important; background: rgba(220, 38, 38, 0.05) !important;
transform: translateX(2px); transform: translateX(3px);
box-shadow: 0 2px 8px rgba(220, 38, 38, 0.1);
&::after {
left: 100%;
}
} }
&.v-list-item--active { &.v-list-item--active {

View File

@ -321,11 +321,60 @@ watch(width, (newWidth) => {
<style scoped lang="scss"> <style scoped lang="scss">
@import '~/assets/scss/main.scss'; @import '~/assets/scss/main.scss';
// Enhanced Glass Drawer Styles // 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 { .enhanced-glass-drawer {
@include enhanced-glass(0.95, 30px); background: linear-gradient(135deg,
border-right: 1px solid rgba(255, 255, 255, 0.2) !important; 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;
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1); 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 { .logo-section {
@ -439,6 +488,43 @@ watch(width, (newWidth) => {
50% { transform: scale(1.1); } 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
.glass-divider { .glass-divider {
opacity: 0.2; opacity: 0.2;

View File

@ -15,7 +15,13 @@ export default defineNuxtConfig({
} }
}, },
modules: ["vuetify-nuxt-module", "@vueuse/motion/nuxt"], modules: ["vuetify-nuxt-module", "@vueuse/motion/nuxt"],
css: ["~/assets/scss/main.scss"], css: ["~/assets/css/tailwind.css", "~/assets/scss/main.scss"],
postcss: {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
},
app: { app: {
head: { head: {
titleTemplate: "%s • MonacoUSA Portal", titleTemplate: "%s • MonacoUSA Portal",

344
package-lock.json generated
View File

@ -45,6 +45,7 @@
"vuetify-nuxt-module": "^0.18.3" "vuetify-nuxt-module": "^0.18.3"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4.1.13",
"@types/cookie": "^0.6.0", "@types/cookie": "^0.6.0",
"@types/formidable": "^3.4.5", "@types/formidable": "^3.4.5",
"@types/mime-types": "^3.0.1", "@types/mime-types": "^3.0.1",
@ -52,6 +53,19 @@
"sass": "^1.91.0" "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": { "node_modules/@ampproject/remapping": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
@ -2664,6 +2678,17 @@
"@jridgewell/trace-mapping": "^0.3.24" "@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": { "node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@ -2684,9 +2709,9 @@
} }
}, },
"node_modules/@jridgewell/sourcemap-codec": { "node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.4", "version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@jridgewell/trace-mapping": { "node_modules/@jridgewell/trace-mapping": {
@ -5417,6 +5442,282 @@
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" "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": { "node_modules/@tanstack/virtual-core": {
"version": "3.13.12", "version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz", "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
@ -11210,9 +11511,8 @@
"version": "1.30.1", "version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
"devOptional": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"detect-libc": "^2.0.3" "detect-libc": "^2.0.3"
}, },
@ -11243,12 +11543,12 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@ -11264,12 +11564,12 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@ -11285,12 +11585,12 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
"freebsd" "freebsd"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@ -11306,12 +11606,12 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@ -11327,12 +11627,12 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@ -11348,12 +11648,12 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@ -11369,12 +11669,12 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@ -11390,12 +11690,12 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@ -11411,12 +11711,12 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@ -11432,12 +11732,12 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
], ],
"peer": true,
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
}, },
@ -11691,12 +11991,12 @@
} }
}, },
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.17", "version": "0.30.18",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "integrity": "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0" "@jridgewell/sourcemap-codec": "^1.5.5"
} }
}, },
"node_modules/magic-string-ast": { "node_modules/magic-string-ast": {
@ -15279,9 +15579,9 @@
} }
}, },
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "4.1.12", "version": "4.1.13",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
"integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==", "integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/tapable": { "node_modules/tapable": {

View File

@ -48,6 +48,7 @@
"vuetify-nuxt-module": "^0.18.3" "vuetify-nuxt-module": "^0.18.3"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4.1.13",
"@types/cookie": "^0.6.0", "@types/cookie": "^0.6.0",
"@types/formidable": "^3.4.5", "@types/formidable": "^3.4.5",
"@types/mime-types": "^3.0.1", "@types/mime-types": "^3.0.1",

View File

@ -1,721 +0,0 @@
<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>

File diff suppressed because it is too large Load Diff

View File

@ -1,996 +0,0 @@
<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>

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
}

249
tailwind.config.js Normal file
View File

@ -0,0 +1,249 @@
/** @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)',
},
});
},
],
}