diff --git a/AGENTS.md b/AGENTS.md index b02fe77..12fcce4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -95,9 +95,10 @@ session with **no other memory of this project**. So: - **Baselines** (verified 2026-09-26): `npm test` → **0 errors, 0 warnings**, **101 tests**, build ok. `package.json` version **1.4.5**. - ✅ Lint is **clean**: `no-explicit-any` = 0 · `react-hooks/exhaustive-deps` = 0 - · `no-unused-vars` = 0 · `react-refresh` = 0 (the last were intentional - component+hook/helper colocations carrying a justified file-level disable). - Keep it at zero — don't reintroduce any. + · `no-unused-vars` = 0 · `react-refresh` = 0. Non-component exports were + **split into their own modules** (`contexts/language.ts`, `lib/sortOptions.ts`, + `lib/trustFilter.ts`) and unused hooks/functions deleted — no blanket + disables. Keep it at zero — don't reintroduce any. - ⚠️ Do **not** mass-fix `no-unused-vars` by name or blanket rename — shadowed names and typed shorthand destructuring make it unsafe. Fix position-precise. Renaming a component to `_Name` also trips `rules-of-hooks` — delete it or diff --git a/src/components/BackupReminder.tsx b/src/components/BackupReminder.tsx index 98f651b..f77d9f4 100644 --- a/src/components/BackupReminder.tsx +++ b/src/components/BackupReminder.tsx @@ -4,7 +4,7 @@ import { AlertTriangle, X, Download, CheckCircle } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useLocalStorage } from '@/hooks/useLocalStorage'; -import { useTranslation } from '@/contexts/LanguageContext'; +import { useTranslation } from '@/contexts/language'; import { Sheet, SheetContent, diff --git a/src/components/CustomizeNavSheet.tsx b/src/components/CustomizeNavSheet.tsx index b91a6f8..4545732 100644 --- a/src/components/CustomizeNavSheet.tsx +++ b/src/components/CustomizeNavSheet.tsx @@ -7,7 +7,7 @@ import { type NavTab, } from '@/hooks/useNavPreferences'; import { useCurrentUser } from '@/hooks/useCurrentUser'; -import { useTranslation } from '@/contexts/LanguageContext'; +import { useTranslation } from '@/contexts/language'; import { cn } from '@/lib/utils'; import { MapPin, User, Lock, ArrowUp, ArrowDown, RotateCcw, Check, Sparkles } from 'lucide-react'; diff --git a/src/components/DesktopNavManager.tsx b/src/components/DesktopNavManager.tsx index 73c0416..42581f5 100644 --- a/src/components/DesktopNavManager.tsx +++ b/src/components/DesktopNavManager.tsx @@ -1,5 +1,5 @@ import { useDesktopNavPreferences, type NavTab } from '@/hooks/useNavPreferences'; -import { useTranslation } from '@/contexts/LanguageContext'; +import { useTranslation } from '@/contexts/language'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import { ArrowUp, ArrowDown, Eye, EyeOff, Lock, RotateCcw, Monitor } from 'lucide-react'; diff --git a/src/components/EventCountdown.tsx b/src/components/EventCountdown.tsx index 625b71c..aef8457 100644 --- a/src/components/EventCountdown.tsx +++ b/src/components/EventCountdown.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react-refresh/only-export-components -- EventCountdown component + useEventStatus hook are colocated by design (dev-only HMR rule). */ import { useEffect, useMemo, useState } from 'react'; import { cn } from '@/lib/utils'; @@ -87,15 +86,3 @@ export function EventCountdown({ ); } - -/** Standalone helper to get just the status (for conditional styling). */ -export function useEventStatus(start: number, end?: number): EventStatus { - const [tick, forceTick] = useState(0); - useEffect(() => { - const id = setInterval(() => forceTick((n) => n + 1), 30_000); - return () => clearInterval(id); - }, []); - // `tick` forces recompute on the interval (it is not used inside the memo). - // eslint-disable-next-line react-hooks/exhaustive-deps - return useMemo(() => computeStatus(start, end).status, [start, end, tick]); -} diff --git a/src/components/LanguageToggle.tsx b/src/components/LanguageToggle.tsx index 3b1ebde..f49f555 100644 --- a/src/components/LanguageToggle.tsx +++ b/src/components/LanguageToggle.tsx @@ -6,7 +6,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; -import { useTranslation, type Language } from '@/contexts/LanguageContext'; +import { useTranslation, type Language } from '@/contexts/language'; import { cn } from '@/lib/utils'; interface LanguageOption { diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index ae18fc5..8e827f7 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -17,7 +17,7 @@ import { LoginArea } from '@/components/auth/LoginArea'; import { useSasquatchGuide } from '@/hooks/useSasquatchGuide'; import { LanguageToggle } from '@/components/LanguageToggle'; import { useCurrentUser } from '@/hooks/useCurrentUser'; -import { useTranslation } from '@/contexts/LanguageContext'; +import { useTranslation } from '@/contexts/language'; import { useLoggedInAccounts } from '@/hooks/useLoggedInAccounts'; import { diff --git a/src/components/PWAInstallPrompt.tsx b/src/components/PWAInstallPrompt.tsx index 33d6920..1459e71 100644 --- a/src/components/PWAInstallPrompt.tsx +++ b/src/components/PWAInstallPrompt.tsx @@ -1,6 +1,6 @@ import { useEffect, useState, useRef, useCallback } from 'react'; import { useLocalStorage } from '@/hooks/useLocalStorage'; -import { useTranslation } from '@/contexts/LanguageContext'; +import { useTranslation } from '@/contexts/language'; import { Sheet, SheetContent, diff --git a/src/components/SortDropdown.tsx b/src/components/SortDropdown.tsx index 8463860..b16a99f 100644 --- a/src/components/SortDropdown.tsx +++ b/src/components/SortDropdown.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react-refresh/only-export-components -- SortDropdown component + its sort option constants/helpers are intentionally colocated (dev-only HMR rule). */ import { ArrowUpDown, Clock, TrendingUp, MapPin, Zap, Shield } from 'lucide-react'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { cn } from '@/lib/utils'; @@ -100,43 +99,3 @@ export function SortDropdown({ ); } - -/** - * Helper function to calculate popularity score for sorting - * Based on comments, reactions, and other engagement metrics - */ -export function calculatePopularity( - commentCount: number = 0, - reactionCount: number = 0, - zapCount: number = 0 -): number { - // Weighted popularity: comments worth 3x, reactions 1x, zaps 5x - return (commentCount * 3) + (reactionCount * 1) + (zapCount * 5); -} - -/** - * Calculate distance between two coordinates in kilometers - * Uses Haversine formula - */ -export function calculateDistance( - lat1: number, - lon1: number, - lat2: number, - lon2: number -): number { - const R = 6371; // Earth's radius in km - const dLat = toRad(lat2 - lat1); - const dLon = toRad(lon2 - lon1); - - const a = - Math.sin(dLat / 2) * Math.sin(dLat / 2) + - Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * - Math.sin(dLon / 2) * Math.sin(dLon / 2); - - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - return R * c; -} - -function toRad(degrees: number): number { - return degrees * (Math.PI / 180); -} diff --git a/src/components/SpotChat.tsx b/src/components/SpotChat.tsx index 0f36cbc..e24a711 100644 --- a/src/components/SpotChat.tsx +++ b/src/components/SpotChat.tsx @@ -21,11 +21,11 @@ import { useAuthor } from '@/hooks/useAuthor'; import { useToast } from '@/hooks/useToast'; import { useBatchTrustScores } from '@/hooks/useBatchTrustScores'; import { genUserName } from '@/lib/genUserName'; +import { TrustFilter } from '@/components/TrustFilter'; import { - TrustFilter, filterByTrustLevel, type TrustFilterLevel, -} from '@/components/TrustFilter'; +} from '@/lib/trustFilter'; import { useSpotChat, useSendSpotChat, diff --git a/src/components/TrustFilter.tsx b/src/components/TrustFilter.tsx index 604a63b..80dd272 100644 --- a/src/components/TrustFilter.tsx +++ b/src/components/TrustFilter.tsx @@ -1,6 +1,6 @@ -/* eslint-disable react-refresh/only-export-components -- TrustFilter component + its level constants/helpers are intentionally colocated (dev-only HMR rule). */ import { Shield, CheckCircle2, Users } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; +import type { TrustFilterLevel } from '@/lib/trustFilter'; import { Button } from '@/components/ui/button'; import { DropdownMenu, @@ -12,8 +12,6 @@ import { } from '@/components/ui/dropdown-menu'; import { cn } from '@/lib/utils'; -export type TrustFilterLevel = 'all' | 'verified' | 'trusted' | 'following'; - interface TrustFilterProps { value: TrustFilterLevel; onChange: (value: TrustFilterLevel) => void; @@ -137,26 +135,3 @@ export function TrustFilter({ value, onChange, className, compact = false }: Tru /** * Helper function to filter items by trust score */ -export function filterByTrustLevel( - items: T[], - level: TrustFilterLevel, - getTrustScore: (pubkey: string) => number | undefined -): T[] { - if (level === 'all') return items; - - return items.filter((item) => { - const score = getTrustScore(item.pubkey); - if (score === undefined) return false; - - switch (level) { - case 'verified': - return score >= 90; - case 'trusted': - return score >= 60; - case 'following': - return score === 90 || score === 100; - default: - return true; - } - }); -} diff --git a/src/contexts/LanguageContext.tsx b/src/contexts/LanguageContext.tsx index f979ebe..0fd9bff 100644 --- a/src/contexts/LanguageContext.tsx +++ b/src/contexts/LanguageContext.tsx @@ -1,75 +1,21 @@ -/* eslint-disable react-refresh/only-export-components -- LanguageProvider + useTranslation hook + context are colocated by design (idiomatic context module; dev-only HMR rule). */ -import { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react'; +import { useCallback, useEffect, useState, type ReactNode } from 'react'; import { useLocalStorage } from '@/hooks/useLocalStorage'; - -// Supported languages -export type Language = 'en' | 'es'; - -// Translation key type - allows nested keys like 'nav.home' -type TranslationKey = string; - -// Translation values can be strings or nested objects -type TranslationValue = string | { [key: string]: TranslationValue }; - -// Translations structure -export type Translations = { - [key: string]: TranslationValue; -}; - -interface LanguageContextType { - /** Current language */ - language: Language; - /** Set the current language */ - setLanguage: (lang: Language) => void; - /** Translate a key - supports dot notation for nested keys */ - t: (key: TranslationKey, params?: Record) => string; - /** Check if current language is RTL (for future Arabic support, etc.) */ - isRTL: boolean; -} - -const LanguageContext = createContext(undefined); - -// Import translations -import en from '@/i18n/en'; -import es from '@/i18n/es'; - -const translations: Record = { en, es }; - -/** - * Get nested value from object using dot notation - * e.g., getNestedValue({ nav: { home: 'Home' } }, 'nav.home') => 'Home' - */ -function getNestedValue(obj: Translations, path: string): string | undefined { - const keys = path.split('.'); - let current: TranslationValue = obj; - - for (const key of keys) { - if (current && typeof current === 'object' && key in current) { - current = current[key]; - } else { - return undefined; - } - } - - return typeof current === 'string' ? current : undefined; -} - -/** - * Replace parameters in translation string - * e.g., interpolate('Hello {{name}}!', { name: 'World' }) => 'Hello World!' - */ -function interpolate(str: string, params?: Record): string { - if (!params) return str; - - return str.replace(/\{\{(\w+)\}\}/g, (_, key) => { - return params[key]?.toString() ?? `{{${key}}}`; - }); -} +import { + LanguageContext, + getNestedValue, + interpolate, + translations, + type Language, +} from './language'; interface LanguageProviderProps { children: ReactNode; } +/** + * Provides language state + translation. The context, types and hooks live in + * `./language` so this file exports only the component (React Fast Refresh). + */ export function LanguageProvider({ children }: LanguageProviderProps) { const [storedLanguage, setStoredLanguage] = useLocalStorage('app-language', 'en'); const [language, setLanguageState] = useState(storedLanguage); @@ -91,14 +37,14 @@ export function LanguageProvider({ children }: LanguageProviderProps) { document.documentElement.lang = language; }, [language]); - const t = useCallback((key: TranslationKey, params?: Record): string => { + const t = useCallback((key: string, params?: Record): string => { // Try to get translation in current language const translation = getNestedValue(translations[language], key); - + if (translation) { return interpolate(translation, params); } - + // Fallback to English if (language !== 'en') { const fallback = getNestedValue(translations.en, key); @@ -106,7 +52,7 @@ export function LanguageProvider({ children }: LanguageProviderProps) { return interpolate(fallback, params); } } - + // Return key if no translation found (helps identify missing translations) console.warn(`Missing translation for key: ${key}`); return key; @@ -121,24 +67,3 @@ export function LanguageProvider({ children }: LanguageProviderProps) { ); } - -/** - * Hook to access translation functions and language state - */ -export function useTranslation() { - const context = useContext(LanguageContext); - - if (context === undefined) { - throw new Error('useTranslation must be used within a LanguageProvider'); - } - - return context; -} - -/** - * Hook to get just the translation function (convenience) - */ -export function useT() { - const { t } = useTranslation(); - return t; -} diff --git a/src/contexts/language.ts b/src/contexts/language.ts new file mode 100644 index 0000000..166cb2d --- /dev/null +++ b/src/contexts/language.ts @@ -0,0 +1,75 @@ +import { createContext, useContext } from 'react'; +import en from '@/i18n/en'; +import es from '@/i18n/es'; + +/** + * Language context + types + hooks. Split from the `LanguageProvider` component + * so the component file exports only a component (React Fast Refresh), per the + * `react-refresh/only-export-components` rule. + */ + +export type Language = 'en' | 'es'; + +type TranslationKey = string; +type TranslationValue = string | { [key: string]: TranslationValue }; + +export type Translations = { + [key: string]: TranslationValue; +}; + +export interface LanguageContextType { + /** Current language */ + language: Language; + /** Set the current language */ + setLanguage: (lang: Language) => void; + /** Translate a key - supports dot notation for nested keys */ + t: (key: TranslationKey, params?: Record) => string; + /** Check if current language is RTL (for future Arabic support, etc.) */ + isRTL: boolean; +} + +export const LanguageContext = createContext(undefined); + +export const translations: Record = { en, es }; + +/** Get a nested value from an object using dot notation (`nav.home`). */ +export function getNestedValue(obj: Translations, path: string): string | undefined { + const keys = path.split('.'); + let current: TranslationValue = obj; + + for (const key of keys) { + if (current && typeof current === 'object' && key in current) { + current = current[key]; + } else { + return undefined; + } + } + + return typeof current === 'string' ? current : undefined; +} + +/** Replace `{{param}}` placeholders in a translation string. */ +export function interpolate(str: string, params?: Record): string { + if (!params) return str; + + return str.replace(/\{\{(\w+)\}\}/g, (_, key) => { + return params[key]?.toString() ?? `{{${key}}}`; + }); +} + +/** Hook to access translation functions and language state. */ +export function useTranslation() { + const context = useContext(LanguageContext); + + if (context === undefined) { + throw new Error('useTranslation must be used within a LanguageProvider'); + } + + return context; +} + +/** Hook to get just the translation function (convenience). */ +export function useT() { + const { t } = useTranslation(); + return t; +} diff --git a/src/lib/sortOptions.ts b/src/lib/sortOptions.ts new file mode 100644 index 0000000..70b6eaa --- /dev/null +++ b/src/lib/sortOptions.ts @@ -0,0 +1,36 @@ +/** + * Sort/geo helpers shared across pages. Kept out of SortDropdown.tsx so that + * component file exports only a component (React Fast Refresh). + */ + +/** Weighted popularity score: comments 3x, reactions 1x, zaps 5x. */ +export function calculatePopularity( + commentCount = 0, + reactionCount = 0, + zapCount = 0, +): number { + return commentCount * 3 + reactionCount * 1 + zapCount * 5; +} + +/** Great-circle distance between two coordinates, in kilometres (Haversine). */ +export function calculateDistance( + lat1: number, + lon1: number, + lat2: number, + lon2: number, +): number { + const R = 6371; // Earth's radius in km + const dLat = toRad(lat2 - lat1); + const dLon = toRad(lon2 - lon1); + + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); + + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return R * c; +} + +function toRad(degrees: number): number { + return degrees * (Math.PI / 180); +} diff --git a/src/lib/trustFilter.ts b/src/lib/trustFilter.ts new file mode 100644 index 0000000..faca316 --- /dev/null +++ b/src/lib/trustFilter.ts @@ -0,0 +1,34 @@ +/** + * Trust-filter levels + filtering helper. Kept out of TrustFilter.tsx so that + * component file exports only a component (React Fast Refresh). + */ + +export type TrustFilterLevel = 'all' | 'verified' | 'trusted' | 'following'; + +/** + * Filter items by the web-of-trust score of their author. + * `verified` ≥ 90, `trusted` ≥ 60, `following` is the top of the source's graph. + */ +export function filterByTrustLevel( + items: T[], + level: TrustFilterLevel, + getTrustScore: (pubkey: string) => number | undefined, +): T[] { + if (level === 'all') return items; + + return items.filter((item) => { + const score = getTrustScore(item.pubkey); + if (score === undefined) return false; + + switch (level) { + case 'verified': + return score >= 90; + case 'trusted': + return score >= 60; + case 'following': + return score === 90 || score === 100; + default: + return true; + } + }); +} diff --git a/src/pages/CreateEventPage.tsx b/src/pages/CreateEventPage.tsx index b3f89c4..ae9fd8c 100644 --- a/src/pages/CreateEventPage.tsx +++ b/src/pages/CreateEventPage.tsx @@ -17,7 +17,7 @@ import { useUploadFile } from '@/hooks/useUploadFile'; import { useTours } from '@/hooks/useTours'; import { useSkateSpots } from '@/hooks/useSkateSpots'; import { useToast } from '@/hooks/useToast'; -import { useTranslation } from '@/contexts/LanguageContext'; +import { useTranslation } from '@/contexts/language'; import { LoginArea } from '@/components/auth/LoginArea'; import { CALENDAR_EVENT_KIND, EVENT_TYPE_LABELS, TOUR_CATEGORIES, SKATE_TOUR_KIND, SKATE_SPOT_KIND, SKATE_CREW_KIND, SKATE_MUSICIAN_KIND, SPOT_CATEGORIES, EVENT_SPONSOR_TIERS, eventSponsorTags, type SkateTour, type SkateSpot, type EventSponsor } from '@/lib/types'; import { useCrew } from '@/hooks/useCrews'; diff --git a/src/pages/CreateSpotPage.tsx b/src/pages/CreateSpotPage.tsx index 33f4208..3b7a239 100644 --- a/src/pages/CreateSpotPage.tsx +++ b/src/pages/CreateSpotPage.tsx @@ -12,7 +12,7 @@ import { useNostrPublish } from '@/hooks/useNostrPublish'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useUploadFile } from '@/hooks/useUploadFile'; import { useToast } from '@/hooks/useToast'; -import { useTranslation } from '@/contexts/LanguageContext'; +import { useTranslation } from '@/contexts/language'; import { LoginArea } from '@/components/auth/LoginArea'; import { QualityMeter } from '@/components/QualityMeter'; import { TagInput } from '@/components/TagInput'; diff --git a/src/pages/EventsPage.tsx b/src/pages/EventsPage.tsx index fa69f38..32ef59b 100644 --- a/src/pages/EventsPage.tsx +++ b/src/pages/EventsPage.tsx @@ -10,7 +10,7 @@ import { EventsCalendar } from '@/components/EventsCalendar'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'; -import { type TrustFilterLevel, filterByTrustLevel } from '@/components/TrustFilter'; +import { filterByTrustLevel, type TrustFilterLevel } from '@/lib/trustFilter'; import { type SortOption } from '@/components/SortDropdown'; import { useSkateEvents } from '@/hooks/useSkateEvents'; import { useBatchEventRsvps, eventCoord } from '@/hooks/useEventRsvps'; @@ -19,7 +19,7 @@ import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useFollowing } from '@/hooks/useFollowing'; import { useBatchTrustScores } from '@/hooks/useBatchTrustScores'; import { useCommentCounts } from '@/hooks/useCommentCounts'; -import { useTranslation } from '@/contexts/LanguageContext'; +import { useTranslation } from '@/contexts/language'; import { EVENT_TYPE_LABELS, TOUR_CATEGORIES, diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 790f822..2ca2587 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -14,7 +14,7 @@ import { useZapLeaderboard } from '@/hooks/useZapLeaderboard'; import { SKATE_SPOT_KIND, type SkateSpot } from '@/lib/types'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useAuthor } from '@/hooks/useAuthor'; -import { useTranslation } from '@/contexts/LanguageContext'; +import { useTranslation } from '@/contexts/language'; import { genUserName } from '@/lib/genUserName'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { Badge } from '@/components/ui/badge'; diff --git a/src/pages/LabPage.tsx b/src/pages/LabPage.tsx index 68c1f3f..f142fa4 100644 --- a/src/pages/LabPage.tsx +++ b/src/pages/LabPage.tsx @@ -5,13 +5,13 @@ import { Layout } from '@/components/Layout'; import { LazyMount } from '@/components/LazyMount'; import { Badge } from '@/components/ui/badge'; import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'; -import { type TrustFilterLevel, filterByTrustLevel } from '@/components/TrustFilter'; +import { filterByTrustLevel, type TrustFilterLevel } from '@/lib/trustFilter'; import { type SortOption } from '@/components/SortDropdown'; import { useLabProfiles, type LabProfile } from '@/hooks/useLabProfiles'; import { useAuthor } from '@/hooks/useAuthor'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useBatchTrustScores } from '@/hooks/useBatchTrustScores'; -import { useTranslation } from '@/contexts/LanguageContext'; +import { useTranslation } from '@/contexts/language'; import { genUserName } from '@/lib/genUserName'; import { COACH_SKILL_LABELS, diff --git a/src/pages/MapPage.tsx b/src/pages/MapPage.tsx index 6e28479..c0298ba 100644 --- a/src/pages/MapPage.tsx +++ b/src/pages/MapPage.tsx @@ -7,8 +7,10 @@ import { Layout } from '@/components/Layout'; const SpotMap = lazy(() => import('@/components/SpotMap').then(m => ({ default: m.SpotMap }))); import { SpotCard, SpotCardSkeleton } from '@/components/SpotCard'; import { WeatherToggle } from '@/components/WeatherToggle'; -import { TrustFilter, type TrustFilterLevel, filterByTrustLevel } from '@/components/TrustFilter'; -import { SortDropdown, type SortOption, calculateDistance } from '@/components/SortDropdown'; +import { TrustFilter } from '@/components/TrustFilter'; +import { filterByTrustLevel, type TrustFilterLevel } from '@/lib/trustFilter'; +import { SortDropdown, type SortOption } from '@/components/SortDropdown'; +import { calculateDistance } from '@/lib/sortOptions'; import { useZapTotals, zapTargetKey } from '@/hooks/useZapTotals'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -39,7 +41,7 @@ import { useAuthor } from '@/hooks/useAuthor'; import { useSatsToUsd } from '@/hooks/useExchangeRate'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { genUserName } from '@/lib/genUserName'; -import { useTranslation } from '@/contexts/LanguageContext'; +import { useTranslation } from '@/contexts/language'; import { SPOT_CATEGORIES, SPOT_TYPE_LABELS, SHELTER_INFO } from '@/lib/types'; import type { SkateSpot } from '@/lib/types'; import ngeohash from 'ngeohash'; diff --git a/src/pages/NewsletterPage.tsx b/src/pages/NewsletterPage.tsx index 66c46f7..034f97d 100644 --- a/src/pages/NewsletterPage.tsx +++ b/src/pages/NewsletterPage.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react-refresh/only-export-components -- NewsletterPage component + its newsletter data exports are intentionally colocated (dev-only HMR rule). */ import { useEffect, useState } from 'react'; import { useSeoMeta } from '@unhead/react'; import { nip19 } from 'nostr-tools'; @@ -43,9 +42,6 @@ import { isAdminPubkey } from '@/lib/admins'; // ─── Newsletter issue structure ─────────────────────────────────────────────── // import { NEWSLETTER_ISSUES, type NewsletterUpdate, type SpotOfTheWeek, type NewsletterIssue } from '@/lib/newsletterIssues'; -// Preserve the previous public surface for existing importers. -export { NEWSLETTER_ISSUES } from '@/lib/newsletterIssues'; -export type { NewsletterUpdate, SpotOfTheWeek, NewsletterIssue } from '@/lib/newsletterIssues'; // ─── Nostr post preview + publish ────────────────────────────────────────────