Browse Source

first commit

master
khanshanawaz10 4 months ago
commit
4c90cbad42
34 changed files with 9400 additions and 0 deletions
  1. +12
    -0
      .env
  2. +36
    -0
      .gitignore
  3. +28
    -0
      eslint.config.js
  4. +14
    -0
      index.html
  5. +5
    -0
      next-env.d.ts
  6. +7
    -0
      next.config.js
  7. +7510
    -0
      package-lock.json
  8. +38
    -0
      package.json
  9. +6
    -0
      postcss.config.js
  10. +85
    -0
      src/components/auth/LoginForm.tsx
  11. +110
    -0
      src/components/dashboard/EventChart.tsx
  12. +55
    -0
      src/components/dashboard/EventCountCard.tsx
  13. +153
    -0
      src/components/dashboard/EventTable.tsx
  14. +21
    -0
      src/components/layout/Layout.tsx
  15. +174
    -0
      src/components/layout/Navbar.tsx
  16. +69
    -0
      src/components/ui/Button.tsx
  17. +74
    -0
      src/components/ui/Card.tsx
  18. +123
    -0
      src/context/AuthContext.tsx
  19. +41
    -0
      src/context/ThemeContext.tsx
  20. +3
    -0
      src/index.css
  21. +14
    -0
      src/lib/db.js
  22. +25
    -0
      src/lib/firebase.ts
  23. +13
    -0
      src/lib/types.ts
  24. +11
    -0
      src/main.tsx
  25. +7
    -0
      src/pages/_app.tsx
  26. +41
    -0
      src/pages/api/events-summary.js
  27. +234
    -0
      src/pages/dashboard.tsx
  28. +13
    -0
      src/pages/index.tsx
  29. +32
    -0
      src/pages/loginPage.tsx
  30. +211
    -0
      src/pages/profilePage.tsx
  31. +78
    -0
      src/types/supabase.ts
  32. +105
    -0
      tailwind.config.js
  33. +24
    -0
      tsconfig.app.json
  34. +28
    -0
      tsconfig.json

+ 12
- 0
.env View File

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

+ 36
- 0
.gitignore View File

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

+ 28
- 0
eslint.config.js View File

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

+ 14
- 0
index.html View File

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

+ 5
- 0
next-env.d.ts View File

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

+ 7
- 0
next.config.js View File

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

+ 7510
- 0
package-lock.json
File diff suppressed because it is too large
View File


+ 38
- 0
package.json View File

@ -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"
}
}

+ 6
- 0
postcss.config.js View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

+ 85
- 0
src/components/auth/LoginForm.tsx View File

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

+ 110
- 0
src/components/dashboard/EventChart.tsx View File

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

+ 55
- 0
src/components/dashboard/EventCountCard.tsx View File

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

+ 153
- 0
src/components/dashboard/EventTable.tsx View File

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

+ 21
- 0
src/components/layout/Layout.tsx View File

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

+ 174
- 0
src/components/layout/Navbar.tsx View File

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

+ 69
- 0
src/components/ui/Button.tsx View File

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

+ 74
- 0
src/components/ui/Card.tsx View File

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

+ 123
- 0
src/context/AuthContext.tsx View File

@ -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>
);
};

+ 41
- 0
src/context/ThemeContext.tsx View File

@ -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>
);
};

+ 3
- 0
src/index.css View File

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

+ 14
- 0
src/lib/db.js View File

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

+ 25
- 0
src/lib/firebase.ts View File

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

+ 13
- 0
src/lib/types.ts View File

@ -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;
}

+ 11
- 0
src/main.tsx View File

@ -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>
);

+ 7
- 0
src/pages/_app.tsx View File

@ -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} />;
}

+ 41
- 0
src/pages/api/events-summary.js View File

@ -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' });
}
}

+ 234
- 0
src/pages/dashboard.tsx View File

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

+ 13
- 0
src/pages/index.tsx View File

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

+ 32
- 0
src/pages/loginPage.tsx View File

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

+ 211
- 0
src/pages/profilePage.tsx View File

@ -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">
Email
</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;

+ 78
- 0
src/types/supabase.ts View File

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

+ 105
- 0
tailwind.config.js View File

@ -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: [],
};

+ 24
- 0
tsconfig.app.json View File

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

+ 28
- 0
tsconfig.json View File

@ -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"
]
}

Loading…
Cancel
Save

Powered by TurnKey Linux.