refactor: split non-component exports (react-refresh) into their own modules
Removes the last react-refresh disables by splitting exports instead: - contexts/language.ts: Language/Translations types + LanguageContext + useTranslation/useT (LanguageProvider stays a component-only file); 12 importers repointed. - lib/sortOptions.ts: calculatePopularity + calculateDistance (out of SortDropdown) - lib/trustFilter.ts: TrustFilterLevel + filterByTrustLevel (out of TrustFilter) - EventCountdown: deleted the unused useEventStatus hook - NewsletterPage: dropped the NEWSLETTER_ISSUES re-export shim - AGENTS.md updated (lint stays clean with no blanket disables) Gate: 101 tests, 0 errors/warnings, boot smoke green.
This commit is contained in:
parent
584561e390
commit
b2a6108eeb
22 changed files with 186 additions and 196 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/** 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]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<T extends { pubkey: string }>(
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, string | number>) => string;
|
||||
/** Check if current language is RTL (for future Arabic support, etc.) */
|
||||
isRTL: boolean;
|
||||
}
|
||||
|
||||
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
|
||||
|
||||
// Import translations
|
||||
import en from '@/i18n/en';
|
||||
import es from '@/i18n/es';
|
||||
|
||||
const translations: Record<Language, Translations> = { 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, string | number>): 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<Language>('app-language', 'en');
|
||||
const [language, setLanguageState] = useState<Language>(storedLanguage);
|
||||
|
|
@ -91,7 +37,7 @@ export function LanguageProvider({ children }: LanguageProviderProps) {
|
|||
document.documentElement.lang = language;
|
||||
}, [language]);
|
||||
|
||||
const t = useCallback((key: TranslationKey, params?: Record<string, string | number>): string => {
|
||||
const t = useCallback((key: string, params?: Record<string, string | number>): string => {
|
||||
// Try to get translation in current language
|
||||
const translation = getNestedValue(translations[language], key);
|
||||
|
||||
|
|
@ -121,24 +67,3 @@ export function LanguageProvider({ children }: LanguageProviderProps) {
|
|||
</LanguageContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
|
|
|||
75
src/contexts/language.ts
Normal file
75
src/contexts/language.ts
Normal file
|
|
@ -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, string | number>) => string;
|
||||
/** Check if current language is RTL (for future Arabic support, etc.) */
|
||||
isRTL: boolean;
|
||||
}
|
||||
|
||||
export const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
|
||||
|
||||
export const translations: Record<Language, Translations> = { 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, string | number>): 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;
|
||||
}
|
||||
36
src/lib/sortOptions.ts
Normal file
36
src/lib/sortOptions.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
34
src/lib/trustFilter.ts
Normal file
34
src/lib/trustFilter.ts
Normal file
|
|
@ -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<T extends { pubkey: string }>(
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 ────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue