From 361511aca0bd31aadf4991e3b27fec84dadd3016 Mon Sep 17 00:00:00 2001 From: snackman Date: Thu, 11 Jun 2026 17:38:50 -0400 Subject: [PATCH 1/3] refactor(cavatelli P1): remove ~150 no-unused-vars + enforce rule in CI gate Phase 1 of cavatelli-typing-debt: eliminate all @typescript-eslint/no-unused-vars violations in frontend (156 -> 0) and flip the CI ratchet so the focused gate enforces the rule going forward. - Removed unused imports, locals, dead helper functions, and unused destructured props/state across 78 frontend files (mechanical dead-code removal; no logic changes). - Intentionally-unused symbols that must stay positionally are _-prefixed (pizzaAlgorithm shouldUseHalfAndHalf _style) or handled via ignoreRestSiblings (ReactMarkdown { node, ...props } overrides). - Unused catch bindings converted to optional catch (} catch {). - eslint.config.js: no-unused-vars now ['error'] with argsIgnorePattern, varsIgnorePattern, caughtErrorsIgnorePattern '^_' + ignoreRestSiblings. - eslint.hooks.config.js (CI gate): registered @typescript-eslint plugin and added the same no-unused-vars rule so PRs are blocked on regressions. Verified: npm run lint no-unused-vars count = 0; CI gate command exits 0. (Pre-existing, out-of-scope: vite build fails on a missing heic2any dep that also fails on origin/master; not touched here.) Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/e2e/mocks/api-handlers.ts | 1 - frontend/e2e/specs/auth.spec.ts | 2 +- frontend/e2e/specs/create-event.spec.ts | 4 +- frontend/e2e/specs/host-dashboard.spec.ts | 1 - frontend/e2e/specs/rsvp-flow.spec.ts | 1 - frontend/eslint.config.js | 9 + frontend/eslint.hooks.config.js | 36 ++-- frontend/src/__tests__/field-sync.test.ts | 13 -- frontend/src/components/AICallStatus.tsx | 2 +- frontend/src/components/AddGuestForm.tsx | 1 - frontend/src/components/CheckInScanner.tsx | 2 +- frontend/src/components/CustomUrlInput.tsx | 2 +- frontend/src/components/DonationForm.tsx | 1 - frontend/src/components/EventDetailsTab.tsx | 169 +----------------- .../src/components/LocationAutocomplete.tsx | 2 +- frontend/src/components/OrderCheckout.tsx | 2 +- frontend/src/components/PartyHeader.tsx | 4 +- frontend/src/components/PizzaOrderSummary.tsx | 5 +- frontend/src/components/PizzeriaSelection.tsx | 2 +- frontend/src/components/PlaceAutocomplete.tsx | 2 +- frontend/src/components/RSVPFlowContent.tsx | 1 - frontend/src/components/RSVPFormStep1.tsx | 2 - frontend/src/components/TableRow.tsx | 3 +- .../src/components/TimezonePickerInput.tsx | 2 +- .../src/components/budget/BudgetSettings.tsx | 1 - .../src/components/day-of/DayOfDashboard.tsx | 1 - .../generative/GenerativeCanvas.tsx | 13 -- .../src/components/generative/renderCanvas.ts | 2 +- .../invoice/InvoiceCryptoPayment.tsx | 2 +- .../src/components/kit/PartyKitWidget.tsx | 2 +- frontend/src/components/music/MusicWidget.tsx | 9 +- .../payments-admin/PayoutsFilterBar.tsx | 2 +- .../components/payouts/RolePhotoPicker.tsx | 1 - .../src/components/photos/PhotoGallery.tsx | 6 +- .../src/components/photos/PhotoUpload.tsx | 2 +- frontend/src/components/promo/BulkInvite.tsx | 2 - .../components/promo/PlatformPublisher.tsx | 1 - .../src/components/raffle/RaffleEntry.tsx | 4 +- frontend/src/components/raffle/RaffleForm.tsx | 2 +- .../src/components/raffle/RaffleWidget.tsx | 2 +- .../shipping/CoordinatorManager.tsx | 6 +- .../components/shipping/CsvImportModal.tsx | 2 +- .../components/shipping/KitDetailModal.tsx | 2 +- frontend/src/components/shipping/KitTable.tsx | 2 +- .../src/components/sponsors/InvoiceButton.tsx | 2 +- frontend/src/components/sponsors/MouForm.tsx | 2 +- .../src/components/sponsors/SponsorCRM.tsx | 2 +- .../src/components/sponsors/SponsorList.tsx | 2 +- .../components/sponsors/SponsorPipeline.tsx | 2 +- .../components/staffing/StaffingWidget.tsx | 2 +- .../src/components/underboss/OutreachTab.tsx | 2 +- .../underboss/PartnerCitiesFlyer.tsx | 1 - .../components/underboss/SuperlativesTab.tsx | 2 +- .../underboss/TelegramBroadcast.tsx | 4 +- .../venue-report/VenueReportPreview.tsx | 2 +- .../src/components/venue/VenueForm.test.tsx | 20 --- .../src/components/venue/VenuePhotoUpload.tsx | 2 +- frontend/src/hooks/useRSVPForm.ts | 2 +- frontend/src/lib/api.ts | 2 +- frontend/src/lib/ordering.ts | 1 - frontend/src/pages/AccountPage.tsx | 2 +- frontend/src/pages/AuthVerifyPage.tsx | 2 +- frontend/src/pages/CheckInPage.tsx | 6 +- frontend/src/pages/DJPage.tsx | 4 +- frontend/src/pages/DayOfRunPage.tsx | 2 +- frontend/src/pages/DisplayPage.tsx | 2 +- frontend/src/pages/EventsMapPage.tsx | 1 - frontend/src/pages/GPPLandingPage.tsx | 2 +- frontend/src/pages/HostPage.tsx | 2 +- frontend/src/pages/InvoicePage.tsx | 2 +- frontend/src/pages/MouPage.tsx | 2 +- frontend/src/pages/PartnerDashboardPage.tsx | 5 +- frontend/src/pages/PhotosFeedPage.tsx | 4 +- frontend/src/pages/RSVPPage.tsx | 2 +- frontend/src/pages/ShippingDashboard.tsx | 6 +- frontend/src/utils/beverageAlgorithm.test.ts | 5 +- frontend/src/utils/dateUtils.test.ts | 2 +- frontend/src/utils/dateUtils.ts | 15 -- frontend/src/utils/pizzaAlgorithm.test.ts | 5 +- frontend/src/utils/pizzaAlgorithm.ts | 11 +- 80 files changed, 112 insertions(+), 353 deletions(-) diff --git a/frontend/e2e/mocks/api-handlers.ts b/frontend/e2e/mocks/api-handlers.ts index 890a991cb..de8d204ae 100644 --- a/frontend/e2e/mocks/api-handlers.ts +++ b/frontend/e2e/mocks/api-handlers.ts @@ -1,7 +1,6 @@ import { Page } from '@playwright/test'; import { TestPublicEvent, TestRSVPResponse, TestGuest } from '../fixtures/test-data'; -const API_URL = process.env.VITE_API_URL || 'http://localhost:3006'; /** * Mock the GET /api/events/:slug endpoint to return a specific event. diff --git a/frontend/e2e/specs/auth.spec.ts b/frontend/e2e/specs/auth.spec.ts index 442ffe47a..1e9c21e9d 100644 --- a/frontend/e2e/specs/auth.spec.ts +++ b/frontend/e2e/specs/auth.spec.ts @@ -1,7 +1,7 @@ import { test, expect } from '@playwright/test'; import { mockAuthFlow, setupCommonMocks } from '../mocks/api-handlers'; import { LoginPage, VerifyPage } from '../pages/login.page'; -import { injectAuth, clearAuth, TEST_USER, TEST_TOKEN } from '../fixtures/auth.fixture'; +import { injectAuth, clearAuth, TEST_TOKEN } from '../fixtures/auth.fixture'; test.describe('Authentication Flow', () => { test('visit /login and see email input', async ({ page }) => { diff --git a/frontend/e2e/specs/create-event.spec.ts b/frontend/e2e/specs/create-event.spec.ts index 770a47f3c..6261f34a8 100644 --- a/frontend/e2e/specs/create-event.spec.ts +++ b/frontend/e2e/specs/create-event.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from '@playwright/test'; -import { setupCommonMocks, mockPartyAPI, blockExternalScripts } from '../mocks/api-handlers'; -import { injectAuth, TEST_USER } from '../fixtures/auth.fixture'; +import { setupCommonMocks, blockExternalScripts } from '../mocks/api-handlers'; +import { injectAuth } from '../fixtures/auth.fixture'; test.describe('Create Event', () => { test('authenticated user fills event form and submits', async ({ page }) => { diff --git a/frontend/e2e/specs/host-dashboard.spec.ts b/frontend/e2e/specs/host-dashboard.spec.ts index b47c4cdd7..a0354adf5 100644 --- a/frontend/e2e/specs/host-dashboard.spec.ts +++ b/frontend/e2e/specs/host-dashboard.spec.ts @@ -60,7 +60,6 @@ function setupHostPageMocks(page: import('@playwright/test').Page, partyOverride // Mock Supabase parties query (both custom_url and invite_code lookups) page.route('**/*.supabase.co/rest/v1/parties*', (route, request) => { - const url = request.url(); if (request.method() === 'GET') { return route.fulfill({ status: 200, diff --git a/frontend/e2e/specs/rsvp-flow.spec.ts b/frontend/e2e/specs/rsvp-flow.spec.ts index 3b05ee648..e6aa0462b 100644 --- a/frontend/e2e/specs/rsvp-flow.spec.ts +++ b/frontend/e2e/specs/rsvp-flow.spec.ts @@ -3,7 +3,6 @@ import { mockEventAPI, mockRSVPSubmission, setupCommonMocks, - mockUserPreferences, } from '../mocks/api-handlers'; import { makePublicEvent, diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index a94e5b95d..b0b75e48a 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -24,6 +24,15 @@ export default tseslint.config( 'warn', { allowConstantExport: true }, ], + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + ignoreRestSiblings: true, + }, + ], }, }, { diff --git a/frontend/eslint.hooks.config.js b/frontend/eslint.hooks.config.js index 5bd0f1cc7..d38ff0965 100644 --- a/frontend/eslint.hooks.config.js +++ b/frontend/eslint.hooks.config.js @@ -1,15 +1,15 @@ -// Focused ESLint config: ONLY react-hooks/rules-of-hooks, over src. +// Focused ESLint config: react-hooks/rules-of-hooks + no-unused-vars, over src. // -// The full `npm run lint` carries ~678 pre-existing debt items (no-explicit-any, -// no-unused-vars, exhaustive-deps warnings), so it can't gate PRs as-is. But -// rules-of-hooks is the high-value incident class: a hook declared below an early -// return builds fine yet black-screens at runtime (arugula-38633 v2, 2026-05-19). +// The full `npm run lint` carries pre-existing debt (no-explicit-any, +// exhaustive-deps warnings), so it can't gate PRs as-is. This config runs just +// the high-value, fully-clean rules so CI can block them without the debt noise: +// - react-hooks/rules-of-hooks: a hook declared below an early return builds +// fine yet black-screens at runtime (arugula-38633 v2, 2026-05-19). +// - @typescript-eslint/no-unused-vars: dead-code ratchet, paid down to 0 in +// cavatelli P1; this gate keeps it there. // -// This config runs just that one rule so CI can block it without the debt noise. -// We pull in ONLY the typescript-eslint parser (so .tsx parses) — none of its -// *rules* — which is why the `any`/unused-vars debt doesn't surface here. // e2e/ is intentionally NOT matched: Playwright's `use` fixture param trips the -// rule (false positive), and e2e is not shipped React anyway. +// hooks rule (false positive), and e2e is not shipped React anyway. // // Run via: npx eslint --config eslint.hooks.config.js "src/**/*.{ts,tsx}" import reactHooks from 'eslint-plugin-react-hooks'; @@ -18,6 +18,20 @@ import tseslint from 'typescript-eslint'; export default tseslint.config({ files: ['src/**/*.{ts,tsx}'], languageOptions: { parser: tseslint.parser }, - plugins: { 'react-hooks': reactHooks }, - rules: { 'react-hooks/rules-of-hooks': 'error' }, + plugins: { + 'react-hooks': reactHooks, + '@typescript-eslint': tseslint.plugin, + }, + rules: { + 'react-hooks/rules-of-hooks': 'error', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + ignoreRestSiblings: true, + }, + ], + }, }); diff --git a/frontend/src/__tests__/field-sync.test.ts b/frontend/src/__tests__/field-sync.test.ts index d2f16aacf..d080cdfe3 100644 --- a/frontend/src/__tests__/field-sync.test.ts +++ b/frontend/src/__tests__/field-sync.test.ts @@ -94,19 +94,6 @@ function extractDbPartyToPartyFields(source: string): string[] { return Array.from(fields); } -// Extract fields destructured in the backend PATCH handler -function extractPatchHandlerFields(source: string): string[] { - // Find the PATCH handler destructuring - const patchRegex = /router\.patch\s*\(\s*'\/:id'[\s\S]*?const\s*\{([^}]+)\}\s*=\s*req\.body/; - const match = source.match(patchRegex); - if (!match) return []; - - return match[1] - .split(',') - .map(s => s.trim()) - .filter(s => s.length > 0 && !s.startsWith('//')); -} - describe('Field Mapping Consistency', () => { const safeColumns = parseSafeColumns(SAFE_PARTY_COLUMNS); diff --git a/frontend/src/components/AICallStatus.tsx b/frontend/src/components/AICallStatus.tsx index f9d7776a3..904a2e340 100644 --- a/frontend/src/components/AICallStatus.tsx +++ b/frontend/src/components/AICallStatus.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Phone, PhoneCall, PhoneOff, CheckCircle, XCircle, Loader2, RotateCcw, Clock } from 'lucide-react'; +import { Phone, PhoneCall, PhoneOff, CheckCircle, Loader2, RotateCcw, Clock } from 'lucide-react'; export interface CallStatusData { id: string; diff --git a/frontend/src/components/AddGuestForm.tsx b/frontend/src/components/AddGuestForm.tsx index 3289f7952..4ec5d943e 100644 --- a/frontend/src/components/AddGuestForm.tsx +++ b/frontend/src/components/AddGuestForm.tsx @@ -1,6 +1,5 @@ import React, { useState, useEffect } from 'react'; import { usePizza } from '../contexts/PizzaContext'; -import { Guest } from '../types'; import { UserPlus, Loader2, ThumbsUp, ThumbsDown, User, X } from 'lucide-react'; import { IconInput } from './IconInput'; import { getExcludedToppingIds, DIETARY_OPTIONS } from '../constants/options'; diff --git a/frontend/src/components/CheckInScanner.tsx b/frontend/src/components/CheckInScanner.tsx index 7388ffafa..f9bacc9e7 100644 --- a/frontend/src/components/CheckInScanner.tsx +++ b/frontend/src/components/CheckInScanner.tsx @@ -11,7 +11,7 @@ interface CheckInScannerProps { onClose: () => void; } -export function CheckInScanner({ inviteCode, currentGuestId, onVouchSuccess, onClose }: CheckInScannerProps) { +export function CheckInScanner({ currentGuestId, onVouchSuccess, onClose }: CheckInScannerProps) { const scannerRef = useRef(null); const containerRef = useRef(null); const [status, setStatus] = useState('Starting camera...'); diff --git a/frontend/src/components/CustomUrlInput.tsx b/frontend/src/components/CustomUrlInput.tsx index 2d83a8158..1b94cc058 100644 --- a/frontend/src/components/CustomUrlInput.tsx +++ b/frontend/src/components/CustomUrlInput.tsx @@ -36,7 +36,7 @@ export function CustomUrlInput({ setValidationError(result.valid ? null : result.error || 'Invalid URL'); setIsValid(result.valid); onValidationChange?.(result.valid, result.error); - } catch (err) { + } catch { setValidationError('Failed to validate URL'); setIsValid(false); onValidationChange?.(false, 'Failed to validate URL'); diff --git a/frontend/src/components/DonationForm.tsx b/frontend/src/components/DonationForm.tsx index fe2a676e4..7ae95bd84 100644 --- a/frontend/src/components/DonationForm.tsx +++ b/frontend/src/components/DonationForm.tsx @@ -51,7 +51,6 @@ const DonationFormInner: React.FC = ({ onSuccess, onBack, guestId, - clientSecret, }) => { const stripe = useStripe(); const elements = useElements(); diff --git a/frontend/src/components/EventDetailsTab.tsx b/frontend/src/components/EventDetailsTab.tsx index 2b0c686e7..49b6bfff5 100644 --- a/frontend/src/components/EventDetailsTab.tsx +++ b/frontend/src/components/EventDetailsTab.tsx @@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next'; import { createPortal } from 'react-dom'; import { useNavigate } from 'react-router-dom'; -import { User, Lock, Image as ImageIcon, FileText, Loader2, X, Square as SquareIcon, Ban, RefreshCcw, Calendar, Play, DollarSign, Wand2, MessageCircle, Send, Check } from 'lucide-react'; +import { User, Lock, Image as ImageIcon, FileText, Loader2, X, Square as SquareIcon, Ban, RefreshCcw, Calendar, Play, Wand2, MessageCircle, Send, Check } from 'lucide-react'; import { IconInput } from './IconInput'; import { usePizza } from '../contexts/PizzaContext'; import { updateParty, uploadEventImage, cancelParty, reinstateParty } from '../lib/supabase'; @@ -42,7 +42,7 @@ export const EventDetailsTab: React.FC = () => { const [password, setPassword] = useState(''); const [customUrl, setCustomUrl] = useState(''); const [customUrlValid, setCustomUrlValid] = useState(true); - const [customUrlError, setCustomUrlError] = useState(); + const [, setCustomUrlError] = useState(); const [eventImageUrl, setEventImageUrl] = useState(''); const [eventImageFile, setEventImageFile] = useState(null); const [imagePreview, setImagePreview] = useState(null); @@ -96,9 +96,7 @@ export const EventDetailsTab: React.FC = () => { const [showOptionalFields, setShowOptionalFields] = useState(false); - const [saving, setSaving] = useState(false); const [savingField, setSavingField] = useState(null); - const [saved, setSaved] = useState(false); // porchetta-81402: cancel state (was `deleting` / `showDeleteConfirm` before // the destructive delete was converted to a non-destructive soft-cancel). const [cancelling, setCancelling] = useState(false); @@ -135,7 +133,7 @@ export const EventDetailsTab: React.FC = () => { const partyTimezone = party.timezone || (() => { try { return Intl.DateTimeFormat().resolvedOptions().timeZone; - } catch (error) { + } catch { return 'UTC'; } })(); @@ -303,167 +301,6 @@ export const EventDetailsTab: React.FC = () => { setEventImageUrl(''); }; - // Check if any values have changed - const hasChanges = () => { - if (!originalValues) return false; - - return ( - name !== originalValues.name || - // hostName is no longer editable - it comes from the user account - startDate !== originalValues.startDate || - startTime !== originalValues.startTime || - endDate !== originalValues.endDate || - endTime !== originalValues.endTime || - timezone !== originalValues.timezone || - address !== originalValues.address || - description !== originalValues.description || - password !== originalValues.password || - customUrl !== originalValues.customUrl || - eventImageUrl !== originalValues.eventImageUrl || - maxGuests !== originalValues.maxGuests || - limitGuests !== originalValues.limitGuests || - hideGuests !== originalValues.hideGuests || - eventImageFile !== null - ); - }; - - // Cancel changes and revert to original values - const handleCancelChanges = () => { - if (!originalValues) return; - - setName(originalValues.name); - // hostName is not reset - it's display-only from user account - setStartDate(originalValues.startDate); - setStartTime(originalValues.startTime); - setEndDate(originalValues.endDate); - setEndTime(originalValues.endTime); - setTimezone(originalValues.timezone); - setAddress(originalValues.address); - setDescription(originalValues.description); - setPassword(originalValues.password); - setCustomUrl(originalValues.customUrl); - setEventImageUrl(originalValues.eventImageUrl); - setImagePreview(originalValues.eventImageUrl || null); - setMaxGuests(originalValues.maxGuests); - setLimitGuests(originalValues.limitGuests); - setHideGuests(originalValues.hideGuests); - setEventImageFile(null); - setImageError(null); - }; - - const handleSave = async (e: React.FormEvent) => { - e.preventDefault(); - if (!party) return; - - setSaving(true); - setMessage(null); - - try { - // Upload image if file is selected - let imageUrl = eventImageUrl.trim() || undefined; - if (eventImageFile) { - try { - imageUrl = await uploadEventImage(eventImageFile); - } catch (err) { - throw new Error(err instanceof Error ? err.message : 'Failed to upload image. Please ensure the storage bucket is configured or use an image URL instead.'); - } - } - - // Calculate duration from start/end times - // Use the event's timezone when parsing the entered date/time - const tz = timezone || 'UTC'; - let calculatedDuration: number | null = null; - let startDateTime: string | null = null; - if (startDate && startTime && endDate && endTime) { - const start = parseDateTimeInTimezone(startDate, startTime, tz); - const end = parseDateTimeInTimezone(endDate, endTime, tz); - const durationMs = end.getTime() - start.getTime(); - calculatedDuration = durationMs / (1000 * 60 * 60); // Convert to hours - startDateTime = start.toISOString(); - } else if (startDate && startTime) { - startDateTime = parseDateTimeInTimezone(startDate, startTime, tz).toISOString(); - } - - // Check if custom URL is valid (already validated by CustomUrlInput) - if (customUrl.trim() && !customUrlValid) { - throw new Error(customUrlError || 'Invalid custom URL'); - } - - // Update party in database - // Note: host_name is now derived from User.name via user_id relationship - const success = await updateParty(party.id, { - name: name.trim(), - date: startDateTime, - duration: calculatedDuration, - timezone: timezone || null, - address: address.trim() || null, - venue_name: venueName || null, - description: description.trim() || null, - password: password.trim() || null, - custom_url: customUrl.trim() || null, - event_image_url: imageUrl || null, - max_guests: limitGuests && maxGuests ? parseInt(maxGuests, 10) : null, - hide_guests: hideGuests, - require_approval: requireApproval, - }); - - if (success) { - setSaved(true); - setToast(true); - setTimeout(() => setToast(false), 2000); - // Update original values to match current form state - setOriginalValues({ - name: name.trim(), - hostName: hostName.trim(), - startDate, - startTime, - endDate, - endTime, - timezone, - address: address.trim(), - venueName: venueName, - description: description.trim(), - password: password.trim(), - customUrl: customUrl.trim(), - eventImageUrl: imageUrl || '', - maxGuests, - limitGuests, - hideGuests, - requireApproval, - }); - // burrata-72104 v2: merge the bulk-saved fields into context so - // sibling components see them without a refetch. - const bulkPatch: Partial = { - name: name.trim(), - date: startDateTime, - duration: calculatedDuration, - timezone: timezone || null, - address: address.trim() || null, - venueName: venueName || null, - description: description.trim() || null, - password: password.trim() || null, - customUrl: customUrl.trim() || null, - eventImageUrl: imageUrl || null, - maxGuests: limitGuests && maxGuests ? parseInt(maxGuests, 10) : null, - hideGuests, - requireApproval, - }; - setParty(prev => prev ? { ...prev, ...bulkPatch } : prev); - // Clear the image file since it's been uploaded - setEventImageFile(null); - // Reset saved state after a moment - setTimeout(() => setSaved(false), 2000); - } else { - throw new Error('Failed to update party'); - } - } catch (error) { - console.error('Error updating party:', error); - setMessage({ type: 'error', text: error instanceof Error ? error.message : 'Failed to update event details' }); - } finally { - setSaving(false); - } - }; - // porchetta-81402: soft-cancel the event. The host stays on this page (no // redirect to home), the public URL keeps working, and the host can // reinstate via the symmetric button — no confirm modal needed for diff --git a/frontend/src/components/LocationAutocomplete.tsx b/frontend/src/components/LocationAutocomplete.tsx index bca8683be..e2ff7fe14 100644 --- a/frontend/src/components/LocationAutocomplete.tsx +++ b/frontend/src/components/LocationAutocomplete.tsx @@ -47,7 +47,7 @@ export const LocationAutocomplete: React.FC = ({ className = '' }) => { const inputRef = useRef(null); - const [isLoaded, setIsLoaded] = useState(false); + const [, setIsLoaded] = useState(false); const [autocomplete, setAutocomplete] = useState(null); // Use refs to avoid stale closures in the event listener diff --git a/frontend/src/components/OrderCheckout.tsx b/frontend/src/components/OrderCheckout.tsx index 8f605573f..25bdc8b9b 100644 --- a/frontend/src/components/OrderCheckout.tsx +++ b/frontend/src/components/OrderCheckout.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { Pizzeria, OrderingOption, PizzaRecommendation, OrderItem } from '../types'; import { createSquareOrder, diff --git a/frontend/src/components/PartyHeader.tsx b/frontend/src/components/PartyHeader.tsx index 050954d9c..ca2144ca5 100644 --- a/frontend/src/components/PartyHeader.tsx +++ b/frontend/src/components/PartyHeader.tsx @@ -1,12 +1,12 @@ import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { usePizza } from '../contexts/PizzaContext'; -import { PartyPopper, Link2, Copy, Check, X, Calendar, User, Loader2, Users, MapPin, Lock, Image, FileText, Link as LinkIcon, Upload, Trash2, ChevronDown, ChevronUp, ExternalLink, Pizza, Globe, EyeOff } from 'lucide-react'; +import { PartyPopper, Copy, Check, X, Calendar, User, Loader2, Users, MapPin, Lock, Image, FileText, Link as LinkIcon, Upload, Trash2, ChevronDown, ChevronUp, ExternalLink, Pizza, Globe, EyeOff } from 'lucide-react'; import { uploadEventImage, cdnUrl } from '../lib/supabase'; import { IconInput } from './IconInput'; export const PartyHeader: React.FC = () => { - const { party, createParty, clearParty, getInviteLink, getHostLink } = usePizza(); + const { party, createParty, getInviteLink, getHostLink } = usePizza(); const navigate = useNavigate(); const [showCreateModal, setShowCreateModal] = useState(false); const [showShareModal, setShowShareModal] = useState(false); diff --git a/frontend/src/components/PizzaOrderSummary.tsx b/frontend/src/components/PizzaOrderSummary.tsx index bc0de6916..aec76f39b 100644 --- a/frontend/src/components/PizzaOrderSummary.tsx +++ b/frontend/src/components/PizzaOrderSummary.tsx @@ -15,14 +15,13 @@ import { getCurrentLocation, geocodeAddress, formatDistance, - getProviderName, getProviderColor, supportsDirectOrdering, } from '../lib/ordering'; export const PizzaOrderSummary: React.FC = () => { const { t } = useTranslation('host'); - const { recommendations, beverageRecommendations, waveRecommendations, party, guests, orderExpectedGuests, setOrderExpectedGuests, generateRecommendations, updatePizzaQuantity, removePizza } = usePizza(); + const { recommendations, beverageRecommendations, waveRecommendations, party, guests, updatePizzaQuantity, removePizza } = usePizza(); const isGppEvent = party?.eventType === 'gpp'; const [isCopied, setIsCopied] = useState(false); const [showCallScript, setShowCallScript] = useState(false); @@ -290,7 +289,7 @@ Can you accommodate these delivery times? Please confirm total and timing.`; }; const handleCopyAllWaves = () => { - const allWavesText = waveRecommendations.map((waveRec, index) => { + const allWavesText = waveRecommendations.map((waveRec) => { const pizzaText = waveRec.pizzas .sort((a, b) => (b.quantity || 1) - (a.quantity || 1)) .map(pizza => { diff --git a/frontend/src/components/PizzeriaSelection.tsx b/frontend/src/components/PizzeriaSelection.tsx index 93cf42e2e..bd72f8988 100644 --- a/frontend/src/components/PizzeriaSelection.tsx +++ b/frontend/src/components/PizzeriaSelection.tsx @@ -52,7 +52,7 @@ export const PizzeriaSelection: React.FC = ({ embedded = const [nearbyPizzerias, setNearbyPizzerias] = useState([]); const [loadingPizzerias, setLoadingPizzerias] = useState(false); const [showAddPizzeriaModal, setShowAddPizzeriaModal] = useState(false); - const [savingField, setSavingField] = useState(null); + const [, setSavingField] = useState(null); const [venueLocation, setVenueLocation] = useState<{lat:number;lng:number}|null>(null); // Discount info modal diff --git a/frontend/src/components/PlaceAutocomplete.tsx b/frontend/src/components/PlaceAutocomplete.tsx index 74d94ae26..bd1d6ba99 100644 --- a/frontend/src/components/PlaceAutocomplete.tsx +++ b/frontend/src/components/PlaceAutocomplete.tsx @@ -17,7 +17,7 @@ export const PlaceAutocomplete: React.FC = ({ autoFocus = false, }) => { const inputRef = useRef(null); - const [isLoaded, setIsLoaded] = useState(false); + const [, setIsLoaded] = useState(false); const [loading, setLoading] = useState(false); const autocompleteRef = useRef(null); const onPlaceSelectedRef = useRef(onPlaceSelected); diff --git a/frontend/src/components/RSVPFlowContent.tsx b/frontend/src/components/RSVPFlowContent.tsx index 4b2302a2c..a760ed072 100644 --- a/frontend/src/components/RSVPFlowContent.tsx +++ b/frontend/src/components/RSVPFlowContent.tsx @@ -28,7 +28,6 @@ export function RSVPFlowContent({ event, form, eventName, - closeButtonLabel, onClose, isEditing, walletFieldSlot, diff --git a/frontend/src/components/RSVPFormStep1.tsx b/frontend/src/components/RSVPFormStep1.tsx index 4ca31898e..6759a6834 100644 --- a/frontend/src/components/RSVPFormStep1.tsx +++ b/frontend/src/components/RSVPFormStep1.tsx @@ -20,8 +20,6 @@ interface RSVPFormStep1Props { export function RSVPFormStep1({ form, - eventName, - isEditing, walletFieldSlot, showWallet, showTurtleRoles, diff --git a/frontend/src/components/TableRow.tsx b/frontend/src/components/TableRow.tsx index 6ca220b8b..65bdb8970 100644 --- a/frontend/src/components/TableRow.tsx +++ b/frontend/src/components/TableRow.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Guest, BeverageRecommendation, PizzaRecommendation } from '../types'; -import { Trash2, Check, X, CheckCircle2, Loader2, ArrowUpCircle, Plus, Minus, Pencil, Star, UserRoundX } from 'lucide-react'; +import { Trash2, Check, X, CheckCircle2, Loader2, ArrowUpCircle, Plus, Minus, Star, UserRoundX } from 'lucide-react'; import { format } from 'date-fns'; import { getToppingEmoji } from '../utils/toppingEmojis'; import { ClickableEmail } from './ClickableEmail'; @@ -58,7 +58,6 @@ export const TableRow: React.FC = ({ guest, beverageRec, pizzaRec, - pizzaIndex = 0, variant = 'basic', requireApproval = false, onApprove, diff --git a/frontend/src/components/TimezonePickerInput.tsx b/frontend/src/components/TimezonePickerInput.tsx index 0bcd25272..511ba61c9 100644 --- a/frontend/src/components/TimezonePickerInput.tsx +++ b/frontend/src/components/TimezonePickerInput.tsx @@ -34,7 +34,7 @@ const ALL_TIMEZONES = (() => { 'Asia/Kolkata', 'Asia/Singapore', 'Asia/Hong_Kong', 'Asia/Shanghai', 'Asia/Tokyo', 'Australia/Sydney' ]; - } catch (e) { + } catch { // Fallback list if supportedValuesOf throws an error return [ 'America/Anchorage', 'America/Los_Angeles', 'America/Phoenix', 'America/Denver', diff --git a/frontend/src/components/budget/BudgetSettings.tsx b/frontend/src/components/budget/BudgetSettings.tsx index f176f398b..261917310 100644 --- a/frontend/src/components/budget/BudgetSettings.tsx +++ b/frontend/src/components/budget/BudgetSettings.tsx @@ -10,7 +10,6 @@ interface BudgetSettingsProps { } export const BudgetSettings: React.FC = ({ - budgetEnabled, budgetTotal, onUpdate, }) => { diff --git a/frontend/src/components/day-of/DayOfDashboard.tsx b/frontend/src/components/day-of/DayOfDashboard.tsx index 6da2c7a96..6a7334669 100644 --- a/frontend/src/components/day-of/DayOfDashboard.tsx +++ b/frontend/src/components/day-of/DayOfDashboard.tsx @@ -8,7 +8,6 @@ import { AnnouncePanel } from './AnnouncePanel'; import { AnnouncementHistory } from './AnnouncementHistory'; import { PizzaStatusCard } from './PizzaStatusCard'; import { MusicNowPlayingCard } from './MusicNowPlayingCard'; -import { ChecklistTodayCard } from './ChecklistTodayCard'; import { PhotoQuickCaptureCard } from './PhotoQuickCaptureCard'; import { BriefingCard } from './BriefingCard'; import { SignedPizzaBoxCard } from './SignedPizzaBoxCard'; diff --git a/frontend/src/components/generative/GenerativeCanvas.tsx b/frontend/src/components/generative/GenerativeCanvas.tsx index 5be7e7a28..4dc55bae7 100644 --- a/frontend/src/components/generative/GenerativeCanvas.tsx +++ b/frontend/src/components/generative/GenerativeCanvas.tsx @@ -420,19 +420,6 @@ export function GenerativeCanvas({ config }: GenerativeCanvasProps) { const scale = containerWidth / config.canvasWidth; const aspectRatio = config.canvasHeight / config.canvasWidth; - // Render to canvas (editor resolution) - const renderToCanvas = async (): Promise => { - return renderCanvas({ - config, - positions, - textValues: { city, dateDisplay, timeDisplay: effectiveTimeDisplay, venueName, streetAddress }, - sponsors: sponsors.map(s => ({ id: s.id, logoUrl: s.logoUrl! })), - sponsorBoxSize, - logoSizes, - poppedLogos, - }); - }; - // Render to canvas at full resolution const renderFullRes = async (): Promise => { const scaleFactor = config.fullResWidth / config.canvasWidth; diff --git a/frontend/src/components/generative/renderCanvas.ts b/frontend/src/components/generative/renderCanvas.ts index 2b1612021..87a92efcc 100644 --- a/frontend/src/components/generative/renderCanvas.ts +++ b/frontend/src/components/generative/renderCanvas.ts @@ -1,7 +1,7 @@ import type { FormatConfig, CanvasPositions } from './types'; import { fitText, loadImg, - CITY_COLOR, TIME_COLOR, VENUE_COLOR, + CITY_COLOR, TIME_COLOR, } from '../flyer/renderFlyer'; export interface RenderCanvasOptions { diff --git a/frontend/src/components/invoice/InvoiceCryptoPayment.tsx b/frontend/src/components/invoice/InvoiceCryptoPayment.tsx index 54646aa85..7bd59a25e 100644 --- a/frontend/src/components/invoice/InvoiceCryptoPayment.tsx +++ b/frontend/src/components/invoice/InvoiceCryptoPayment.tsx @@ -39,7 +39,7 @@ export const InvoiceCryptoPayment: React.FC = ({ invoice, onSuccess, }) => { - const { address, chainId, isConnected } = useAccount(); + const { chainId, isConnected } = useAccount(); const { switchChain } = useSwitchChain(); const { balances, isLoading: balancesLoading } = useTokenBalances(); const { status: txStatus, txHash, error: txError, sendDonation, reset: resetTx } = useCryptoDonation(); diff --git a/frontend/src/components/kit/PartyKitWidget.tsx b/frontend/src/components/kit/PartyKitWidget.tsx index 830f1dc0f..5a7e7ccea 100644 --- a/frontend/src/components/kit/PartyKitWidget.tsx +++ b/frontend/src/components/kit/PartyKitWidget.tsx @@ -65,7 +65,7 @@ export const PartyKitWidget: React.FC = ({ partyId }) => { if (success) { setKit(null); } - } catch (err) { + } catch { setError(t('kit.failedToCancel')); } finally { setCanceling(false); diff --git a/frontend/src/components/music/MusicWidget.tsx b/frontend/src/components/music/MusicWidget.tsx index f84293502..a3e794838 100644 --- a/frontend/src/components/music/MusicWidget.tsx +++ b/frontend/src/components/music/MusicWidget.tsx @@ -1,5 +1,5 @@ -import React, { useState, useEffect, useCallback, useContext, useRef } from 'react'; -import { Performer, Song, Playlist, MusicPlatform } from '../../types'; +import React, { useState, useEffect, useCallback, useContext } from 'react'; +import { Performer, Song, Playlist } from '../../types'; import { PizzaContext } from '../../contexts/PizzaContext'; import { getPerformers, @@ -37,7 +37,7 @@ export const MusicWidget: React.FC = ({ isHost = false, partyI // Performers state const [performers, setPerformers] = useState([]); const [musicEnabled, setMusicEnabled] = useState(false); - const [musicNotes, setMusicNotes] = useState(null); + const [, setMusicNotes] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -64,8 +64,7 @@ export const MusicWidget: React.FC = ({ isHost = false, partyI const [savingPlaylist, setSavingPlaylist] = useState(false); // File upload state for songs - const [isSongDragOver, setIsSongDragOver] = useState(false); - const songFileInputRef = useRef(null); + const [isSongDragOver] = useState(false); // Share button state const [copied, setCopied] = useState(false); diff --git a/frontend/src/components/payments-admin/PayoutsFilterBar.tsx b/frontend/src/components/payments-admin/PayoutsFilterBar.tsx index f23747394..dd5256c23 100644 --- a/frontend/src/components/payments-admin/PayoutsFilterBar.tsx +++ b/frontend/src/components/payments-admin/PayoutsFilterBar.tsx @@ -3,7 +3,7 @@ import { Search, X, SlidersHorizontal, ChevronDown, ChevronUp } from 'lucide-rea import { IconInput } from '../IconInput'; import { Checkbox } from '../Checkbox'; import { TriStateFilterDropdown } from '../TriStateFilterDropdown'; -import type { AdminPayoutFilters, PayoutMethod, PayoutStatus } from '../../types'; +import type { AdminPayoutFilters, PayoutMethod } from '../../types'; import { PAYOUT_METHOD_LABELS } from '../payments-shared'; import { PAYMENTS_REGION_DISPLAY_ORDER, diff --git a/frontend/src/components/payouts/RolePhotoPicker.tsx b/frontend/src/components/payouts/RolePhotoPicker.tsx index edc725e4d..002128006 100644 --- a/frontend/src/components/payouts/RolePhotoPicker.tsx +++ b/frontend/src/components/payouts/RolePhotoPicker.tsx @@ -36,7 +36,6 @@ const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif */ export const RolePhotoPicker: React.FC = ({ partyId, - role, roleLabel, eventStart, selectedPhotoId, diff --git a/frontend/src/components/photos/PhotoGallery.tsx b/frontend/src/components/photos/PhotoGallery.tsx index b2bece8bf..ec91ca65f 100644 --- a/frontend/src/components/photos/PhotoGallery.tsx +++ b/frontend/src/components/photos/PhotoGallery.tsx @@ -1,6 +1,6 @@ -import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { createPortal } from 'react-dom'; -import { Camera, Star, Loader2, Upload, Filter, Clock, CheckCircle2, XCircle, CheckCheck, Tag, Video } from 'lucide-react'; +import { Camera, Star, Loader2, Upload, Clock, CheckCheck, Tag, Video } from 'lucide-react'; import { Photo, PhotoStats } from '../../types'; import { getPartyPhotos, getPhotoStats, updatePhoto, deletePhoto, restorePhoto, batchReviewPhotos, getPhotoTags } from '../../lib/api'; import { PhotoCard } from './PhotoCard'; @@ -108,7 +108,7 @@ export const PhotoGallery: React.FC = ({ } else { setError('Failed to load photos'); } - } catch (err) { + } catch { setError('Failed to load photos'); } finally { setLoading(false); diff --git a/frontend/src/components/photos/PhotoUpload.tsx b/frontend/src/components/photos/PhotoUpload.tsx index a915d7f3f..79345b84b 100644 --- a/frontend/src/components/photos/PhotoUpload.tsx +++ b/frontend/src/components/photos/PhotoUpload.tsx @@ -1,5 +1,5 @@ import React, { useState, useCallback, useRef } from 'react'; -import { Upload, X, Loader2, Image as ImageIcon, Check, Tag, Play } from 'lucide-react'; +import { Upload, X, Loader2, Check, Tag, Play } from 'lucide-react'; import { uploadEventPhoto, uploadEventVideo } from '../../lib/supabase'; import { uploadPhoto as uploadPhotoApi, PhotoUploadData } from '../../lib/api'; import { Photo } from '../../types'; diff --git a/frontend/src/components/promo/BulkInvite.tsx b/frontend/src/components/promo/BulkInvite.tsx index f2be0e662..35b6aa3bc 100644 --- a/frontend/src/components/promo/BulkInvite.tsx +++ b/frontend/src/components/promo/BulkInvite.tsx @@ -17,7 +17,6 @@ import { IconInput } from '../IconInput'; import { Checkbox } from '../Checkbox'; import { Party } from '../../types'; import { usePizza } from '../../contexts/PizzaContext'; -import { useAuth } from '../../contexts/AuthContext'; import { parseCsv, ParsedCsvRow } from '../../lib/csvParser'; import { bulkInviteGuests, BulkInviteResult } from '../../lib/api'; @@ -65,7 +64,6 @@ function statusBadgeClass(status: RowStatus): string { export const BulkInvite: React.FC = ({ party }) => { const { t } = useTranslation('host'); const { guests, loadParty } = usePizza(); - const { user } = useAuth(); const [stage, setStage] = useState('upload'); const [testSending, setTestSending] = useState(false); diff --git a/frontend/src/components/promo/PlatformPublisher.tsx b/frontend/src/components/promo/PlatformPublisher.tsx index fbfcd3bf6..615f2d3ff 100644 --- a/frontend/src/components/promo/PlatformPublisher.tsx +++ b/frontend/src/components/promo/PlatformPublisher.tsx @@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Copy, ExternalLink, Check, MessageSquare, Link, Tag, X, Plus, Loader2 } from 'lucide-react'; import { IconInput } from '../IconInput'; -import { Party } from '../../types'; import { usePizza } from '../../contexts/PizzaContext'; import { updateParty } from '../../lib/supabase'; import { diff --git a/frontend/src/components/raffle/RaffleEntry.tsx b/frontend/src/components/raffle/RaffleEntry.tsx index 8467be1a9..20232a167 100644 --- a/frontend/src/components/raffle/RaffleEntry.tsx +++ b/frontend/src/components/raffle/RaffleEntry.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Gift, Trophy, Loader2, CheckCircle, Users } from 'lucide-react'; -import { Raffle, RaffleStatus } from '../../types'; +import { Raffle } from '../../types'; import { getRaffles, enterRaffle } from '../../lib/api'; interface RaffleEntryProps { @@ -10,7 +10,7 @@ interface RaffleEntryProps { guestName?: string; } -export function RaffleEntry({ partyId, guestId, guestName }: RaffleEntryProps) { +export function RaffleEntry({ partyId, guestId }: RaffleEntryProps) { const { t } = useTranslation('host'); const [raffles, setRaffles] = useState([]); const [loading, setLoading] = useState(true); diff --git a/frontend/src/components/raffle/RaffleForm.tsx b/frontend/src/components/raffle/RaffleForm.tsx index 9a43f78bc..39be12c69 100644 --- a/frontend/src/components/raffle/RaffleForm.tsx +++ b/frontend/src/components/raffle/RaffleForm.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { X, Loader2, Type, AlignLeft, Users } from 'lucide-react'; +import { X, Loader2, Type, AlignLeft } from 'lucide-react'; import { IconInput } from '../IconInput'; import { Raffle } from '../../types'; diff --git a/frontend/src/components/raffle/RaffleWidget.tsx b/frontend/src/components/raffle/RaffleWidget.tsx index ff486ee15..891f1af37 100644 --- a/frontend/src/components/raffle/RaffleWidget.tsx +++ b/frontend/src/components/raffle/RaffleWidget.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { Plus, Gift, Loader2, Trash2, Edit2, Play, Trophy, CheckCircle, X } from 'lucide-react'; +import { Plus, Gift, Loader2 } from 'lucide-react'; import { Raffle, RafflePrize } from '../../types'; import { getRaffles, diff --git a/frontend/src/components/shipping/CoordinatorManager.tsx b/frontend/src/components/shipping/CoordinatorManager.tsx index 31efbc33b..9caa68724 100644 --- a/frontend/src/components/shipping/CoordinatorManager.tsx +++ b/frontend/src/components/shipping/CoordinatorManager.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState, useCallback } from 'react'; -import { UserPlus, Shield, Trash2, Pencil, ToggleLeft, ToggleRight } from 'lucide-react'; +import { UserPlus, Shield, Pencil, ToggleLeft, ToggleRight } from 'lucide-react'; import { CoordinatorModal } from './CoordinatorModal'; import { GPP_REGIONS } from '../../types'; import type { ShippingCoordinator } from '../../types'; @@ -52,10 +52,6 @@ export function CoordinatorManager() { loadCoordinators(); }; - const regionLabels = (regions: string[]) => - regions - .map((r) => GPP_REGIONS.find((g) => g.id === r)?.label || r) - .join(', '); return (
diff --git a/frontend/src/components/shipping/CsvImportModal.tsx b/frontend/src/components/shipping/CsvImportModal.tsx index 699f0881d..c52cfcd0b 100644 --- a/frontend/src/components/shipping/CsvImportModal.tsx +++ b/frontend/src/components/shipping/CsvImportModal.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef } from 'react'; import { createPortal } from 'react-dom'; -import { X, Upload, FileText, CheckCircle, AlertCircle } from 'lucide-react'; +import { X, Upload, CheckCircle, AlertCircle } from 'lucide-react'; import { splitCsvLine } from '../../lib/csvParser'; import { detectCarrier, detectTrackingUrl } from '../../lib/trackingUtils'; import { importShippingTracking } from '../../lib/api'; diff --git a/frontend/src/components/shipping/KitDetailModal.tsx b/frontend/src/components/shipping/KitDetailModal.tsx index 4b87ebdfd..562201148 100644 --- a/frontend/src/components/shipping/KitDetailModal.tsx +++ b/frontend/src/components/shipping/KitDetailModal.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { createPortal } from 'react-dom'; -import { X, Package, MapPin, Truck, User, Calendar, FileText, ExternalLink, Gift, Star, Check } from 'lucide-react'; +import { X, Package, MapPin, Truck, FileText, ExternalLink, Gift, Star, Check } from 'lucide-react'; import { IconInput } from '../IconInput'; import type { ShippingKit, KitStatus, KitTier } from '../../types'; import { GPP_REGIONS, KIT_TIERS } from '../../types'; diff --git a/frontend/src/components/shipping/KitTable.tsx b/frontend/src/components/shipping/KitTable.tsx index d842cdd71..6c07517f0 100644 --- a/frontend/src/components/shipping/KitTable.tsx +++ b/frontend/src/components/shipping/KitTable.tsx @@ -1,7 +1,7 @@ import React, { useState, useMemo } from 'react'; import { ArrowUpDown } from 'lucide-react'; import { KitRow } from './KitRow'; -import type { ShippingKit, KitStatus, KitTier } from '../../types'; +import type { ShippingKit } from '../../types'; interface KitTableProps { kits: ShippingKit[]; diff --git a/frontend/src/components/sponsors/InvoiceButton.tsx b/frontend/src/components/sponsors/InvoiceButton.tsx index 3e08b1fcb..a06dec577 100644 --- a/frontend/src/components/sponsors/InvoiceButton.tsx +++ b/frontend/src/components/sponsors/InvoiceButton.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { FileText, Check, Clock, ExternalLink, Copy, Send, DollarSign, - Loader2, X, MoreHorizontal + Loader2, X } from 'lucide-react'; import { Sponsor, Invoice } from '../../types'; import { markInvoicePaid, sendInvoice } from '../../lib/api'; diff --git a/frontend/src/components/sponsors/MouForm.tsx b/frontend/src/components/sponsors/MouForm.tsx index 0d06100f1..08ca46f90 100644 --- a/frontend/src/components/sponsors/MouForm.tsx +++ b/frontend/src/components/sponsors/MouForm.tsx @@ -31,7 +31,7 @@ const DEFAULT_MOU_BODY = `This Memorandum of Understanding ("MOU") outlines the This MOU is a statement of intent and good-faith collaboration between the parties.`; -export function MouForm({ sponsor, partyId, existingMou, onClose, onSave, onSponsorUpdate }: MouFormProps) { +export function MouForm({ sponsor, partyId, existingMou, onClose, onSave }: MouFormProps) { const modalRef = useRef(null); const [isSaving, setIsSaving] = useState(false); const [isSending, setIsSending] = useState(false); diff --git a/frontend/src/components/sponsors/SponsorCRM.tsx b/frontend/src/components/sponsors/SponsorCRM.tsx index b289e4e25..d72000056 100644 --- a/frontend/src/components/sponsors/SponsorCRM.tsx +++ b/frontend/src/components/sponsors/SponsorCRM.tsx @@ -451,7 +451,7 @@ export function SponsorCRM({ partyId, onAddAsCoHost }: SponsorCRMProps) { {isPrivileged && ( { + onInvoiceUpdate={() => { // Reload invoices to get fresh paid status getInvoices(partyId).then((result) => { if (result) setInvoices(result.invoices); diff --git a/frontend/src/components/sponsors/SponsorList.tsx b/frontend/src/components/sponsors/SponsorList.tsx index 17c3cf94e..debc3db8a 100644 --- a/frontend/src/components/sponsors/SponsorList.tsx +++ b/frontend/src/components/sponsors/SponsorList.tsx @@ -52,7 +52,7 @@ const STATUS_ORDER: Record = { skip: 7, }; -export function SponsorList({ sponsors, partyId, invoices = [], mous = [], onEdit, onDelete, onSponsorUpdate, onInvoiceUpdate, onMouUpdate, onMouDelete, onStatusChange, isLoading, avatarUrls, isPrivileged = false }: SponsorListProps) { +export function SponsorList({ sponsors, partyId, invoices = [], mous = [], onEdit, onDelete, onSponsorUpdate, onInvoiceUpdate, onMouUpdate, onMouDelete, onStatusChange, avatarUrls, isPrivileged = false }: SponsorListProps) { const { t } = useTranslation('host'); const [sortField, setSortField] = useState('createdAt'); const [sortOrder, setSortOrder] = useState('desc'); diff --git a/frontend/src/components/sponsors/SponsorPipeline.tsx b/frontend/src/components/sponsors/SponsorPipeline.tsx index d7dda5341..6fba29ffb 100644 --- a/frontend/src/components/sponsors/SponsorPipeline.tsx +++ b/frontend/src/components/sponsors/SponsorPipeline.tsx @@ -25,7 +25,7 @@ const PIPELINE_STATUSES: SponsorStatus[] = ['todo', 'asked', 'yes', 'billed', 'p // Secondary statuses const SECONDARY_STATUSES: SponsorStatus[] = ['stuck', 'skip', 'alum']; -export function SponsorPipeline({ stats, onUpdateGoal, isLoading }: SponsorPipelineProps) { +export function SponsorPipeline({ stats, onUpdateGoal }: SponsorPipelineProps) { const { t } = useTranslation('host'); const [isEditingGoal, setIsEditingGoal] = useState(false); const [goalInput, setGoalInput] = useState(''); diff --git a/frontend/src/components/staffing/StaffingWidget.tsx b/frontend/src/components/staffing/StaffingWidget.tsx index a43702945..04acd2b50 100644 --- a/frontend/src/components/staffing/StaffingWidget.tsx +++ b/frontend/src/components/staffing/StaffingWidget.tsx @@ -46,7 +46,7 @@ export const StaffingWidget: React.FC = ({ partyId }) => { } else { setError('Failed to load staff'); } - } catch (err) { + } catch { setError('Failed to load staff'); } finally { setLoading(false); diff --git a/frontend/src/components/underboss/OutreachTab.tsx b/frontend/src/components/underboss/OutreachTab.tsx index 20cd81c55..218f5e5f5 100644 --- a/frontend/src/components/underboss/OutreachTab.tsx +++ b/frontend/src/components/underboss/OutreachTab.tsx @@ -204,7 +204,7 @@ export function OutreachTab(_props: OutreachTabProps) { ); try { await updateOutreachAttempt(attemptId, { status }); - } catch (e) { + } catch { // Reload on failure to revert loadCommunities(); } diff --git a/frontend/src/components/underboss/PartnerCitiesFlyer.tsx b/frontend/src/components/underboss/PartnerCitiesFlyer.tsx index 5c13e4a5a..3c64fdf17 100644 --- a/frontend/src/components/underboss/PartnerCitiesFlyer.tsx +++ b/frontend/src/components/underboss/PartnerCitiesFlyer.tsx @@ -132,7 +132,6 @@ function flagForCountry(country: string | null | undefined): string { const DEFAULT_LOGO_POS = { x: 340, y: 36 }; const DEFAULT_LOGO_SIZE = 50; const CITY_BOX = { x: 55, y: 597, width: 500, height: 490 }; -const MAX_VISIBLE = 10; const CITY_FONT_SIZE = 42; const CITY_LINE_SPACING = 1.25; const SUBHEAD_TEXT = 'SUPPORTING EVENTS IN'; diff --git a/frontend/src/components/underboss/SuperlativesTab.tsx b/frontend/src/components/underboss/SuperlativesTab.tsx index ea98730c8..39fff4b7c 100644 --- a/frontend/src/components/underboss/SuperlativesTab.tsx +++ b/frontend/src/components/underboss/SuperlativesTab.tsx @@ -63,7 +63,7 @@ export function SuperlativesTab() { setStatusOverride((s) => ({ ...s, [id]: status })); try { await markSuperlative(id, status); - } catch (err) { + } catch { // Roll back the optimistic override on failure. setStatusOverride((s) => { const n = { ...s }; diff --git a/frontend/src/components/underboss/TelegramBroadcast.tsx b/frontend/src/components/underboss/TelegramBroadcast.tsx index d6dd72a66..56491fc2a 100644 --- a/frontend/src/components/underboss/TelegramBroadcast.tsx +++ b/frontend/src/components/underboss/TelegramBroadcast.tsx @@ -545,7 +545,7 @@ export function TelegramBroadcast({ onClose, preSelectedCities, events, allHosts allResults.push(...r.results.map(res => ({ ...res, kind: 'group' as const }))); totalSent += r.sent; totalFailed += r.failed; - } catch (err: any) { + } catch { totalFailed += selectedGroups.length; } } @@ -562,7 +562,7 @@ export function TelegramBroadcast({ onClose, preSelectedCities, events, allHosts blockedHostCount = r.results.filter( x => x.error === 'Host blocked the bot — disconnected' ).length; - } catch (err: any) { + } catch { totalFailed += selectedHostsPayload.length; } } diff --git a/frontend/src/components/venue-report/VenueReportPreview.tsx b/frontend/src/components/venue-report/VenueReportPreview.tsx index 0c7e7b88f..fb605a487 100644 --- a/frontend/src/components/venue-report/VenueReportPreview.tsx +++ b/frontend/src/components/venue-report/VenueReportPreview.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { MapPin, Users, DollarSign, Globe, ThumbsUp, ThumbsDown, Check, ChevronLeft, ChevronRight, X, Camera } from 'lucide-react'; +import { MapPin, Users, DollarSign, Globe, ThumbsUp, ThumbsDown, Check, ChevronLeft, ChevronRight, X } from 'lucide-react'; import { VenueReport, Venue, VenuePhoto } from '../../types'; interface VenueReportPreviewProps { diff --git a/frontend/src/components/venue/VenueForm.test.tsx b/frontend/src/components/venue/VenueForm.test.tsx index 4f6a6ea90..2a4418206 100644 --- a/frontend/src/components/venue/VenueForm.test.tsx +++ b/frontend/src/components/venue/VenueForm.test.tsx @@ -14,26 +14,6 @@ const mockPlaceDetails = { place_id: 'ChIJmwpQ1HBZwokR9w', }; -// Mock predictions from autocomplete -const mockPredictions = [ - { - description: "Joe's Pizza, Carmine St, New York, NY, USA", - place_id: 'ChIJmwpQ1HBZwokR9w', - structured_formatting: { - main_text: "Joe's Pizza", - secondary_text: 'Carmine St, New York, NY, USA', - }, - }, - { - description: "Joe's Pizza, Broadway, New York, NY, USA", - place_id: 'ChIJd8BlQ2BZwokRjM', - structured_formatting: { - main_text: "Joe's Pizza", - secondary_text: 'Broadway, New York, NY, USA', - }, - }, -]; - // Will be assigned by our mock Autocomplete constructor let placesChangedCallback: (() => void) | null = null; let mockGetPlace: ReturnType; diff --git a/frontend/src/components/venue/VenuePhotoUpload.tsx b/frontend/src/components/venue/VenuePhotoUpload.tsx index fc3ac7ffd..3302afb5a 100644 --- a/frontend/src/components/venue/VenuePhotoUpload.tsx +++ b/frontend/src/components/venue/VenuePhotoUpload.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { Upload, X, Loader2, ImagePlus } from 'lucide-react'; +import { Loader2, ImagePlus } from 'lucide-react'; import { VenuePhotoCategory } from '../../types'; import { uploadVenuePhoto } from '../../lib/supabase'; import { createVenuePhoto } from '../../lib/api'; diff --git a/frontend/src/hooks/useRSVPForm.ts b/frontend/src/hooks/useRSVPForm.ts index 3f128cc5f..eb7a55a48 100644 --- a/frontend/src/hooks/useRSVPForm.ts +++ b/frontend/src/hooks/useRSVPForm.ts @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react'; -import { addGuestToParty, getUserPreferences, saveUserPreferences, ExistingGuestData, getExperimentFlag } from '../lib/supabase'; +import { addGuestToParty, getUserPreferences, saveUserPreferences, ExistingGuestData } from '../lib/supabase'; import { getExcludedToppingIds } from '../constants/options'; import { searchPizzerias, geocodeAddress } from '../lib/ordering'; import { Pizzeria } from '../types'; diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 621bb99e8..593c2faa0 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -863,7 +863,7 @@ export async function getLeaderboardRank( `/api/parties/${partyId}/leaderboard-rank?metric=${encodeURIComponent(metric)}`, { method: 'GET', requireAuth: true }, ); - } catch (error) { + } catch { // Graceful hide — leaderboard is decorative, not load-bearing. return null; } diff --git a/frontend/src/lib/ordering.ts b/frontend/src/lib/ordering.ts index f19b5f15c..15aa96403 100644 --- a/frontend/src/lib/ordering.ts +++ b/frontend/src/lib/ordering.ts @@ -1,4 +1,3 @@ -import { supabase } from './supabase'; import { Pizzeria, OrderItem, OrderingProvider } from '../types'; const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL; diff --git a/frontend/src/pages/AccountPage.tsx b/frontend/src/pages/AccountPage.tsx index bc211028f..10cfaed3b 100644 --- a/frontend/src/pages/AccountPage.tsx +++ b/frontend/src/pages/AccountPage.tsx @@ -65,7 +65,7 @@ export function AccountPage() { // Helper to compare arrays const arraysEqual = (a: string[], b: string[]) => - a.length === b.length && a.every((v, i) => b.includes(v)) && b.every((v) => a.includes(v)); + a.length === b.length && a.every((v) => b.includes(v)) && b.every((v) => a.includes(v)); // Check if preferences have changed const preferencesChanged = originalPreferences diff --git a/frontend/src/pages/AuthVerifyPage.tsx b/frontend/src/pages/AuthVerifyPage.tsx index d4174ca8d..e84a36d6d 100644 --- a/frontend/src/pages/AuthVerifyPage.tsx +++ b/frontend/src/pages/AuthVerifyPage.tsx @@ -15,7 +15,7 @@ export function AuthVerifyPage() { const [status, setStatus] = useState<'idle' | 'verifying' | 'name_prompt' | 'saving_name' | 'success' | 'error'>('idle'); const [error, setError] = useState(null); const [code, setCode] = useState(['', '', '', '', '', '']); - const [lastSubmittedCode, setLastSubmittedCode] = useState(null); + const [, setLastSubmittedCode] = useState(null); const [isNewUser, setIsNewUser] = useState(false); const [name, setName] = useState(''); const [pendingAuthData, setPendingAuthData] = useState(null); diff --git a/frontend/src/pages/CheckInPage.tsx b/frontend/src/pages/CheckInPage.tsx index ee4fbf618..508d8ba39 100644 --- a/frontend/src/pages/CheckInPage.tsx +++ b/frontend/src/pages/CheckInPage.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useCallback, useRef } from 'react'; +import React, { useEffect, useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Helmet } from 'react-helmet-async'; import { useTranslation } from 'react-i18next'; @@ -6,8 +6,6 @@ import { Layout } from '../components/Layout'; import { Loader2, CheckCircle2, XCircle, AlertCircle, QrCode } from 'lucide-react'; import { useAuth } from '../contexts/AuthContext'; import { vouchForGuest, checkInGuest, getDiscountStatus, claimDiscount, type Attestation } from '../lib/api'; -import { CheckInQRDisplay } from '../components/CheckInQRDisplay'; -import { GPPClouds } from '../components/GPPClouds'; // provolone-39042: friendly display name for an attestation row. const attestationDisplay = (a: Attestation): string => a.name || a.email || 'someone'; @@ -214,7 +212,7 @@ export function CheckInPage() { const result = await claimDiscount(inviteCode!, guestId!); setDiscountData(result); setState('discount-claimed'); - } catch (err) { + } catch { setState('error'); setErrorMessage(t('postCheckIn.failedToClaim')); } diff --git a/frontend/src/pages/DJPage.tsx b/frontend/src/pages/DJPage.tsx index 860adf869..be7415240 100644 --- a/frontend/src/pages/DJPage.tsx +++ b/frontend/src/pages/DJPage.tsx @@ -101,12 +101,12 @@ export function DJPage() { try { const storedSongs = localStorage.getItem(`music_songs_${foundEvent.id}`); if (storedSongs) setSongs(JSON.parse(storedSongs)); - } catch (e) { /* ignore parse errors */ } + } catch { /* ignore parse errors */ } try { const storedPlaylists = localStorage.getItem(`music_playlists_${foundEvent.id}`); if (storedPlaylists) setPlaylists(JSON.parse(storedPlaylists)); - } catch (e) { /* ignore parse errors */ } + } catch { /* ignore parse errors */ } } catch (err) { console.error('Error loading DJ page data:', err); diff --git a/frontend/src/pages/DayOfRunPage.tsx b/frontend/src/pages/DayOfRunPage.tsx index 613e73a71..59611f57b 100644 --- a/frontend/src/pages/DayOfRunPage.tsx +++ b/frontend/src/pages/DayOfRunPage.tsx @@ -23,7 +23,7 @@ function DayOfRunPageContent() { useEffect(() => { if (!inviteCode || loaded) return; - loadParty(inviteCode).then((ok) => setLoaded(true)); + loadParty(inviteCode).then(() => setLoaded(true)); }, [inviteCode, loadParty, loaded]); // salami-39204: gate Day-Of on party approval status instead of the prior diff --git a/frontend/src/pages/DisplayPage.tsx b/frontend/src/pages/DisplayPage.tsx index 146f1de21..147f2fa0d 100644 --- a/frontend/src/pages/DisplayPage.tsx +++ b/frontend/src/pages/DisplayPage.tsx @@ -28,7 +28,7 @@ export function DisplayPage() { } else { setError(t('display.notFound')); } - } catch (err) { + } catch { setError(t('display.failedToLoad')); } finally { setLoading(false); diff --git a/frontend/src/pages/EventsMapPage.tsx b/frontend/src/pages/EventsMapPage.tsx index 5a7f6aaa2..75648e698 100644 --- a/frontend/src/pages/EventsMapPage.tsx +++ b/frontend/src/pages/EventsMapPage.tsx @@ -10,7 +10,6 @@ import { LoginModal } from '../components/LoginModal'; const GPPEventsMap = lazy(() => import('../components/GPPEventsMap')); const STATUS_FILTER_KEYS = ['approved', 'pending', 'listed', 'rejected'] as const; -type StatusFilterKey = (typeof STATUS_FILTER_KEYS)[number]; // Semantic colors used by the marker icons + legend, keyed on underbossStatus. // Keep in sync with STATUS_COLORS in GPPEventsMap.tsx. diff --git a/frontend/src/pages/GPPLandingPage.tsx b/frontend/src/pages/GPPLandingPage.tsx index b507cc712..240ef531a 100644 --- a/frontend/src/pages/GPPLandingPage.tsx +++ b/frontend/src/pages/GPPLandingPage.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect, useCallback, lazy, Suspense } from 'react'; +import React, { useState, useRef, useEffect, lazy, Suspense } from 'react'; import { useNavigate, Link } from 'react-router-dom'; import { Helmet } from 'react-helmet-async'; import { CheckCircle, Loader2, ArrowRight, MapPin, Globe, ChevronDown } from 'lucide-react'; diff --git a/frontend/src/pages/HostPage.tsx b/frontend/src/pages/HostPage.tsx index 7ed51cb04..84c6dd41f 100644 --- a/frontend/src/pages/HostPage.tsx +++ b/frontend/src/pages/HostPage.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState, useMemo, useRef } from 'react'; -import { useParams, useNavigate, Link } from 'react-router-dom'; +import { useParams, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Loader2, AlertCircle, Settings, Pizza, Users, Camera, LayoutGrid, Home, Zap, MessageSquare } from 'lucide-react'; import { PizzaProvider, usePizza } from '../contexts/PizzaContext'; diff --git a/frontend/src/pages/InvoicePage.tsx b/frontend/src/pages/InvoicePage.tsx index b89aa42f6..5cebb25d4 100644 --- a/frontend/src/pages/InvoicePage.tsx +++ b/frontend/src/pages/InvoicePage.tsx @@ -27,7 +27,7 @@ export function InvoicePage() { } else { setError('Invoice not found'); } - } catch (err) { + } catch { setError('Failed to load invoice'); } finally { setLoading(false); diff --git a/frontend/src/pages/MouPage.tsx b/frontend/src/pages/MouPage.tsx index de858edb6..70f74ddd3 100644 --- a/frontend/src/pages/MouPage.tsx +++ b/frontend/src/pages/MouPage.tsx @@ -32,7 +32,7 @@ export function MouPage() { } else { setError('MOU not found'); } - } catch (err) { + } catch { setError('Failed to load MOU'); } finally { setLoading(false); diff --git a/frontend/src/pages/PartnerDashboardPage.tsx b/frontend/src/pages/PartnerDashboardPage.tsx index c8231db82..d9759885c 100644 --- a/frontend/src/pages/PartnerDashboardPage.tsx +++ b/frontend/src/pages/PartnerDashboardPage.tsx @@ -20,7 +20,6 @@ import { PlatformIcon, detectPlatform } from '../components/report/platformIcon' import { cdnUrl } from '../lib/supabase'; import { getGppPhotosForCity, getGppPhotoCounts } from '../lib/gppPhotos'; import { fetchSheetCities } from '../lib/cities'; -import type { SheetCity } from '../lib/cities'; import type { SponsorDashboardEvent, SponsorMeResponse, SponsorDashboardData, CoHost } from '../types'; import { GPP_REGIONS } from '../types'; import { PartnerTimeSeriesChart } from '../components/partner/PartnerTimeSeriesChart'; @@ -264,7 +263,7 @@ export function PartnerDashboardPage() { // For admins, extract unique tags from events to build a tag picker if (me.isAdmin && !selectedTag) { const tags = new Set(); - data.events.forEach(e => { + data.events.forEach(() => { // eventTags are on the event but not in the response — use the dashboard tag if (data.tag) tags.add(data.tag); }); @@ -989,7 +988,7 @@ interface EventCardProps { isAdmin?: boolean; } -function EventCard({ event, onToggleChecklist, cityChats, isAdmin = false }: EventCardProps) { +function EventCard({ event, cityChats, isAdmin = false }: EventCardProps) { const { t } = useTranslation('partner'); // Filter co-hosts to show only visible ones const visibleCoHosts = event.coHosts.filter((h: CoHost) => h.showOnEvent !== false); diff --git a/frontend/src/pages/PhotosFeedPage.tsx b/frontend/src/pages/PhotosFeedPage.tsx index 60eb8a28e..5c581290a 100644 --- a/frontend/src/pages/PhotosFeedPage.tsx +++ b/frontend/src/pages/PhotosFeedPage.tsx @@ -62,7 +62,7 @@ export function PhotosFeedPage() { // Feed const [photos, setPhotos] = useState([]); - const [hasMore, setHasMore] = useState(true); + const [, setHasMore] = useState(true); const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [error, setError] = useState(null); @@ -335,7 +335,7 @@ export function PhotosFeedPage() { a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); - } catch (e) { + } catch { setDownloadError('Download failed. Please try again.'); } finally { setDownloading(false); diff --git a/frontend/src/pages/RSVPPage.tsx b/frontend/src/pages/RSVPPage.tsx index 13e73d0f6..dde4197dc 100644 --- a/frontend/src/pages/RSVPPage.tsx +++ b/frontend/src/pages/RSVPPage.tsx @@ -194,7 +194,7 @@ export function RSVPPage() { }, user, isOpen: !!party, // Only active when party is loaded - onSuccess: (result) => { + onSuccess: () => { if (isGPP) fireFromCenter(); }, }); diff --git a/frontend/src/pages/ShippingDashboard.tsx b/frontend/src/pages/ShippingDashboard.tsx index 542315f56..eb6797e3c 100644 --- a/frontend/src/pages/ShippingDashboard.tsx +++ b/frontend/src/pages/ShippingDashboard.tsx @@ -197,7 +197,7 @@ export function ShippingDashboard() { // Refresh stats const statsResult = await fetchShippingStats(); setStats(statsResult.stats); - } catch (err) { + } catch { // Revert on failure loadData(); } @@ -210,7 +210,7 @@ export function ShippingDashboard() { ); try { await updateShippingKit(kitId, { allocatedTier: tier }); - } catch (err) { + } catch { loadData(); } }; @@ -236,7 +236,7 @@ export function ShippingDashboard() { ); const statsResult = await fetchShippingStats(); setStats(statsResult.stats); - } catch (err) { + } catch { loadData(); } }; diff --git a/frontend/src/utils/beverageAlgorithm.test.ts b/frontend/src/utils/beverageAlgorithm.test.ts index f1bb60dbe..de0720e29 100644 --- a/frontend/src/utils/beverageAlgorithm.test.ts +++ b/frontend/src/utils/beverageAlgorithm.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; import { generateBeverageRecommendations } from './beverageAlgorithm'; -import { Guest, Beverage } from '../types'; +import { Guest } from '../types'; import { DRINK_CATEGORIES } from '../constants/options'; function makeGuest(overrides: Partial = {}): Guest { @@ -17,9 +17,6 @@ function makeGuest(overrides: Partial = {}): Guest { } const ALL_BEVERAGE_IDS = DRINK_CATEGORIES.map(b => b.id); -const WATER_BEVERAGE = DRINK_CATEGORIES.find(b => b.id === 'water')!; -const SODA_BEVERAGE = DRINK_CATEGORIES.find(b => b.id === 'soda')!; -const BEER_BEVERAGE = DRINK_CATEGORIES.find(b => b.id === 'beer')!; describe('generateBeverageRecommendations', () => { it('returns empty array when no beverages are selected by host', () => { diff --git a/frontend/src/utils/dateUtils.test.ts b/frontend/src/utils/dateUtils.test.ts index 0850751fc..77694c4c8 100644 --- a/frontend/src/utils/dateUtils.test.ts +++ b/frontend/src/utils/dateUtils.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { formatShortDate, formatFullDate, diff --git a/frontend/src/utils/dateUtils.ts b/frontend/src/utils/dateUtils.ts index 9fd63dd71..55e25adce 100644 --- a/frontend/src/utils/dateUtils.ts +++ b/frontend/src/utils/dateUtils.ts @@ -156,12 +156,6 @@ export function getDateTimeInTimezone(date: Date | string, timezone: string): { * @returns Date object in UTC */ export function parseDateTimeInTimezone(dateStr: string, timeStr: string, timezone: string): Date { - // Create a date string that we can parse - const dateTimeStr = `${dateStr}T${timeStr}:00`; - - // Create a date in the local timezone first - const localDate = new Date(dateTimeStr); - // Get what this date/time would be in the target timezone const targetFormatter = new Intl.DateTimeFormat('en-US', { timeZone: timezone, @@ -174,9 +168,6 @@ export function parseDateTimeInTimezone(dateStr: string, timeStr: string, timezo hour12: false, }); - // Get what the local date looks like in the target timezone - const localInTarget = targetFormatter.format(localDate); - // Get what the local date looks like in UTC const utcFormatter = new Intl.DateTimeFormat('en-US', { timeZone: 'UTC', @@ -188,7 +179,6 @@ export function parseDateTimeInTimezone(dateStr: string, timeStr: string, timezo second: '2-digit', hour12: false, }); - const localInUTC = utcFormatter.format(localDate); // Calculate the offset between local and target timezones by comparing the formatted strings // This is a workaround since JavaScript doesn't have native timezone-aware parsing @@ -199,10 +189,6 @@ export function parseDateTimeInTimezone(dateStr: string, timeStr: string, timezo return Date.UTC(+year, +month - 1, +day, +hour, +minute, +second); }; - const targetMs = parseFormatted(localInTarget); - const utcMs = parseFormatted(localInUTC); - const localOffset = targetMs - utcMs; - // Now we need to figure out the offset of the target timezone from UTC // Create a reference date at the target time const refDate = new Date(`${dateStr}T12:00:00Z`); // noon UTC on the target date @@ -216,7 +202,6 @@ export function parseDateTimeInTimezone(dateStr: string, timeStr: string, timezo // The time the user entered is in the target timezone // We need to convert it to UTC // If target is UTC-5 (EST), then 6pm EST = 11pm UTC, so we ADD the offset - const [hours, minutes] = timeStr.split(':').map(Number); const targetDate = new Date(`${dateStr}T${timeStr}:00Z`); // Subtract the target timezone's offset from UTC to get the actual UTC time diff --git a/frontend/src/utils/pizzaAlgorithm.test.ts b/frontend/src/utils/pizzaAlgorithm.test.ts index c72b0c6b8..73e6e3671 100644 --- a/frontend/src/utils/pizzaAlgorithm.test.ts +++ b/frontend/src/utils/pizzaAlgorithm.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; import { generatePizzaRecommendations } from './pizzaAlgorithm'; -import { Guest, PizzaStyle, PizzaSize, PizzaRecommendation } from '../types'; -import { PIZZA_STYLES, PIZZA_SIZES } from '../constants/options'; +import { Guest, PizzaRecommendation } from '../types'; +import { PIZZA_STYLES } from '../constants/options'; // Helpers const NY_STYLE = PIZZA_STYLES.find(s => s.id === 'new-york')!; @@ -172,7 +172,6 @@ describe('generatePizzaRecommendations', () => { // Should have grouped identical pizzas const customPizzas = result.filter(p => !p.isForNonRespondents); - const hasGrouped = customPizzas.some(p => (p.quantity || 1) > 1); // With 10 guests on NY style, they should be split into groups and potentially merged expect(customPizzas.length).toBeGreaterThan(0); }); diff --git a/frontend/src/utils/pizzaAlgorithm.ts b/frontend/src/utils/pizzaAlgorithm.ts index e397da59f..240fa483e 100644 --- a/frontend/src/utils/pizzaAlgorithm.ts +++ b/frontend/src/utils/pizzaAlgorithm.ts @@ -7,15 +7,6 @@ import { DIETARY_TOPPING_EXCLUSIONS } from '../constants/options'; // - Detroit: 2 slices per person (similar to NY-style serving) // - NY/default: based on surface area (18" feeds 4) -function getServingsForStyle(size: PizzaSize, style: PizzaStyle): number { - if (style.id === 'neapolitan') { - // Neapolitan pizzas are personal-sized, 1 pizza per 1.5 people regardless of size - return 1.5; - } - // Detroit and NY use surface-area based servings - return size.servings; -} - function getMaxGuestsPerPizza(style: PizzaStyle): number { if (style.id === 'neapolitan') { // Neapolitan is personal-sized, max ~2 people sharing one @@ -323,7 +314,7 @@ function splitIntoTwoGroups(guests: Guest[]): [Guest[], Guest[]] { } // Determine if half-and-half would improve satisfaction for a group -function shouldUseHalfAndHalf(guests: Guest[], style: PizzaStyle): boolean { +function shouldUseHalfAndHalf(guests: Guest[], _style: PizzaStyle): boolean { // Don't use half-and-half for very small groups or non-respondent pizzas if (guests.length < 2) return false; From bb842aaf6ced60a6222b90b11614e5b97fd0285a Mon Sep 17 00:00:00 2001 From: snackman Date: Thu, 11 Jun 2026 14:37:05 -0400 Subject: [PATCH 2/3] style(cavatelli P0): eslint --fix safe auto-fixables (prefer-const x2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 0 of the typing-debt cleanup (plans/cavatelli-typing-debt.md). Ran `npm run lint -- --fix --report-unused-disable-directives-severity off`. On current master the only safe auto-fixables are 2 prefer-const (let->const, neither reassigned). NOTE: a plain `--fix` ALSO strips eslint-disable directives (ESLint 9 flat config defaults reportUnusedDisableDirectives to "warn"), including deliberate react-hooks/exhaustive-deps suppressions in payments files. That's deliberately EXCLUDED here — stale-directive cleanup is a judgment call handled in a later phase, not a blanket sweep. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/components/ShareRSVP.tsx | 2 +- frontend/src/components/sponsors/SponsorCRM.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/ShareRSVP.tsx b/frontend/src/components/ShareRSVP.tsx index c237763fa..eb07f3871 100644 --- a/frontend/src/components/ShareRSVP.tsx +++ b/frontend/src/components/ShareRSVP.tsx @@ -30,7 +30,7 @@ export function normalizeHandle(raw: string): string { /** Build share text that fits within maxLen chars */ function buildShareText(base: string, handles: string[], maxLen: number): string { - let mentions = handles.map(h => `@${h}`); + const mentions = handles.map(h => `@${h}`); let text = `${base}\n\n${mentions.join(' ')}`; if (text.length <= maxLen) return text; // Drop handles from end but always keep @Pizza_DAO (first) diff --git a/frontend/src/components/sponsors/SponsorCRM.tsx b/frontend/src/components/sponsors/SponsorCRM.tsx index d72000056..37989f2ce 100644 --- a/frontend/src/components/sponsors/SponsorCRM.tsx +++ b/frontend/src/components/sponsors/SponsorCRM.tsx @@ -331,7 +331,7 @@ export function SponsorCRM({ partyId, onAddAsCoHost }: SponsorCRMProps) { p => p.source === 'underboss' && !p.sponsorId && p.sponsorUserId ); - let sponsorIdMap: Record = {}; + const sponsorIdMap: Record = {}; if (underbossOnly.length > 0) { const result = await ensureUnderbossSponsors( From 6510aeab30da81bec0bc853f0ec57cb7db7980c5 Mon Sep 17 00:00:00 2001 From: snackman Date: Thu, 11 Jun 2026 20:04:01 -0400 Subject: [PATCH 3/3] refactor(cavatelli P2): clear long-tail lint errors (no-empty/case-decl/useless-catch/constant-binary/etc) + ratchet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cleared all 34 non-(no-explicit-any/no-unused-vars) ESLint errors and added each cleared rule to eslint.hooks.config.js as 'error' so the CI gate enforces them. The two `false &&` constant-binary-expression JSX guards were intentional feature-hides ("Coming Soon" AI ordering); converted to named const flags (SHOW_LEGACY_PIZZERIA_SELECTION / SHOW_AI_ORDER_BUTTON) preserving exact runtime behavior. BOM-strip regexes switched from literal U+FEFF to  escape. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/eslint.hooks.config.js | 13 ++++++++ frontend/src/__tests__/field-sync.test.ts | 2 +- frontend/src/components/AICallStatus.tsx | 15 ++++----- frontend/src/components/CheckInScanner.tsx | 2 +- frontend/src/components/ImportGuestsModal.tsx | 2 +- frontend/src/components/PizzaOrderSummary.tsx | 6 +++- frontend/src/components/PizzeriaSelection.tsx | 6 +++- .../src/components/TimezonePickerInput.tsx | 9 ++++-- .../src/components/displays/DisplayCard.tsx | 3 +- .../src/components/flyer/FlyerGenerator.tsx | 12 +++---- .../generative/GenerativeCanvas.tsx | 4 +-- frontend/src/components/music/SongForm.tsx | 4 +-- .../src/components/sponsors/SponsorList.tsx | 3 +- frontend/src/components/venue/VenueWidget.tsx | 32 +++++++------------ frontend/src/lib/api.ts | 4 +-- frontend/src/lib/csvParser.ts | 4 +-- frontend/src/lib/supabase.ts | 2 +- frontend/src/pages/EventPage.tsx | 2 +- frontend/src/pages/PartnerIntakePage.tsx | 4 +-- frontend/src/types.ts | 4 +-- 20 files changed, 75 insertions(+), 58 deletions(-) diff --git a/frontend/eslint.hooks.config.js b/frontend/eslint.hooks.config.js index d38ff0965..f92153372 100644 --- a/frontend/eslint.hooks.config.js +++ b/frontend/eslint.hooks.config.js @@ -33,5 +33,18 @@ export default tseslint.config({ ignoreRestSiblings: true, }, ], + // cavatelli P2: long-tail lint errors paid down to 0; these gates keep them there. + // Core ESLint rules (no plugin needed): + 'no-empty': 'error', + 'no-case-declarations': 'error', + 'no-useless-catch': 'error', + 'no-constant-binary-expression': 'error', + 'no-irregular-whitespace': 'error', + 'no-useless-escape': 'error', + 'prefer-const': 'error', + // typescript-eslint rules: + '@typescript-eslint/no-empty-object-type': 'error', + '@typescript-eslint/prefer-as-const': 'error', + '@typescript-eslint/ban-ts-comment': 'error', }, }); diff --git a/frontend/src/__tests__/field-sync.test.ts b/frontend/src/__tests__/field-sync.test.ts index d080cdfe3..5d0c0cd74 100644 --- a/frontend/src/__tests__/field-sync.test.ts +++ b/frontend/src/__tests__/field-sync.test.ts @@ -49,7 +49,7 @@ function extractInterfaceFields(source: string, interfaceName: string): string[] if (!match) return []; const body = match[1]; - const fieldRegex = /^\s*(\w+)[\?:]?\s*:/gm; + const fieldRegex = /^\s*(\w+)[?:]?\s*:/gm; const fields: string[] = []; let fieldMatch; while ((fieldMatch = fieldRegex.exec(body)) !== null) { diff --git a/frontend/src/components/AICallStatus.tsx b/frontend/src/components/AICallStatus.tsx index 904a2e340..d6ee08a27 100644 --- a/frontend/src/components/AICallStatus.tsx +++ b/frontend/src/components/AICallStatus.tsx @@ -39,8 +39,7 @@ export const AICallStatus: React.FC = ({ // Poll for status updates useEffect(() => { - let intervalId: NodeJS.Timeout; - let timeIntervalId: NodeJS.Timeout; + const intervals: { poll?: NodeJS.Timeout; time?: NodeJS.Timeout } = {}; const fetchStatus = async () => { try { @@ -66,8 +65,8 @@ export const AICallStatus: React.FC = ({ // If call is complete, stop polling if (['completed', 'failed', 'no_answer'].includes(data.status)) { - clearInterval(intervalId); - clearInterval(timeIntervalId); + clearInterval(intervals.poll); + clearInterval(intervals.time); onComplete(data); } } catch (err) { @@ -78,16 +77,16 @@ export const AICallStatus: React.FC = ({ // Start polling fetchStatus(); - intervalId = setInterval(fetchStatus, 3000); + intervals.poll = setInterval(fetchStatus, 3000); // Update elapsed time - timeIntervalId = setInterval(() => { + intervals.time = setInterval(() => { setElapsedTime((prev) => prev + 1); }, 1000); return () => { - clearInterval(intervalId); - clearInterval(timeIntervalId); + clearInterval(intervals.poll); + clearInterval(intervals.time); }; }, [aiPhoneCallId, onComplete]); diff --git a/frontend/src/components/CheckInScanner.tsx b/frontend/src/components/CheckInScanner.tsx index f9bacc9e7..0a5315361 100644 --- a/frontend/src/components/CheckInScanner.tsx +++ b/frontend/src/components/CheckInScanner.tsx @@ -39,7 +39,7 @@ export function CheckInScanner({ currentGuestId, onVouchSuccess, onClose }: Chec if (segments.length === 3 && segments[0] === 'checkin') { return { inviteCode: segments[1], guestId: segments[2] }; } - } catch {} + } catch { /* not a URL — fall through */ } return null; }, []); diff --git a/frontend/src/components/ImportGuestsModal.tsx b/frontend/src/components/ImportGuestsModal.tsx index f0dfc6a28..ea9d8b509 100644 --- a/frontend/src/components/ImportGuestsModal.tsx +++ b/frontend/src/components/ImportGuestsModal.tsx @@ -209,7 +209,7 @@ export const ImportGuestsModal: React.FC = ({ isOpen, onClose, existingEm // Build the API payload. Apply the host's landing-status override. const approved = landStatus === 'pending' ? null : true; - const baseStatus: 'CONFIRMED' = 'CONFIRMED'; + const baseStatus = 'CONFIRMED' as const; const guests = Array.from(selected) .sort((a, b) => a - b) .map((idx) => { diff --git a/frontend/src/components/PizzaOrderSummary.tsx b/frontend/src/components/PizzaOrderSummary.tsx index aec76f39b..e82652ffc 100644 --- a/frontend/src/components/PizzaOrderSummary.tsx +++ b/frontend/src/components/PizzaOrderSummary.tsx @@ -19,6 +19,10 @@ import { supportsDirectOrdering, } from '../lib/ordering'; +// Legacy pizzeria selection UI is hidden while AI ordering is "Coming Soon". +// Flip to true to restore the manual search/select flow below. +const SHOW_LEGACY_PIZZERIA_SELECTION = false; + export const PizzaOrderSummary: React.FC = () => { const { t } = useTranslation('host'); const { recommendations, beverageRecommendations, waveRecommendations, party, guests, updatePizzaQuantity, removePizza } = usePizza(); @@ -408,7 +412,7 @@ Can you accommodate these delivery times? Please confirm total and timing.`;
{/* Original pizzeria selection - hidden while coming soon */} - {false &&
+ {SHOW_LEGACY_PIZZERIA_SELECTION &&
{!hasSearched || pizzerias.length === 0 ? ( // Show search form if no results yet
diff --git a/frontend/src/components/PizzeriaSelection.tsx b/frontend/src/components/PizzeriaSelection.tsx index bd72f8988..d1b0c442d 100644 --- a/frontend/src/components/PizzeriaSelection.tsx +++ b/frontend/src/components/PizzeriaSelection.tsx @@ -17,6 +17,10 @@ interface PizzeriaSelectionProps { embedded?: boolean; } +// AI "Order" button is hidden while AI ordering is "Coming Soon". +// Flip to true to restore the per-pizzeria Order button below. +const SHOW_AI_ORDER_BUTTON = false; + export const PizzeriaSelection: React.FC = ({ embedded = false }) => { const { party, guests, recommendations } = usePizza(); @@ -456,7 +460,7 @@ export const PizzeriaSelection: React.FC = ({ embedded =
{/* AI Order button - disabled while AI ordering is Coming Soon */} - {false && pizzeria.phone && recommendations.length > 0 && ( + {SHOW_AI_ORDER_BUTTON && pizzeria.phone && recommendations.length > 0 && (