diff --git a/.gitignore b/.gitignore index 056e71c..e3ca65d 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ backend/uploads/ # Tooling .turbo/ .cursor/ +.npm-cache/ # OS .DS_Store diff --git a/backend/src/routes/users.ts b/backend/src/routes/users.ts index 7a6c64b..ebeeb43 100644 --- a/backend/src/routes/users.ts +++ b/backend/src/routes/users.ts @@ -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( + (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)); diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx index a0c1c65..dc96bdc 100644 --- a/frontend/src/components/layout/Header.tsx +++ b/frontend/src/components/layout/Header.tsx @@ -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 ( @@ -46,6 +45,65 @@ export default function Header() { const { t } = useLanguage(); const { user, isAdmin, logout } = useAuth(); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const menuRef = useRef(null); + const touchStartX = useRef(0); + const touchCurrentX = useRef(0); + const isDragging = useRef(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() { )} - {/* Mobile menu button */} + {/* Mobile menu button (hamburger) */} + + + + {/* Mobile Slide-in Menu */} + {/* Overlay */} +