Add .npm-cache to gitignore; users route and mobile header updates
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -37,6 +37,7 @@ backend/uploads/
|
||||
# Tooling
|
||||
.turbo/
|
||||
.cursor/
|
||||
.npm-cache/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Hono } from 'hono';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { z } from 'zod';
|
||||
import { db, dbGet, dbAll, users, tickets, events, payments } from '../db/index.js';
|
||||
import { db, dbGet, dbAll, users, tickets, events, payments, magicLinkTokens, userSessions, invoices, auditLogs, emailLogs, paymentOptions, legalPages, siteSettings } from '../db/index.js';
|
||||
import { eq, desc, sql } from 'drizzle-orm';
|
||||
import { requireAuth } from '../lib/auth.js';
|
||||
import { getNow } from '../lib/utils.js';
|
||||
@@ -191,14 +191,73 @@ usersRouter.delete('/:id', requireAuth(['admin']), async (c) => {
|
||||
.where(eq((tickets as any).userId, id))
|
||||
);
|
||||
|
||||
// Delete payments associated with user's tickets
|
||||
// Delete invoices associated with user's tickets (invoices reference payments which reference tickets)
|
||||
for (const ticket of userTickets) {
|
||||
// Get payments for this ticket
|
||||
const ticketPayments = await dbAll<any>(
|
||||
(db as any)
|
||||
.select()
|
||||
.from(payments)
|
||||
.where(eq((payments as any).ticketId, ticket.id))
|
||||
);
|
||||
|
||||
// Delete invoices for each payment
|
||||
for (const payment of ticketPayments) {
|
||||
await (db as any).delete(invoices).where(eq((invoices as any).paymentId, payment.id));
|
||||
}
|
||||
|
||||
// Delete payments for this ticket
|
||||
await (db as any).delete(payments).where(eq((payments as any).ticketId, ticket.id));
|
||||
}
|
||||
|
||||
// Delete invoices directly associated with the user (if any)
|
||||
await (db as any).delete(invoices).where(eq((invoices as any).userId, id));
|
||||
|
||||
// Delete user's tickets
|
||||
await (db as any).delete(tickets).where(eq((tickets as any).userId, id));
|
||||
|
||||
// Delete magic link tokens for the user
|
||||
await (db as any).delete(magicLinkTokens).where(eq((magicLinkTokens as any).userId, id));
|
||||
|
||||
// Delete user sessions
|
||||
await (db as any).delete(userSessions).where(eq((userSessions as any).userId, id));
|
||||
|
||||
// Set userId to null in audit_logs (nullable reference)
|
||||
await (db as any)
|
||||
.update(auditLogs)
|
||||
.set({ userId: null })
|
||||
.where(eq((auditLogs as any).userId, id));
|
||||
|
||||
// Set sentBy to null in email_logs (nullable reference)
|
||||
await (db as any)
|
||||
.update(emailLogs)
|
||||
.set({ sentBy: null })
|
||||
.where(eq((emailLogs as any).sentBy, id));
|
||||
|
||||
// Set updatedBy to null in payment_options (nullable reference)
|
||||
await (db as any)
|
||||
.update(paymentOptions)
|
||||
.set({ updatedBy: null })
|
||||
.where(eq((paymentOptions as any).updatedBy, id));
|
||||
|
||||
// Set updatedBy to null in legal_pages (nullable reference)
|
||||
await (db as any)
|
||||
.update(legalPages)
|
||||
.set({ updatedBy: null })
|
||||
.where(eq((legalPages as any).updatedBy, id));
|
||||
|
||||
// Set updatedBy to null in site_settings (nullable reference)
|
||||
await (db as any)
|
||||
.update(siteSettings)
|
||||
.set({ updatedBy: null })
|
||||
.where(eq((siteSettings as any).updatedBy, id));
|
||||
|
||||
// Clear checkedInByAdminId references in tickets
|
||||
await (db as any)
|
||||
.update(tickets)
|
||||
.set({ checkedInByAdminId: null })
|
||||
.where(eq((tickets as any).checkedInByAdminId, id));
|
||||
|
||||
// Delete the user
|
||||
await (db as any).delete(users).where(eq((users as any).id, id));
|
||||
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useLanguage } from '@/context/LanguageContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import LanguageToggle from '@/components/LanguageToggle';
|
||||
import Button from '@/components/ui/Button';
|
||||
import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline';
|
||||
import clsx from 'clsx';
|
||||
|
||||
function NavLink({ href, children }: { href: string; children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
@@ -33,7 +32,7 @@ function MobileNavLink({ href, children, onClick }: { href: string; children: Re
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className="px-4 py-2 hover:bg-gray-50 rounded-lg font-medium"
|
||||
className="block px-6 py-3 text-lg font-medium transition-colors hover:bg-gray-50"
|
||||
style={{ color: isActive ? '#FBB82B' : '#002F44' }}
|
||||
onClick={onClick}
|
||||
>
|
||||
@@ -46,6 +45,65 @@ export default function Header() {
|
||||
const { t } = useLanguage();
|
||||
const { user, isAdmin, logout } = useAuth();
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const touchStartX = useRef<number>(0);
|
||||
const touchCurrentX = useRef<number>(0);
|
||||
const isDragging = useRef<boolean>(false);
|
||||
|
||||
// Close menu on route change
|
||||
const pathname = usePathname();
|
||||
useEffect(() => {
|
||||
setMobileMenuOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
// Prevent body scroll when menu is open
|
||||
useEffect(() => {
|
||||
if (mobileMenuOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
return () => {
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, [mobileMenuOpen]);
|
||||
|
||||
// Handle swipe to close
|
||||
const handleTouchStart = useCallback((e: React.TouchEvent) => {
|
||||
touchStartX.current = e.touches[0].clientX;
|
||||
touchCurrentX.current = e.touches[0].clientX;
|
||||
isDragging.current = true;
|
||||
}, []);
|
||||
|
||||
const handleTouchMove = useCallback((e: React.TouchEvent) => {
|
||||
if (!isDragging.current) return;
|
||||
touchCurrentX.current = e.touches[0].clientX;
|
||||
|
||||
const deltaX = touchCurrentX.current - touchStartX.current;
|
||||
// Only allow dragging to the right (to close)
|
||||
if (deltaX > 0 && menuRef.current) {
|
||||
menuRef.current.style.transform = `translateX(${deltaX}px)`;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleTouchEnd = useCallback(() => {
|
||||
if (!isDragging.current) return;
|
||||
isDragging.current = false;
|
||||
|
||||
const deltaX = touchCurrentX.current - touchStartX.current;
|
||||
const threshold = 100; // Minimum swipe distance to close
|
||||
|
||||
if (menuRef.current) {
|
||||
menuRef.current.style.transform = '';
|
||||
if (deltaX > threshold) {
|
||||
setMobileMenuOpen(false);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const closeMenu = useCallback(() => {
|
||||
setMobileMenuOpen(false);
|
||||
}, []);
|
||||
|
||||
const navLinks = [
|
||||
{ href: '/', label: t('nav.home') },
|
||||
@@ -118,81 +176,142 @@ export default function Header() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
{/* Mobile menu button (hamburger) */}
|
||||
<button
|
||||
className="md:hidden p-2 rounded-lg hover:bg-gray-100"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
className="md:hidden p-2 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
onClick={() => setMobileMenuOpen(true)}
|
||||
aria-label="Open menu"
|
||||
>
|
||||
{mobileMenuOpen ? (
|
||||
<XMarkIcon className="w-6 h-6" />
|
||||
) : (
|
||||
<Bars3Icon className="w-6 h-6" />
|
||||
)}
|
||||
<Bars3Icon className="w-6 h-6" style={{ color: '#002F44' }} />
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Mobile Slide-in Menu */}
|
||||
{/* Overlay */}
|
||||
<div
|
||||
className={`
|
||||
fixed inset-0 bg-black/50 z-40 md:hidden
|
||||
transition-opacity duration-300 ease-in-out
|
||||
${mobileMenuOpen ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}
|
||||
`}
|
||||
onClick={closeMenu}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Slide-in Panel */}
|
||||
<div
|
||||
ref={menuRef}
|
||||
className={`
|
||||
fixed top-0 right-0 h-full w-[280px] max-w-[85vw] bg-white z-50 md:hidden
|
||||
shadow-xl transform transition-transform duration-300 ease-in-out
|
||||
${mobileMenuOpen ? 'translate-x-0' : 'translate-x-full'}
|
||||
`}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
{/* Menu Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-100">
|
||||
<Image
|
||||
src="/images/logo-spanglish.png"
|
||||
alt="Spanglish"
|
||||
width={100}
|
||||
height={28}
|
||||
className="h-7 w-auto"
|
||||
/>
|
||||
<button
|
||||
className="p-2 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
onClick={closeMenu}
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<XMarkIcon className="w-6 h-6" style={{ color: '#002F44' }} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
<div
|
||||
className={clsx(
|
||||
'md:hidden overflow-hidden transition-all duration-300',
|
||||
{
|
||||
'max-h-0': !mobileMenuOpen,
|
||||
'max-h-96 pb-4': mobileMenuOpen,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-2 pt-4">
|
||||
{/* Menu Content */}
|
||||
<div className="flex flex-col h-[calc(100%-65px)] overflow-y-auto">
|
||||
{/* Navigation Links */}
|
||||
<nav className="py-4">
|
||||
{navLinks.map((link) => (
|
||||
<MobileNavLink
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
onClick={closeMenu}
|
||||
>
|
||||
{link.label}
|
||||
</MobileNavLink>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="border-t border-gray-100 mt-2 pt-4 px-4">
|
||||
<LanguageToggle variant="buttons" />
|
||||
</div>
|
||||
{/* Divider */}
|
||||
<div className="border-t border-gray-100 mx-6" />
|
||||
|
||||
<div className="px-4 pt-2 flex flex-col gap-2">
|
||||
{user ? (
|
||||
<>
|
||||
<Link href="/dashboard" onClick={() => setMobileMenuOpen(false)}>
|
||||
<Button variant="outline" className="w-full">
|
||||
{t('nav.dashboard')}
|
||||
</Button>
|
||||
</Link>
|
||||
{isAdmin && (
|
||||
<Link href="/admin" onClick={() => setMobileMenuOpen(false)}>
|
||||
<Button variant="outline" className="w-full">
|
||||
{t('nav.admin')}
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
<Button variant="secondary" onClick={logout} className="w-full">
|
||||
{t('nav.logout')}
|
||||
{/* Language Toggle */}
|
||||
<div className="px-6 py-4">
|
||||
<LanguageToggle variant="buttons" />
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="border-t border-gray-100 mx-6" />
|
||||
|
||||
{/* Auth Actions */}
|
||||
<div className="px-6 py-4 flex flex-col gap-3 mt-auto">
|
||||
{user ? (
|
||||
<>
|
||||
<div className="text-sm text-gray-500 mb-2 flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-full bg-[#002F44] flex items-center justify-center text-white text-sm font-medium">
|
||||
{user.name?.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<span className="font-medium text-[#002F44]">{user.name}</span>
|
||||
</div>
|
||||
<Link href="/dashboard" onClick={closeMenu}>
|
||||
<Button variant="outline" className="w-full justify-center">
|
||||
{t('nav.dashboard')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link href="/login" onClick={() => setMobileMenuOpen(false)}>
|
||||
<Button variant="outline" className="w-full">
|
||||
{t('nav.login')}
|
||||
</Link>
|
||||
{isAdmin && (
|
||||
<Link href="/admin" onClick={closeMenu}>
|
||||
<Button variant="outline" className="w-full justify-center">
|
||||
{t('nav.admin')}
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/events" onClick={() => setMobileMenuOpen(false)}>
|
||||
<Button className="w-full">
|
||||
{t('nav.joinEvent')}
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
logout();
|
||||
closeMenu();
|
||||
}}
|
||||
className="w-full justify-center"
|
||||
>
|
||||
{t('nav.logout')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link href="/login" onClick={closeMenu}>
|
||||
<Button variant="outline" className="w-full justify-center">
|
||||
{t('nav.login')}
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/events" onClick={closeMenu}>
|
||||
<Button className="w-full justify-center">
|
||||
{t('nav.joinEvent')}
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Swipe hint */}
|
||||
<div className="px-6 pb-6 pt-2">
|
||||
<p className="text-xs text-gray-400 text-center">
|
||||
Swipe right to close
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user