The entire skate-spots codebase was accidentally copied into src/src/ and public/public/ instead of src/ and public/ directly. This left the real src/ directory as blank MKStack template stubs, explaining why the landing page showed the default template instead of SkateSpots. Changes made: - Copied all files from src/src/ up to src/ (App.tsx, AppRouter.tsx, main.tsx, all pages, components, hooks, contexts, lib, i18n, test) - Copied all files from public/public/ up to public/ (images, manifest, etc.) - Deleted src/src/ and public/public/ nested directories - Removed HelloWorld.tsx template stub from pages/ - Retained the previously fixed index.css (Tailwind v3 directives) Full skate-spots feature set is now active: - All 30+ routes (map, events, spots, coaches, marketplace, wallet, etc.) - NWCProvider, LanguageProvider, PWAInstallPrompt, AmberCallbackHandler - Skate community relays (goskatespots.nostr1.com, wotr.relatr.xyz, etc.) - Outfit Variable font, Amber pre-check in main.tsx - All hooks (useSkateSpots, useCoaches, useZaps, etc.) - i18n support (en/es) Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
148 lines
3.8 KiB
TypeScript
148 lines
3.8 KiB
TypeScript
import { ArrowUpDown, Clock, TrendingUp, MapPin, MessageCircle, Zap, Shield } from 'lucide-react';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
export type SortOption =
|
|
| 'recent'
|
|
| 'trust'
|
|
| 'proximity'
|
|
| 'popular'
|
|
| 'zaps';
|
|
|
|
interface SortDropdownProps {
|
|
value: SortOption;
|
|
onChange: (value: SortOption) => void;
|
|
className?: string;
|
|
showProximity?: boolean; // Only show if geolocation available
|
|
contentType?: 'spots' | 'events' | 'coaches' | 'generic';
|
|
}
|
|
|
|
const SORT_OPTIONS = {
|
|
recent: {
|
|
label: 'Most Recent',
|
|
icon: Clock,
|
|
description: 'Newest first',
|
|
},
|
|
trust: {
|
|
label: 'Highest Trust',
|
|
icon: Shield,
|
|
description: 'Most trusted creators',
|
|
},
|
|
proximity: {
|
|
label: 'Closest to Me',
|
|
icon: MapPin,
|
|
description: 'Nearest locations',
|
|
},
|
|
popular: {
|
|
label: 'Most Popular',
|
|
icon: TrendingUp,
|
|
description: 'Most comments/reactions',
|
|
},
|
|
zaps: {
|
|
label: 'Most Zapped',
|
|
icon: Zap,
|
|
description: 'Highest value received',
|
|
},
|
|
};
|
|
|
|
export function SortDropdown({
|
|
value,
|
|
onChange,
|
|
className,
|
|
showProximity = false,
|
|
contentType = 'generic',
|
|
}: SortDropdownProps) {
|
|
const selectedOption = SORT_OPTIONS[value];
|
|
const Icon = selectedOption.icon;
|
|
|
|
// Determine which sort options to show based on content type
|
|
const availableOptions: SortOption[] = ['recent', 'trust'];
|
|
|
|
if (showProximity) {
|
|
availableOptions.push('proximity');
|
|
}
|
|
|
|
// Popular sorting available for all content types
|
|
availableOptions.push('popular');
|
|
|
|
// Zaps available for all content
|
|
availableOptions.push('zaps');
|
|
|
|
return (
|
|
<Select value={value} onValueChange={(v) => onChange(v as SortOption)}>
|
|
<SelectTrigger className={cn("w-[200px] gap-2", className)}>
|
|
<ArrowUpDown className="h-4 w-4 text-muted-foreground" />
|
|
<SelectValue>
|
|
<span className="flex items-center gap-2">
|
|
<Icon className="h-4 w-4" />
|
|
{selectedOption.label}
|
|
</span>
|
|
</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{availableOptions.map((option) => {
|
|
const opt = SORT_OPTIONS[option];
|
|
const OptionIcon = opt.icon;
|
|
|
|
return (
|
|
<SelectItem key={option} value={option}>
|
|
<div className="flex items-center gap-2 w-full">
|
|
<OptionIcon className="h-4 w-4 text-muted-foreground" />
|
|
<div className="flex-1">
|
|
<div className="font-medium">{opt.label}</div>
|
|
<div className="text-xs text-muted-foreground">{opt.description}</div>
|
|
</div>
|
|
</div>
|
|
</SelectItem>
|
|
);
|
|
})}
|
|
</SelectContent>
|
|
</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);
|
|
}
|