@ -0,0 +1,12 @@ | |||
# Firebase Configuration | |||
VITE_FIREBASE_API_KEY = AIzaSyCH7XHFOpz9767boAB_OJeeVuqi7wqv6IM | |||
VITE_FIREBASE_AUTH_DOMAIN = niveshweb-b6b85.firebaseapp.com | |||
VITE_FIREBASE_PROJECT_ID = niveshweb-b6b85 | |||
VITE_FIREBASE_STORAGE_BUCKET = niveshweb-b6b85.firebasestorage.app | |||
VITE_FIREBASE_MESSAGING_SENDER_ID = 731306267322 | |||
VITE_FIREBASE_APP_ID = 1:731306267322:web:a3181d9bcd27d91974a883 | |||
# Database Configuration | |||
DB_HOST = 10.0.50.4 | |||
DB_USER = pgadmin | |||
DB_PASSWORD = vHWUj.Z8&kp3dmy-YgSygZ;?6ne)a)hC | |||
DB_NAME = EventsTracking |
@ -0,0 +1,36 @@ | |||
# Node modules | |||
node_modules/ | |||
# Build output | |||
.next/ | |||
out/ | |||
# Logs | |||
npm-debug.log* | |||
yarn-debug.log* | |||
yarn-error.log* | |||
pnpm-debug.log* | |||
# OS files | |||
.DS_Store | |||
# Env files | |||
.env.local | |||
.env.development.local | |||
.env.test.local | |||
.env.production.local | |||
# Optional: Vercel / Netlify | |||
.vercel/ | |||
.netlify/ | |||
# Optional: Ignore IDE config | |||
.vscode/ | |||
.idea/ | |||
# Optional: Coverage and test output | |||
coverage/ | |||
*.lcov | |||
# Optional: Mac/Windows junk | |||
Thumbs.db |
@ -0,0 +1,28 @@ | |||
import js from '@eslint/js'; | |||
import globals from 'globals'; | |||
import reactHooks from 'eslint-plugin-react-hooks'; | |||
import reactRefresh from 'eslint-plugin-react-refresh'; | |||
import tseslint from 'typescript-eslint'; | |||
export default tseslint.config( | |||
{ ignores: ['dist'] }, | |||
{ | |||
extends: [js.configs.recommended, ...tseslint.configs.recommended], | |||
files: ['**/*.{ts,tsx}'], | |||
languageOptions: { | |||
ecmaVersion: 2020, | |||
globals: globals.browser, | |||
}, | |||
plugins: { | |||
'react-hooks': reactHooks, | |||
'react-refresh': reactRefresh, | |||
}, | |||
rules: { | |||
...reactHooks.configs.recommended.rules, | |||
'react-refresh/only-export-components': [ | |||
'warn', | |||
{ allowConstantExport: true }, | |||
], | |||
}, | |||
} | |||
); |
@ -0,0 +1,14 @@ | |||
<!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>EventDash - Event Analytics Dashboard</title> | |||
<meta name="description" content="Track and analyze events with a beautiful Google-authenticated dashboard"> | |||
</head> | |||
<body> | |||
<div id="root"></div> | |||
<script type="module" src="/src/main.tsx"></script> | |||
</body> | |||
</html> |
@ -0,0 +1,5 @@ | |||
/// <reference types="next" /> | |||
/// <reference types="next/image-types/global" /> | |||
// NOTE: This file should not be edited | |||
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. |
@ -0,0 +1,7 @@ | |||
/** @type {import('next').NextConfig} */ | |||
const nextConfig = { | |||
reactStrictMode: true, | |||
// If you want to add any API routes, middleware, or special configs, add here | |||
}; | |||
module.exports = nextConfig; |
@ -0,0 +1,38 @@ | |||
{ | |||
"name": "google-events-dashboard", | |||
"private": true, | |||
"version": "0.1.0", | |||
"scripts": { | |||
"dev": "next dev", | |||
"build": "next build", | |||
"start": "next start", | |||
"lint": "next lint" | |||
}, | |||
"dependencies": { | |||
"@supabase/supabase-js": "^2.39.0", | |||
"axios": "^1.9.0", | |||
"chart.js": "^4.4.0", | |||
"cors": "^2.8.5", | |||
"dotenv": "^16.5.0", | |||
"firebase": "^10.7.0", | |||
"lucide-react": "^0.344.0", | |||
"next": "^14.1.5", | |||
"pg": "^8.16.0", | |||
"react": "^18.3.1", | |||
"react-chartjs-2": "^5.2.0", | |||
"react-dom": "^18.3.1", | |||
"react-firebase-hooks": "^5.1.1" | |||
}, | |||
"devDependencies": { | |||
"@eslint/js": "^9.9.1", | |||
"@types/node": "20.4.1", | |||
"@types/react": "^18.3.5", | |||
"@types/react-dom": "^18.3.0", | |||
"autoprefixer": "^10.4.21", | |||
"eslint": "8.39.0", | |||
"eslint-config-next": "^14.1.5", | |||
"postcss": "^8.5.4", | |||
"tailwindcss": "^3.4.17", | |||
"typescript": "^5.5.3" | |||
} | |||
} |
@ -0,0 +1,6 @@ | |||
module.exports = { | |||
plugins: { | |||
tailwindcss: {}, | |||
autoprefixer: {}, | |||
}, | |||
}; |
@ -0,0 +1,85 @@ | |||
import React, { useState } from 'react'; | |||
import { LogIn } from 'lucide-react'; | |||
import { useAuth } from '../../context/AuthContext'; | |||
import Button from '../ui/Button'; | |||
import Card, { CardContent, CardHeader, CardTitle } from '../ui/Card'; | |||
const LoginForm: React.FC = () => { | |||
const { signInWithGoogle } = useAuth(); | |||
const [isLoading, setIsLoading] = useState(false); | |||
const [error, setError] = useState<string | null>(null); | |||
const handleGoogleSignIn = async () => { | |||
try { | |||
console.log("DKDKKDOKDODKDKODOD"); | |||
setIsLoading(true); | |||
setError(null); | |||
await signInWithGoogle(); | |||
} catch (err) { | |||
setError('Failed to sign in with Google. Please try again.'); | |||
console.error('Login error:', err); | |||
} finally { | |||
setIsLoading(false); | |||
} | |||
}; | |||
return ( | |||
<div className="w-full max-w-md mx-auto"> | |||
<Card className="animate-fade-in"> | |||
<CardHeader> | |||
<CardTitle className="text-center">Sign In</CardTitle> | |||
</CardHeader> | |||
<CardContent className="space-y-6"> | |||
{error && ( | |||
<div className="p-3 bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-200 rounded-md text-sm"> | |||
{error} | |||
</div> | |||
)} | |||
<div className="flex flex-col space-y-4"> | |||
<Button | |||
onClick={handleGoogleSignIn} | |||
isLoading={isLoading} | |||
variant="outline" | |||
fullWidth | |||
className="flex items-center justify-center" | |||
icon={<GoogleIcon />} | |||
> | |||
Sign in with Google | |||
</Button> | |||
<div className="text-center text-sm text-gray-500 dark:text-gray-400 mt-4"> | |||
By signing in, you agree to our Terms of Service and Privacy Policy. | |||
</div> | |||
</div> | |||
</CardContent> | |||
</Card> | |||
</div> | |||
); | |||
}; | |||
// Google icon component | |||
const GoogleIcon = () => ( | |||
<svg className="w-5 h-5" viewBox="0 0 24 24"> | |||
<path | |||
fill="currentColor" | |||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" | |||
/> | |||
<path | |||
fill="currentColor" | |||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" | |||
/> | |||
<path | |||
fill="currentColor" | |||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" | |||
/> | |||
<path | |||
fill="currentColor" | |||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" | |||
/> | |||
<path fill="none" d="M1 1h22v22H1z" /> | |||
</svg> | |||
); | |||
export default LoginForm; |
@ -0,0 +1,110 @@ | |||
import React from 'react'; | |||
import { | |||
Chart as ChartJS, | |||
CategoryScale, | |||
LinearScale, | |||
BarElement, | |||
Title, | |||
Tooltip, | |||
Legend, | |||
PointElement, | |||
LineElement, | |||
ArcElement, | |||
} from 'chart.js'; | |||
import { Bar, Doughnut } from 'react-chartjs-2'; | |||
import Card, { CardContent, CardHeader, CardTitle } from '../ui/Card'; | |||
import { useTheme } from '../../context/ThemeContext'; | |||
// Register ChartJS components | |||
ChartJS.register( | |||
CategoryScale, | |||
LinearScale, | |||
BarElement, | |||
Title, | |||
Tooltip, | |||
Legend, | |||
PointElement, | |||
LineElement, | |||
ArcElement | |||
); | |||
interface EventChartProps { | |||
type: 'bar' | 'doughnut'; | |||
title: string; | |||
data: { | |||
labels: string[]; | |||
datasets: { | |||
label: string; | |||
data: number[]; | |||
backgroundColor: string[] | string; | |||
borderColor?: string[] | string; | |||
borderWidth?: number; | |||
}[]; | |||
}; | |||
className?: string; | |||
} | |||
const EventChart: React.FC<EventChartProps> = ({ type, title, data, className = '' }) => { | |||
const { isDarkMode } = useTheme(); | |||
const options = { | |||
responsive: true, | |||
maintainAspectRatio: false, | |||
plugins: { | |||
legend: { | |||
position: 'top' as const, | |||
labels: { | |||
color: isDarkMode ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.8)', | |||
font: { | |||
size: 12, | |||
}, | |||
}, | |||
}, | |||
tooltip: { | |||
backgroundColor: isDarkMode ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.8)', | |||
titleColor: isDarkMode ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.8)', | |||
bodyColor: isDarkMode ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.8)', | |||
borderColor: isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)', | |||
borderWidth: 1, | |||
}, | |||
}, | |||
scales: type === 'bar' ? { | |||
x: { | |||
grid: { | |||
color: isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)', | |||
}, | |||
ticks: { | |||
color: isDarkMode ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.8)', | |||
}, | |||
}, | |||
y: { | |||
grid: { | |||
color: isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)', | |||
}, | |||
ticks: { | |||
color: isDarkMode ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.8)', | |||
}, | |||
}, | |||
} : undefined, | |||
}; | |||
return ( | |||
<Card className={`h-full ${className}`}> | |||
<CardHeader> | |||
<CardTitle>{title}</CardTitle> | |||
</CardHeader> | |||
<CardContent> | |||
<div className="w-full h-[300px]"> | |||
{type === 'bar' && ( | |||
<Bar data={data} options={options} /> | |||
)} | |||
{type === 'doughnut' && ( | |||
<Doughnut data={data} options={options} /> | |||
)} | |||
</div> | |||
</CardContent> | |||
</Card> | |||
); | |||
}; | |||
export default EventChart; |
@ -0,0 +1,55 @@ | |||
import React from 'react'; | |||
import { TrendingUp, TrendingDown } from 'lucide-react'; | |||
import Card, { CardContent } from '../ui/Card'; | |||
interface EventCountCardProps { | |||
title: string; | |||
count: number; | |||
change?: number; | |||
icon: React.ReactNode; | |||
color: string; | |||
} | |||
const EventCountCard: React.FC<EventCountCardProps> = ({ | |||
title, | |||
count, | |||
change, | |||
icon, | |||
color | |||
}) => { | |||
const isPositiveChange = change && change > 0; | |||
const changeText = change ? `${isPositiveChange ? '+' : ''}${change}%` : null; | |||
return ( | |||
<Card className="h-full transition-all duration-300 hover:translate-y-[-2px]"> | |||
<CardContent className="p-6"> | |||
<div className="flex items-center justify-between"> | |||
<div> | |||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">{title}</p> | |||
<h3 className="mt-1 text-2xl font-semibold text-gray-900 dark:text-white">{count.toLocaleString()}</h3> | |||
{change !== undefined && ( | |||
<div className="mt-1 flex items-center"> | |||
<span className={`flex items-center text-sm font-medium ${ | |||
isPositiveChange | |||
? 'text-green-600 dark:text-green-400' | |||
: 'text-red-600 dark:text-red-400' | |||
}`}> | |||
{isPositiveChange ? <TrendingUp size={16} className="mr-1" /> : <TrendingDown size={16} className="mr-1" />} | |||
{changeText} | |||
</span> | |||
<span className="text-sm text-gray-500 dark:text-gray-400 ml-1">from last period</span> | |||
</div> | |||
)} | |||
</div> | |||
<div className={`p-3 rounded-full ${color}`}> | |||
{icon} | |||
</div> | |||
</div> | |||
</CardContent> | |||
</Card> | |||
); | |||
}; | |||
export default EventCountCard; |
@ -0,0 +1,153 @@ | |||
import React, { useState } from 'react'; | |||
import Card, { CardContent, CardHeader, CardTitle } from '../ui/Card'; | |||
interface EventTableProps { | |||
events: Array<{ | |||
id: string; | |||
name: string; | |||
category: string; | |||
created_at: string; | |||
description?: string | null; | |||
}>; | |||
className?: string; | |||
} | |||
const EventTable: React.FC<EventTableProps> = ({ events, className = '' }) => { | |||
const [sortField, setSortField] = useState<string>('created_at'); | |||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); | |||
const handleSort = (field: string) => { | |||
if (sortField === field) { | |||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); | |||
} else { | |||
setSortField(field); | |||
setSortDirection('asc'); | |||
} | |||
}; | |||
const sortedEvents = [...events].sort((a, b) => { | |||
let aValue = a[sortField as keyof typeof a]; | |||
let bValue = b[sortField as keyof typeof b]; | |||
if (sortField === 'created_at') { | |||
aValue = new Date(aValue as string).getTime(); | |||
bValue = new Date(bValue as string).getTime(); | |||
} | |||
if (aValue === null) return 1; | |||
if (bValue === null) return -1; | |||
if (aValue < bValue) return sortDirection === 'asc' ? -1 : 1; | |||
if (aValue > bValue) return sortDirection === 'asc' ? 1 : -1; | |||
return 0; | |||
}); | |||
const formatDate = (dateString: string) => { | |||
const date = new Date(dateString); | |||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); | |||
}; | |||
const getCategoryColor = (category: string) => { | |||
const colors: Record<string, string> = { | |||
'signup': 'bg-primary-100 text-primary-800 dark:bg-primary-900/30 dark:text-primary-300', | |||
'login': 'bg-success-100 text-success-800 dark:bg-success-900/30 dark:text-success-300', | |||
'purchase': 'bg-accent-100 text-accent-800 dark:bg-accent-900/30 dark:text-accent-300', | |||
'page_view': 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300', | |||
'click': 'bg-secondary-100 text-secondary-800 dark:bg-secondary-900/30 dark:text-secondary-300', | |||
}; | |||
return colors[category?.toLowerCase()] || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'; | |||
}; | |||
return ( | |||
<Card className={`h-full overflow-hidden ${className}`}> | |||
<CardHeader> | |||
<CardTitle>Recent Events</CardTitle> | |||
</CardHeader> | |||
<CardContent className="p-0"> | |||
<div className="overflow-x-auto"> | |||
<table className="w-full"> | |||
<thead className="bg-gray-50 dark:bg-gray-800 text-left"> | |||
<tr> | |||
<th | |||
className="px-6 py-3 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:text-gray-700 dark:hover:text-gray-200" | |||
onClick={() => handleSort('name')} | |||
> | |||
<div className="flex items-center"> | |||
Name | |||
{sortField === 'name' && ( | |||
<span className="ml-1"> | |||
{sortDirection === 'asc' ? '↑' : '↓'} | |||
</span> | |||
)} | |||
</div> | |||
</th> | |||
<th | |||
className="px-6 py-3 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:text-gray-700 dark:hover:text-gray-200" | |||
onClick={() => handleSort('category')} | |||
> | |||
<div className="flex items-center"> | |||
Category | |||
{sortField === 'category' && ( | |||
<span className="ml-1"> | |||
{sortDirection === 'asc' ? '↑' : '↓'} | |||
</span> | |||
)} | |||
</div> | |||
</th> | |||
<th | |||
className="px-6 py-3 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:text-gray-700 dark:hover:text-gray-200" | |||
onClick={() => handleSort('created_at')} | |||
> | |||
<div className="flex items-center"> | |||
Date | |||
{sortField === 'created_at' && ( | |||
<span className="ml-1"> | |||
{sortDirection === 'asc' ? '↑' : '↓'} | |||
</span> | |||
)} | |||
</div> | |||
</th> | |||
</tr> | |||
</thead> | |||
<tbody className="divide-y divide-gray-200 dark:divide-gray-700"> | |||
{sortedEvents.length > 0 ? ( | |||
sortedEvents.map((event) => ( | |||
<tr | |||
key={event.id} | |||
className="bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-150" | |||
> | |||
<td className="px-6 py-4 text-sm text-gray-900 dark:text-white"> | |||
<div className="font-medium">{event.name}</div> | |||
{event.description && ( | |||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1 line-clamp-1"> | |||
{event.description} | |||
</div> | |||
)} | |||
</td> | |||
<td className="px-6 py-4 text-sm"> | |||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getCategoryColor(event.category)}`}> | |||
{event.category} | |||
</span> | |||
</td> | |||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400"> | |||
{formatDate(event.created_at)} | |||
</td> | |||
</tr> | |||
)) | |||
) : ( | |||
<tr> | |||
<td colSpan={3} className="px-6 py-4 text-sm text-center text-gray-500 dark:text-gray-400"> | |||
No events found | |||
</td> | |||
</tr> | |||
)} | |||
</tbody> | |||
</table> | |||
</div> | |||
</CardContent> | |||
</Card> | |||
); | |||
}; | |||
export default EventTable; |
@ -0,0 +1,21 @@ | |||
import React from 'react'; | |||
import Navbar from './Navbar'; | |||
interface LayoutProps { | |||
children: React.ReactNode; | |||
} | |||
const Layout: React.FC<LayoutProps> = ({ children }) => { | |||
return ( | |||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 transition-colors duration-200"> | |||
<Navbar /> | |||
<main className="pt-16 pb-8 px-4 sm:px-6 lg:px-8"> | |||
<div className="max-w-7xl mx-auto"> | |||
{children} | |||
</div> | |||
</main> | |||
</div> | |||
); | |||
}; | |||
export default Layout; |
@ -0,0 +1,174 @@ | |||
import React, { useState } from 'react'; | |||
// import { Link, useLocation } from 'react-router-dom'; | |||
import { Menu, X, Sun, Moon, LogOut, User, BarChart2 } from 'lucide-react'; | |||
import { useAuth } from '../../context/AuthContext'; | |||
import { useTheme } from '../../context/ThemeContext'; | |||
import Button from '../ui/Button'; | |||
const Navbar: React.FC = () => { | |||
const { currentUser, signOut } = useAuth(); | |||
const { isDarkMode, toggleTheme } = useTheme(); | |||
const [isMenuOpen, setIsMenuOpen] = useState(false); | |||
// const location = useLocation(); | |||
const toggleMenu = () => { | |||
setIsMenuOpen(!isMenuOpen); | |||
}; | |||
const closeMenu = () => { | |||
setIsMenuOpen(false); | |||
}; | |||
const handleSignOut = async () => { | |||
await signOut(); | |||
closeMenu(); | |||
}; | |||
// Navigation links with active state | |||
const navLinks = [ | |||
{ name: 'Dashboard', path: '/', icon: <BarChart2 size={18} /> }, | |||
{ name: 'Profile', path: '/profile', icon: <User size={18} /> }, | |||
]; | |||
return ( | |||
<nav className="bg-white dark:bg-gray-900 shadow-sm fixed w-full z-10"> | |||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | |||
<div className="flex justify-between h-16"> | |||
<div className="flex items-center"> | |||
{/* <Link to="/" className="flex-shrink-0 flex items-center" onClick={closeMenu}> */} | |||
<div className="h-8 w-8 bg-primary-500 text-white rounded flex items-center justify-center"> | |||
<BarChart2 size={20} /> | |||
</div> | |||
<span className="ml-2 text-xl font-semibold text-gray-900 dark:text-white">EventDash</span> | |||
{/* </Link> */} | |||
</div> | |||
{/* Desktop Navigation */} | |||
<div className="hidden md:flex md:items-center md:space-x-4"> | |||
{currentUser && ( | |||
<div className="flex items-center space-x-4"> | |||
{/* {navLinks.map((link) => ( | |||
<Link | |||
key={link.name} | |||
to={link.path} | |||
className={`flex items-center px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200 ${location.pathname === link.path | |||
? 'text-primary-500 dark:text-primary-400 bg-primary-50 dark:bg-primary-900/20' | |||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800' | |||
}`} | |||
> | |||
<span className="mr-1.5">{link.icon}</span> | |||
{link.name} | |||
</Link> | |||
))} */} | |||
<button | |||
onClick={toggleTheme} | |||
className="ml-2 p-2 rounded-full text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500" | |||
aria-label="Toggle theme" | |||
> | |||
{isDarkMode ? <Sun size={20} /> : <Moon size={20} />} | |||
</button> | |||
<Button | |||
onClick={handleSignOut} | |||
variant="ghost" | |||
size="sm" | |||
className="ml-2" | |||
icon={<LogOut size={18} />} | |||
> | |||
Sign Out | |||
</Button> | |||
</div> | |||
)} | |||
{!currentUser && ( | |||
<div className="flex items-center space-x-4"> | |||
<button | |||
onClick={toggleTheme} | |||
className="p-2 rounded-full text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none" | |||
aria-label="Toggle theme" | |||
> | |||
{isDarkMode ? <Sun size={20} /> : <Moon size={20} />} | |||
</button> | |||
{/* <Link to="/login"> */} | |||
<Button variant="primary" size="sm" icon={<LogOut size={18} />}> | |||
Sign In | |||
</Button> | |||
{/* </Link> */} | |||
</div> | |||
)} | |||
</div> | |||
{/* Mobile menu button */} | |||
<div className="flex items-center md:hidden"> | |||
<button | |||
onClick={toggleTheme} | |||
className="p-2 rounded-full text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none" | |||
aria-label="Toggle theme" | |||
> | |||
{isDarkMode ? <Sun size={20} /> : <Moon size={20} />} | |||
</button> | |||
<button | |||
onClick={toggleMenu} | |||
className="ml-2 p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none" | |||
aria-label="Open menu" | |||
> | |||
{isMenuOpen ? <X size={24} /> : <Menu size={24} />} | |||
</button> | |||
</div> | |||
</div> | |||
</div> | |||
{/* Mobile menu */} | |||
{isMenuOpen && ( | |||
<div className="md:hidden bg-white dark:bg-gray-900 shadow-lg animate-fade-in"> | |||
<div className="px-2 pt-2 pb-3 space-y-1 sm:px-3"> | |||
{currentUser ? ( | |||
<> | |||
{/* {navLinks.map((link) => ( | |||
<Link | |||
key={link.name} | |||
to={link.path} | |||
className={`flex items-center px-3 py-2 rounded-md text-base font-medium ${location.pathname === link.path | |||
? 'text-primary-500 dark:text-primary-400 bg-primary-50 dark:bg-primary-900/20' | |||
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800' | |||
}`} | |||
onClick={closeMenu} | |||
> | |||
<span className="mr-2">{link.icon}</span> | |||
{link.name} | |||
</Link> | |||
))} */} | |||
<div className="pt-4 pb-3 border-t border-gray-200 dark:border-gray-700"> | |||
<Button | |||
onClick={handleSignOut} | |||
variant="ghost" | |||
fullWidth | |||
className="mt-1 justify-start" | |||
icon={<LogOut size={18} />} | |||
> | |||
Sign Out | |||
</Button> | |||
</div> | |||
</> | |||
) : ( | |||
<></> | |||
// <Link | |||
// to="/login" | |||
// className="block px-3 py-2 rounded-md text-base font-medium text-primary-500 dark:text-primary-400 hover:bg-gray-100 dark:hover:bg-gray-800" | |||
// onClick={closeMenu} | |||
// > | |||
// Sign In | |||
// </Link> | |||
)} | |||
</div> | |||
</div> | |||
)} | |||
</nav> | |||
); | |||
}; | |||
export default Navbar; |
@ -0,0 +1,69 @@ | |||
import React from 'react'; | |||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { | |||
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'; | |||
size?: 'sm' | 'md' | 'lg'; | |||
isLoading?: boolean; | |||
fullWidth?: boolean; | |||
icon?: React.ReactNode; | |||
iconPosition?: 'left' | 'right'; | |||
} | |||
const Button: React.FC<ButtonProps> = ({ | |||
children, | |||
variant = 'primary', | |||
size = 'md', | |||
isLoading = false, | |||
fullWidth = false, | |||
icon, | |||
iconPosition = 'left', | |||
className = '', | |||
disabled, | |||
...props | |||
}) => { | |||
const baseClasses = 'inline-flex items-center justify-center font-medium transition-colors duration-200 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2'; | |||
const variantClasses = { | |||
primary: 'bg-primary-500 text-white hover:bg-primary-600 focus:ring-primary-500', | |||
secondary: 'bg-secondary-500 text-white hover:bg-secondary-600 focus:ring-secondary-500', | |||
outline: 'border border-gray-300 dark:border-gray-700 bg-transparent hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-200 focus:ring-primary-500', | |||
ghost: 'bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-200 focus:ring-primary-500', | |||
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500', | |||
}; | |||
const sizeClasses = { | |||
sm: 'px-3 py-1.5 text-sm', | |||
md: 'px-4 py-2 text-base', | |||
lg: 'px-6 py-3 text-lg', | |||
}; | |||
const widthClass = fullWidth ? 'w-full' : ''; | |||
const disabledClass = disabled || isLoading ? 'opacity-60 cursor-not-allowed' : ''; | |||
return ( | |||
<button | |||
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${widthClass} ${disabledClass} ${className}`} | |||
disabled={disabled || isLoading} | |||
{...props} | |||
> | |||
{isLoading && ( | |||
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-current\" xmlns="http://www.w3.org/2000/svg\" fill="none\" viewBox="0 0 24 24"> | |||
<circle className="opacity-25\" cx="12\" cy="12\" r="10\" stroke="currentColor\" strokeWidth="4"></circle> | |||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> | |||
</svg> | |||
)} | |||
{!isLoading && icon && iconPosition === 'left' && ( | |||
<span className="mr-2">{icon}</span> | |||
)} | |||
{children} | |||
{!isLoading && icon && iconPosition === 'right' && ( | |||
<span className="ml-2">{icon}</span> | |||
)} | |||
</button> | |||
); | |||
}; | |||
export default Button; |
@ -0,0 +1,74 @@ | |||
import React from 'react'; | |||
interface CardProps { | |||
children: React.ReactNode; | |||
className?: string; | |||
onClick?: () => void; | |||
hover?: boolean; | |||
} | |||
const Card: React.FC<CardProps> = ({ | |||
children, | |||
className = '', | |||
onClick, | |||
hover = false | |||
}) => { | |||
const baseClasses = 'bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-card'; | |||
const hoverClasses = hover ? 'transition-shadow duration-300 hover:shadow-card-hover' : ''; | |||
const clickableClasses = onClick ? 'cursor-pointer' : ''; | |||
return ( | |||
<div | |||
className={`${baseClasses} ${hoverClasses} ${clickableClasses} ${className}`} | |||
onClick={onClick} | |||
> | |||
{children} | |||
</div> | |||
); | |||
}; | |||
export const CardHeader: React.FC<{ | |||
children: React.ReactNode; | |||
className?: string; | |||
}> = ({ children, className = '' }) => { | |||
return ( | |||
<div className={`px-6 py-4 border-b border-gray-200 dark:border-gray-700 ${className}`}> | |||
{children} | |||
</div> | |||
); | |||
}; | |||
export const CardTitle: React.FC<{ | |||
children: React.ReactNode; | |||
className?: string; | |||
}> = ({ children, className = '' }) => { | |||
return ( | |||
<h3 className={`text-lg font-semibold text-gray-900 dark:text-white ${className}`}> | |||
{children} | |||
</h3> | |||
); | |||
}; | |||
export const CardContent: React.FC<{ | |||
children: React.ReactNode; | |||
className?: string; | |||
}> = ({ children, className = '' }) => { | |||
return ( | |||
<div className={`px-6 py-4 ${className}`}> | |||
{children} | |||
</div> | |||
); | |||
}; | |||
export const CardFooter: React.FC<{ | |||
children: React.ReactNode; | |||
className?: string; | |||
}> = ({ children, className = '' }) => { | |||
return ( | |||
<div className={`px-6 py-4 bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 ${className}`}> | |||
{children} | |||
</div> | |||
); | |||
}; | |||
export default Card; |
@ -0,0 +1,123 @@ | |||
import React, { createContext, useContext, useEffect, useState } from 'react'; | |||
import { | |||
User, | |||
signInWithPopup, | |||
signOut as firebaseSignOut, | |||
onAuthStateChanged | |||
} from 'firebase/auth'; | |||
import { auth, googleProvider } from '../lib/firebase'; | |||
// import { supabase } from '../lib/supabase'; | |||
interface AuthContextType { | |||
currentUser: User | null; | |||
loading: boolean; | |||
signInWithGoogle: () => Promise<void>; | |||
signOut: () => Promise<void>; | |||
userProfile: any | null; | |||
} | |||
const AuthContext = createContext<AuthContextType>({ | |||
currentUser: null, | |||
loading: true, | |||
signInWithGoogle: async () => { }, | |||
signOut: async () => { }, | |||
userProfile: null | |||
}); | |||
export const useAuth = () => useContext(AuthContext); | |||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { | |||
const [currentUser, setCurrentUser] = useState<User | null>(null); | |||
const [loading, setLoading] = useState(true); | |||
const [userProfile, setUserProfile] = useState<any | null>(null); | |||
// Function to sync Firebase user with Supabase | |||
const syncUserWithDatabase = async (user: User) => { | |||
if (!user) return null; | |||
try { | |||
// Check if user exists in database | |||
// const { data: existingUser } = await supabase | |||
// .from('users') | |||
// .select('*') | |||
// .eq('id', user.uid) | |||
// .single(); | |||
// if (!existingUser) { | |||
// // Create new user in database | |||
// const { data: newUser, error } = await supabase | |||
// .from('users') | |||
// .insert({ | |||
// id: user.uid, | |||
// email: user.email || '', | |||
// display_name: user.displayName, | |||
// avatar_url: user.photoURL, | |||
// role: 'user' | |||
// }) | |||
// .select() | |||
// .single(); | |||
// if (error) throw error; | |||
// return newUser; | |||
// } | |||
// return existingUser; | |||
} catch (error) { | |||
console.error('Error syncing user with database:', error); | |||
return null; | |||
} | |||
}; | |||
// Sign in with Google | |||
const signInWithGoogle = async () => { | |||
try { | |||
const result = await signInWithPopup(auth, googleProvider); | |||
const profile = await syncUserWithDatabase(result.user); | |||
setUserProfile(profile); | |||
} catch (error) { | |||
console.error('Error signing in with Google:', error); | |||
} | |||
}; | |||
// Sign out | |||
const signOut = async () => { | |||
try { | |||
await firebaseSignOut(auth); | |||
setUserProfile(null); | |||
} catch (error) { | |||
console.error('Error signing out:', error); | |||
} | |||
}; | |||
// Listen for auth state changes | |||
useEffect(() => { | |||
const unsubscribe = onAuthStateChanged(auth, async (user) => { | |||
setCurrentUser(user); | |||
if (user) { | |||
const profile = await syncUserWithDatabase(user); | |||
setUserProfile(profile); | |||
} else { | |||
setUserProfile(null); | |||
} | |||
setLoading(false); | |||
}); | |||
return unsubscribe; | |||
}, []); | |||
const value = { | |||
currentUser, | |||
loading, | |||
signInWithGoogle, | |||
signOut, | |||
userProfile | |||
}; | |||
return ( | |||
<AuthContext.Provider value={value}> | |||
{!loading && children} | |||
</AuthContext.Provider> | |||
); | |||
}; |
@ -0,0 +1,41 @@ | |||
import React, { createContext, useContext, useEffect, useState } from 'react'; | |||
interface ThemeContextType { | |||
isDarkMode: boolean; | |||
toggleTheme: () => void; | |||
} | |||
const ThemeContext = createContext<ThemeContextType>({ | |||
isDarkMode: false, | |||
toggleTheme: () => {}, | |||
}); | |||
export const useTheme = () => useContext(ThemeContext); | |||
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { | |||
const [isDarkMode, setIsDarkMode] = useState(() => { | |||
const savedTheme = localStorage.getItem('theme'); | |||
return savedTheme === 'dark' || | |||
(!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches); | |||
}); | |||
useEffect(() => { | |||
if (isDarkMode) { | |||
document.documentElement.classList.add('dark'); | |||
} else { | |||
document.documentElement.classList.remove('dark'); | |||
} | |||
localStorage.setItem('theme', isDarkMode ? 'dark' : 'light'); | |||
}, [isDarkMode]); | |||
const toggleTheme = () => { | |||
setIsDarkMode(prev => !prev); | |||
}; | |||
return ( | |||
<ThemeContext.Provider value={{ isDarkMode, toggleTheme }}> | |||
{children} | |||
</ThemeContext.Provider> | |||
); | |||
}; |
@ -0,0 +1,3 @@ | |||
@tailwind base; | |||
@tailwind components; | |||
@tailwind utilities; |
@ -0,0 +1,14 @@ | |||
import { Pool } from 'pg'; | |||
const pool = new Pool({ | |||
user: process.env.DB_USER, | |||
host: process.env.DB_HOST, | |||
database: process.env.DB_NAME, | |||
password: process.env.DB_PASSWORD, | |||
port: 5432, | |||
ssl: { | |||
rejectUnauthorized: false, // Required for most cloud/VPC DBs | |||
}, | |||
}); | |||
export default pool; |
@ -0,0 +1,25 @@ | |||
import { initializeApp } from 'firebase/app'; | |||
import { getAuth, GoogleAuthProvider } from 'firebase/auth'; | |||
// This would typically come from environment variables | |||
const firebaseConfig = { | |||
apiKey:process.env.VITE_FIREBASE_API_KEY || "YOUR_API_KEY", | |||
authDomain:process.env.VITE_FIREBASE_AUTH_DOMAIN || "YOUR_AUTH_DOMAIN", | |||
projectId:process.env.VITE_FIREBASE_PROJECT_ID || "YOUR_PROJECT_ID", | |||
storageBucket:process.env.VITE_FIREBASE_STORAGE_BUCKET || "YOUR_STORAGE_BUCKET", | |||
messagingSenderId:process.env.VITE_FIREBASE_MESSAGING_SENDER_ID || "YOUR_MESSAGING_SENDER_ID", | |||
appId:process.env.VITE_FIREBASE_APP_ID || "YOUR_APP_ID" | |||
}; | |||
// Initialize Firebase | |||
const app = initializeApp(firebaseConfig); | |||
const auth = getAuth(app); | |||
const googleProvider = new GoogleAuthProvider(); | |||
// Always prompt for account selection | |||
googleProvider.setCustomParameters({ | |||
prompt: 'select_account' | |||
}); | |||
export { auth, googleProvider }; | |||
export default app; |
@ -0,0 +1,13 @@ | |||
export interface Event { | |||
id: string; | |||
title: string; | |||
description: string; | |||
date: string; | |||
// Add other event properties | |||
} | |||
export interface ApiResponse<T> { | |||
data?: T; | |||
message?: string; | |||
error?: string; | |||
} |
@ -0,0 +1,11 @@ | |||
import { StrictMode } from 'react'; | |||
import { createRoot } from 'react-dom/client'; | |||
// import App from './App.tsx'; | |||
import '@/index.css'; | |||
import '@/styles/globals.css'; // ← adjust if path is different | |||
createRoot(document.getElementById('root')!).render( | |||
<StrictMode> | |||
{/* <App /> */} | |||
</StrictMode> | |||
); |
@ -0,0 +1,7 @@ | |||
// src/pages/_app.tsx | |||
import '../index.css'; // correct path to your Tailwind CSS file | |||
import type { AppProps } from 'next/app'; | |||
export default function App({ Component, pageProps }: AppProps) { | |||
return <Component {...pageProps} />; | |||
} |
@ -0,0 +1,41 @@ | |||
import pool from '../../lib/db'; // ✅ adjust this path if needed | |||
export default async function handler(req, res) { | |||
if (req.method !== 'GET') { | |||
return res.status(405).json({ success: false, error: 'Method Not Allowed' }); | |||
} | |||
const { start, end } = req.query; | |||
if (!start || !end) { | |||
return res.status(400).json({ success: false, error: 'Missing start or end date' }); | |||
} | |||
try { | |||
// ✅ Check DB connection | |||
await pool.query('SELECT 1'); | |||
const result = await pool.query( | |||
` | |||
SELECT | |||
DATE(createdat) AS event_date, | |||
eventname, | |||
COUNT(*) AS count | |||
FROM | |||
EventsData | |||
WHERE | |||
DATE(createdat) BETWEEN $1 AND $2 | |||
GROUP BY | |||
DATE(createdat), eventname | |||
ORDER BY | |||
event_date ASC, count DESC; | |||
`, | |||
[start, end] | |||
); | |||
res.status(200).json({ success: true, data: result.rows }); | |||
} catch (err) { | |||
console.error('Error in DB connection or query:', err); | |||
res.status(500).json({ success: false, error: 'Database error or connection issue' }); | |||
} | |||
} |
@ -0,0 +1,234 @@ | |||
import React, { useState, useEffect, useRef } from 'react'; | |||
import { Activity, Users, ShoppingCart, MousePointer, LogIn } from 'lucide-react'; | |||
import { useAuth } from '../context/AuthContext'; | |||
import EventCountCard from '../components/dashboard/EventCountCard'; | |||
import EventChart from '../components/dashboard/EventChart'; | |||
import EventTable from '../components/dashboard/EventTable'; | |||
import axios from 'axios'; | |||
const Dashboard: React.FC = () => { | |||
const { currentUser } = useAuth(); | |||
const [events, setEvents] = useState<any[]>([]); | |||
const [isLoading, setIsLoading] = useState(true); | |||
const [error, setError] = useState<string | null>(null); | |||
const [eventCounts, setEventCounts] = useState<Record<string, number>>({}); | |||
const hasFetched = useRef(false); | |||
useEffect(() => { | |||
const loadData = async () => { | |||
try { | |||
setIsLoading(true); | |||
setError(null); | |||
// In a real app, we'd fetch user-specific data | |||
const userId = currentUser?.uid; | |||
// This is mock data for demonstration purposes | |||
// In a real app, this would come from the Supabase database | |||
const mockEventData = [ | |||
{ id: '1', name: 'User Registration', category: 'Signup', created_at: '2025-03-01T12:00:00Z', description: 'New user registered via email' }, | |||
{ id: '2', name: 'Product Purchase', category: 'Purchase', created_at: '2025-03-02T14:30:00Z', description: 'User purchased premium plan' }, | |||
{ id: '3', name: 'Login Event', category: 'Login', created_at: '2025-03-03T09:15:00Z', description: 'User logged in from mobile device' }, | |||
{ id: '4', name: 'Page View', category: 'Page_View', created_at: '2025-03-03T10:45:00Z', description: 'User viewed pricing page' }, | |||
{ id: '5', name: 'Button Click', category: 'Click', created_at: '2025-03-04T16:20:00Z', description: 'User clicked sign up button' }, | |||
{ id: '6', name: 'Login Event', category: 'Login', created_at: '2025-03-05T08:10:00Z', description: 'User logged in from desktop' }, | |||
{ id: '7', name: 'Page View', category: 'Page_View', created_at: '2025-03-05T11:30:00Z', description: 'User viewed dashboard' }, | |||
{ id: '8', name: 'User Registration', category: 'Signup', created_at: '2025-03-06T13:45:00Z', description: 'New user registered via Google' }, | |||
{ id: '9', name: 'Product Purchase', category: 'Purchase', created_at: '2025-03-07T15:15:00Z', description: 'User upgraded to business plan' }, | |||
{ id: '10', name: 'Button Click', category: 'Click', created_at: '2025-03-08T09:50:00Z', description: 'User clicked help button' }, | |||
]; | |||
if (hasFetched.current) return; | |||
hasFetched.current = true; | |||
const fetchData = async () => { | |||
try { | |||
const res = await axios.get('/api/events-summary?start=2025-05-08&end=2025-05-08'); | |||
console.log('✅ API Response:', res.data); // 👈 This logs the data | |||
// setSummaryData(res.data.data) | |||
setEvents(res.data.data); | |||
const counts: Record<string, number> = {}; | |||
res.data.data.forEach(event => { | |||
const eventname = event?.eventname?.toLowerCase(); | |||
counts[eventname] = (counts?.[eventname] || 0) + 1; | |||
}); | |||
setEventCounts(counts); | |||
} catch (err) { | |||
console.error('❌ API Error:', err); | |||
} | |||
}; | |||
fetchData(); | |||
// Count events by category | |||
// In a real app, we would fetch from Supabase like this: | |||
// const eventData = await fetchEvents(userId); | |||
// const eventCountData = await fetchEventCounts(userId); | |||
// setEvents(eventData); | |||
// setEventCounts(eventCountData); | |||
} catch (err) { | |||
console.error('Error loading dashboard data:', err); | |||
setError('Failed to load dashboard data. Please try again.'); | |||
} finally { | |||
setIsLoading(false); | |||
} | |||
}; | |||
loadData(); | |||
}, [currentUser]); | |||
// Prepare chart data | |||
const barChartData = { | |||
labels: ['Signup', 'Login', 'Purchase', 'Page View', 'Click'], | |||
datasets: [ | |||
{ | |||
label: 'Event Count', | |||
data: [ | |||
eventCounts['signup'] || 0, | |||
eventCounts['login'] || 0, | |||
eventCounts['purchase'] || 0, | |||
eventCounts['page_view'] || 0, | |||
eventCounts['click'] || 0, | |||
], | |||
backgroundColor: [ | |||
'rgba(66, 133, 244, 0.7)', // Google blue | |||
'rgba(52, 168, 83, 0.7)', // Google green | |||
'rgba(251, 188, 5, 0.7)', // Google yellow | |||
'rgba(234, 67, 53, 0.7)', // Google red | |||
'rgba(102, 102, 102, 0.7)', // Gray | |||
], | |||
}, | |||
], | |||
}; | |||
const doughnutChartData = { | |||
labels: ['Signup', 'Login', 'Purchase', 'Page View', 'Click'], | |||
datasets: [ | |||
{ | |||
label: 'Event Distribution', | |||
data: [ | |||
eventCounts['signup'] || 0, | |||
eventCounts['login'] || 0, | |||
eventCounts['purchase'] || 0, | |||
eventCounts['page_view'] || 0, | |||
eventCounts['click'] || 0, | |||
], | |||
backgroundColor: [ | |||
'rgba(66, 133, 244, 0.7)', // Google blue | |||
'rgba(52, 168, 83, 0.7)', // Google green | |||
'rgba(251, 188, 5, 0.7)', // Google yellow | |||
'rgba(234, 67, 53, 0.7)', // Google red | |||
'rgba(102, 102, 102, 0.7)', // Gray | |||
], | |||
borderColor: [ | |||
'rgba(66, 133, 244, 1)', | |||
'rgba(52, 168, 83, 1)', | |||
'rgba(251, 188, 5, 1)', | |||
'rgba(234, 67, 53, 1)', | |||
'rgba(102, 102, 102, 1)', | |||
], | |||
borderWidth: 1, | |||
}, | |||
], | |||
}; | |||
if (isLoading) { | |||
return ( | |||
<div className="min-h-[500px] flex items-center justify-center"> | |||
<div className="animate-pulse-slow flex flex-col items-center"> | |||
<div className="h-12 w-12 rounded-full bg-primary-200 dark:bg-primary-800 mb-4"></div> | |||
<div className="h-4 w-32 bg-gray-200 dark:bg-gray-700 rounded"></div> | |||
</div> | |||
</div> | |||
); | |||
} | |||
if (error) { | |||
return ( | |||
<div className="min-h-[300px] flex items-center justify-center"> | |||
<div className="text-center p-6 max-w-md bg-white dark:bg-gray-800 rounded-lg shadow-md"> | |||
<div className="text-red-500 mb-4"> | |||
<svg className="mx-auto h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> | |||
</svg> | |||
</div> | |||
<h3 className="text-lg font-medium text-gray-900 dark:text-white">Error Loading Dashboard</h3> | |||
<p className="mt-2 text-gray-600 dark:text-gray-400">{error}</p> | |||
<button | |||
onClick={() => window.location.reload()} | |||
className="mt-4 px-4 py-2 bg-primary-500 text-white rounded-md hover:bg-primary-600 transition-colors" | |||
> | |||
Retry | |||
</button> | |||
</div> | |||
</div> | |||
); | |||
} | |||
return ( | |||
<div className="space-y-6 animate-fade-in"> | |||
<div> | |||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Dashboard</h1> | |||
<p className="mt-1 text-sm text-gray-600 dark:text-gray-400"> | |||
Overview of event activity and analytics | |||
</p> | |||
</div> | |||
{/* Stat cards */} | |||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> | |||
<EventCountCard | |||
title="Total Events" | |||
count={events.length} | |||
icon={<Activity size={24} className="text-white" />} | |||
color="bg-primary-500" | |||
change={12} | |||
/> | |||
<EventCountCard | |||
title="Signups" | |||
count={eventCounts['signup'] || 0} | |||
icon={<Users size={24} className="text-white" />} | |||
color="bg-success-500" | |||
change={5} | |||
/> | |||
<EventCountCard | |||
title="Purchases" | |||
count={eventCounts['purchase'] || 0} | |||
icon={<ShoppingCart size={24} className="text-white" />} | |||
color="bg-accent-500" | |||
change={-3} | |||
/> | |||
<EventCountCard | |||
title="Logins" | |||
count={eventCounts['login'] || 0} | |||
icon={<LogIn size={24} className="text-white" />} | |||
color="bg-secondary-500" | |||
change={8} | |||
/> | |||
</div> | |||
{/* Charts */} | |||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> | |||
<EventChart | |||
type="bar" | |||
title="Event Distribution" | |||
data={barChartData} | |||
/> | |||
<EventChart | |||
type="doughnut" | |||
title="Event Types" | |||
data={doughnutChartData} | |||
/> | |||
</div> | |||
{/* Recent Events Table */} | |||
<EventTable events={events} /> | |||
</div> | |||
); | |||
}; | |||
export default Dashboard; |
@ -0,0 +1,13 @@ | |||
// pages/index.tsx | |||
import React from 'react'; | |||
const HomePage = () => { | |||
return ( | |||
<div style={{ padding: '2rem' }}> | |||
<h1>Welcome to EventDash!</h1> | |||
<p>This is your homepage (index.tsx)</p> | |||
</div> | |||
); | |||
}; | |||
export default HomePage; |
@ -0,0 +1,32 @@ | |||
import React from 'react'; | |||
import { useAuth } from '../context/AuthContext'; | |||
import Layout from '../components/layout/Layout'; | |||
import LoginForm from '../components/auth/LoginForm'; | |||
const LoginPage: React.FC = () => { | |||
const { currentUser, loading } = useAuth(); | |||
// If already logged in, redirect to dashboard | |||
if (!loading && currentUser) { | |||
// return <Navigate to="/" replace />; | |||
} | |||
return ( | |||
<Layout> | |||
<div className="min-h-[calc(100vh-64px)] flex flex-col items-center justify-center py-12 px-4 sm:px-6 lg:px-8"> | |||
<div className="w-full max-w-md space-y-8"> | |||
<div className="text-center"> | |||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Welcome to EventDash</h1> | |||
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400"> | |||
Sign in to view your event analytics | |||
</p> | |||
</div> | |||
<LoginForm /> | |||
</div> | |||
</div> | |||
</Layout> | |||
); | |||
}; | |||
export default LoginPage; |
@ -0,0 +1,211 @@ | |||
import React, { useState } from 'react'; | |||
// import { Navigate } from 'react-router-dom'; | |||
import { User, Mail, Key } from 'lucide-react'; | |||
import { useAuth } from '../context/AuthContext'; | |||
import Layout from '../components/layout/Layout'; | |||
import Card, { CardContent, CardHeader, CardTitle } from '../components/ui/Card'; | |||
import Button from '../components/ui/Button'; | |||
const ProfilePage: React.FC = () => { | |||
const { currentUser, loading, userProfile } = useAuth(); | |||
const [isEditing, setIsEditing] = useState(false); | |||
// If not logged in, redirect to login page | |||
if (!loading && !currentUser) { | |||
// return <Navigate to="/login" replace />; | |||
} | |||
if (loading) { | |||
return ( | |||
<Layout> | |||
<div className="min-h-[300px] flex items-center justify-center"> | |||
<div className="animate-pulse-slow flex flex-col items-center"> | |||
<div className="h-12 w-12 rounded-full bg-primary-200 dark:bg-primary-800 mb-4"></div> | |||
<div className="h-4 w-32 bg-gray-200 dark:bg-gray-700 rounded"></div> | |||
</div> | |||
</div> | |||
</Layout> | |||
); | |||
} | |||
const avatarUrl = currentUser?.photoURL || 'https://via.placeholder.com/100'; | |||
const displayName = currentUser?.displayName || 'User'; | |||
const email = currentUser?.email; | |||
return ( | |||
<Layout> | |||
<div className="space-y-6 animate-fade-in"> | |||
<div> | |||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Profile</h1> | |||
<p className="mt-1 text-sm text-gray-600 dark:text-gray-400"> | |||
Manage your account settings and preferences | |||
</p> | |||
</div> | |||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> | |||
{/* Profile Card */} | |||
<Card className="lg:col-span-2"> | |||
<CardHeader> | |||
<CardTitle>Profile Information</CardTitle> | |||
</CardHeader> | |||
<CardContent className="space-y-6"> | |||
<div className="flex flex-col md:flex-row items-start md:items-center gap-6"> | |||
<div className="relative"> | |||
<img | |||
src={avatarUrl} | |||
alt="Profile" | |||
className="w-20 h-20 rounded-full object-cover border-2 border-primary-100 dark:border-primary-900" | |||
/> | |||
<div className="absolute bottom-0 right-0 w-5 h-5 bg-green-500 rounded-full border-2 border-white dark:border-gray-900"></div> | |||
</div> | |||
<div> | |||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">{displayName}</h2> | |||
<p className="text-gray-600 dark:text-gray-400">{email}</p> | |||
{userProfile?.role && ( | |||
<div className="mt-1"> | |||
<span className="px-2 py-1 text-xs rounded-full bg-primary-100 text-primary-800 dark:bg-primary-900/30 dark:text-primary-300"> | |||
{userProfile.role} | |||
</span> | |||
</div> | |||
)} | |||
</div> | |||
</div> | |||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |||
<div className="space-y-2"> | |||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300"> | |||
Display Name | |||
</label> | |||
<div className="flex items-center bg-gray-50 dark:bg-gray-900 p-3 rounded-md"> | |||
<User size={18} className="text-gray-400 mr-2" /> | |||
<span className="text-gray-800 dark:text-gray-200">{displayName}</span> | |||
</div> | |||
</div> | |||
<div className="space-y-2"> | |||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300"> | |||
</label> | |||
<div className="flex items-center bg-gray-50 dark:bg-gray-900 p-3 rounded-md"> | |||
<Mail size={18} className="text-gray-400 mr-2" /> | |||
<span className="text-gray-800 dark:text-gray-200">{email}</span> | |||
</div> | |||
</div> | |||
</div> | |||
<div className="pt-4 border-t border-gray-200 dark:border-gray-700"> | |||
<Button | |||
variant="primary" | |||
onClick={() => setIsEditing(!isEditing)} | |||
> | |||
Edit Profile | |||
</Button> | |||
</div> | |||
</CardContent> | |||
</Card> | |||
{/* Account Security */} | |||
<Card> | |||
<CardHeader> | |||
<CardTitle>Account Security</CardTitle> | |||
</CardHeader> | |||
<CardContent className="space-y-6"> | |||
<div className="space-y-2"> | |||
<div className="flex items-center justify-between"> | |||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300"> | |||
Password | |||
</label> | |||
</div> | |||
<div className="flex items-center bg-gray-50 dark:bg-gray-900 p-3 rounded-md"> | |||
<Key size={18} className="text-gray-400 mr-2" /> | |||
<span className="text-gray-800 dark:text-gray-200">••••••••</span> | |||
</div> | |||
</div> | |||
<div className="space-y-2"> | |||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300"> | |||
Two-Factor Authentication | |||
</label> | |||
<div className="flex items-center justify-between bg-gray-50 dark:bg-gray-900 p-3 rounded-md"> | |||
<div className="flex items-center"> | |||
<div className="w-4 h-4 rounded-full bg-red-500 mr-2"></div> | |||
<span className="text-gray-800 dark:text-gray-200">Not enabled</span> | |||
</div> | |||
<Button variant="outline" size="sm"> | |||
Enable | |||
</Button> | |||
</div> | |||
</div> | |||
<div className="pt-4 border-t border-gray-200 dark:border-gray-700"> | |||
<Button variant="outline" fullWidth> | |||
Change Password | |||
</Button> | |||
</div> | |||
</CardContent> | |||
</Card> | |||
</div> | |||
{/* Activity and Preferences */} | |||
<Card> | |||
<CardHeader> | |||
<CardTitle>Account Preferences</CardTitle> | |||
</CardHeader> | |||
<CardContent> | |||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> | |||
<div className="space-y-2"> | |||
<label className="flex items-center space-x-2"> | |||
<input | |||
type="checkbox" | |||
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded" | |||
defaultChecked | |||
/> | |||
<span className="text-sm text-gray-700 dark:text-gray-300">Email notifications</span> | |||
</label> | |||
<p className="text-xs text-gray-500 dark:text-gray-400"> | |||
Receive email notifications about your account activity | |||
</p> | |||
</div> | |||
<div className="space-y-2"> | |||
<label className="flex items-center space-x-2"> | |||
<input | |||
type="checkbox" | |||
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded" | |||
defaultChecked | |||
/> | |||
<span className="text-sm text-gray-700 dark:text-gray-300">Marketing emails</span> | |||
</label> | |||
<p className="text-xs text-gray-500 dark:text-gray-400"> | |||
Receive updates about product news and features | |||
</p> | |||
</div> | |||
<div className="space-y-2"> | |||
<label className="flex items-center space-x-2"> | |||
<input | |||
type="checkbox" | |||
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded" | |||
/> | |||
<span className="text-sm text-gray-700 dark:text-gray-300">Data sharing</span> | |||
</label> | |||
<p className="text-xs text-gray-500 dark:text-gray-400"> | |||
Allow anonymous usage data to improve our service | |||
</p> | |||
</div> | |||
</div> | |||
<div className="mt-6 flex justify-end"> | |||
<Button> | |||
Save Preferences | |||
</Button> | |||
</div> | |||
</CardContent> | |||
</Card> | |||
</div> | |||
</Layout> | |||
); | |||
}; | |||
export default ProfilePage; |
@ -0,0 +1,78 @@ | |||
export type Json = | |||
| string | |||
| number | |||
| boolean | |||
| null | |||
| { [key: string]: Json | undefined } | |||
| Json[] | |||
export interface Database { | |||
public: { | |||
Tables: { | |||
events: { | |||
Row: { | |||
id: string | |||
created_at: string | |||
user_id: string | |||
category: string | |||
name: string | |||
description: string | null | |||
metadata: Json | null | |||
} | |||
Insert: { | |||
id?: string | |||
created_at?: string | |||
user_id: string | |||
category: string | |||
name: string | |||
description?: string | null | |||
metadata?: Json | null | |||
} | |||
Update: { | |||
id?: string | |||
created_at?: string | |||
user_id?: string | |||
category?: string | |||
name?: string | |||
description?: string | null | |||
metadata?: Json | null | |||
} | |||
} | |||
users: { | |||
Row: { | |||
id: string | |||
email: string | |||
created_at: string | |||
display_name: string | null | |||
avatar_url: string | null | |||
role: string | |||
} | |||
Insert: { | |||
id: string | |||
email: string | |||
created_at?: string | |||
display_name?: string | null | |||
avatar_url?: string | null | |||
role?: string | |||
} | |||
Update: { | |||
id?: string | |||
email?: string | |||
created_at?: string | |||
display_name?: string | null | |||
avatar_url?: string | null | |||
role?: string | |||
} | |||
} | |||
} | |||
Views: { | |||
[_ in never]: never | |||
} | |||
Functions: { | |||
[_ in never]: never | |||
} | |||
Enums: { | |||
[_ in never]: never | |||
} | |||
} | |||
} |
@ -0,0 +1,105 @@ | |||
/** @type {import('tailwindcss').Config} */ | |||
module.exports = { | |||
content: [ | |||
'./src/**/*.{js,ts,jsx,tsx}', | |||
'./pages/**/*.{js,ts,jsx,tsx}', | |||
'./components/**/*.{js,ts,jsx,tsx}', | |||
], | |||
darkMode: 'class', | |||
theme: { | |||
extend: { | |||
colors: { | |||
primary: { | |||
50: '#eef5ff', | |||
100: '#d9e8ff', | |||
200: '#bcd7ff', | |||
300: '#8cbeff', | |||
400: '#569cff', | |||
500: '#4285F4', | |||
600: '#2563eb', | |||
700: '#1d4ed8', | |||
800: '#1e40af', | |||
900: '#1e3a8a', | |||
950: '#172554', | |||
}, | |||
secondary: { | |||
50: '#fdf2f2', | |||
100: '#fde8e8', | |||
200: '#fbd5d5', | |||
300: '#f8b4b4', | |||
400: '#f98080', | |||
500: '#EA4335', | |||
600: '#e02424', | |||
700: '#c81e1e', | |||
800: '#9b1c1c', | |||
900: '#771d1d', | |||
950: '#450a0a', | |||
}, | |||
accent: { | |||
50: '#fffbeb', | |||
100: '#fef3c7', | |||
200: '#fde68a', | |||
300: '#fcd34d', | |||
400: '#FBBC05', | |||
500: '#f59e0b', | |||
600: '#d97706', | |||
700: '#b45309', | |||
800: '#92400e', | |||
900: '#78350f', | |||
950: '#451a03', | |||
}, | |||
success: { | |||
50: '#ecfdf5', | |||
100: '#d1fae5', | |||
200: '#a7f3d0', | |||
300: '#6ee7b7', | |||
400: '#34d399', | |||
500: '#34A853', | |||
600: '#059669', | |||
700: '#047857', | |||
800: '#065f46', | |||
900: '#064e3b', | |||
950: '#022c22', | |||
}, | |||
gray: { | |||
50: '#f9fafb', | |||
100: '#f3f4f6', | |||
200: '#e5e7eb', | |||
300: '#d1d5db', | |||
400: '#9ca3af', | |||
500: '#6b7280', | |||
600: '#4b5563', | |||
700: '#374151', | |||
800: '#1f2937', | |||
900: '#111827', | |||
950: '#030712', | |||
}, | |||
}, | |||
animation: { | |||
'fade-in': 'fadeIn 0.5s ease-in-out', | |||
'slide-up': 'slideUp 0.3s ease-out', | |||
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite', | |||
}, | |||
keyframes: { | |||
fadeIn: { | |||
'0%': { opacity: '0' }, | |||
'100%': { opacity: '1' }, | |||
}, | |||
slideUp: { | |||
'0%': { transform: 'translateY(10px)', opacity: '0' }, | |||
'100%': { transform: 'translateY(0)', opacity: '1' }, | |||
}, | |||
}, | |||
spacing: { | |||
'72': '18rem', | |||
'84': '21rem', | |||
'96': '24rem', | |||
}, | |||
boxShadow: { | |||
card: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', | |||
'card-hover': '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', | |||
}, | |||
}, | |||
}, | |||
plugins: [], | |||
}; |
@ -0,0 +1,24 @@ | |||
{ | |||
"compilerOptions": { | |||
"target": "ES2020", | |||
"useDefineForClassFields": true, | |||
"lib": ["ES2020", "DOM", "DOM.Iterable"], | |||
"module": "ESNext", | |||
"skipLibCheck": true, | |||
/* Bundler mode */ | |||
"moduleResolution": "bundler", | |||
"allowImportingTsExtensions": true, | |||
"isolatedModules": true, | |||
"moduleDetection": "force", | |||
"noEmit": true, | |||
"jsx": "react-jsx", | |||
/* Linting */ | |||
"strict": true, | |||
"noUnusedLocals": true, | |||
"noUnusedParameters": true, | |||
"noFallthroughCasesInSwitch": true | |||
}, | |||
"include": ["src"] | |||
} |
@ -0,0 +1,28 @@ | |||
{ | |||
"compilerOptions": { | |||
"lib": [ | |||
"dom", | |||
"dom.iterable", | |||
"esnext" | |||
], | |||
"allowJs": true, | |||
"skipLibCheck": true, | |||
"strict": false, | |||
"noEmit": true, | |||
"incremental": true, | |||
"module": "esnext", | |||
"esModuleInterop": true, | |||
"moduleResolution": "node", | |||
"resolveJsonModule": true, | |||
"isolatedModules": true, | |||
"jsx": "preserve" | |||
}, | |||
"include": [ | |||
"next-env.d.ts", | |||
"**/*.ts", | |||
"**/*.tsx" | |||
], | |||
"exclude": [ | |||
"node_modules" | |||
] | |||
} |
Powered by TurnKey Linux.