@ -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.