SkateSpots/src/components/QualityMeter.tsx
shakespeare.diy e29d35f1e4 fix: restore full skate-spots source tree from misplaced src/src/ copy
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>
2026-06-15 22:18:28 +07:00

231 lines
6.2 KiB
TypeScript

import { useMemo } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import {
Zap,
Camera,
FileText,
MapPin,
Phone,
Globe,
Clock,
Check,
Circle,
} from 'lucide-react';
import { cn } from '@/lib/utils';
export interface QualityCheckItem {
id: string;
label: string;
description?: string;
points: number;
completed: boolean;
icon: React.ComponentType<{ className?: string }>;
}
interface QualityMeterProps {
type: 'spot' | 'shop';
// Form state checks
hasLocation: boolean;
hasPhotos: boolean;
photoCount: number;
hasDescription: boolean;
descriptionLength: number;
// Shop-specific
hasPhone?: boolean;
hasWebsite?: boolean;
hasHours?: boolean;
// Spot-specific
hasSpotType?: boolean;
className?: string;
}
export function QualityMeter({
type,
hasLocation,
hasPhotos,
photoCount,
hasDescription,
descriptionLength,
hasPhone,
hasWebsite,
hasHours,
hasSpotType,
className,
}: QualityMeterProps) {
const checks = useMemo(() => {
const items: QualityCheckItem[] = [
{
id: 'location',
label: 'Location added',
points: 20,
completed: hasLocation,
icon: MapPin,
},
{
id: 'photo1',
label: 'At least 1 photo',
points: 20,
completed: hasPhotos,
icon: Camera,
},
{
id: 'photo3',
label: '3+ photos',
description: 'More angles = better',
points: 15,
completed: photoCount >= 3,
icon: Camera,
},
{
id: 'description',
label: 'Description',
points: 15,
completed: hasDescription && descriptionLength >= 20,
icon: FileText,
},
{
id: 'description-detailed',
label: 'Detailed description (100+ chars)',
points: 10,
completed: descriptionLength >= 100,
icon: FileText,
},
];
if (type === 'shop') {
items.push(
{
id: 'contact',
label: 'Contact info',
description: 'Phone or website',
points: 10,
completed: !!hasPhone || !!hasWebsite,
icon: Phone,
},
{
id: 'hours',
label: 'Opening hours',
points: 10,
completed: !!hasHours,
icon: Clock,
}
);
} else {
items.push({
id: 'spot-type',
label: 'Spot type selected',
points: 10,
completed: !!hasSpotType,
icon: MapPin,
});
}
return items;
}, [type, hasLocation, hasPhotos, photoCount, hasDescription, descriptionLength, hasPhone, hasWebsite, hasHours, hasSpotType]);
const { score, maxScore, completedItems, totalItems } = useMemo(() => {
const completed = checks.filter(c => c.completed);
const total = checks.reduce((sum, c) => sum + c.points, 0);
const current = completed.reduce((sum, c) => sum + c.points, 0);
return {
score: current,
maxScore: total,
completedItems: completed.length,
totalItems: checks.length,
};
}, [checks]);
const percentage = Math.round((score / maxScore) * 100);
const getScoreColor = () => {
if (percentage >= 80) return 'text-green-400';
if (percentage >= 50) return 'text-[#ccff00]';
if (percentage >= 30) return 'text-yellow-400';
return 'text-orange-400';
};
const getProgressColor = () => {
if (percentage >= 80) return 'bg-green-500';
if (percentage >= 50) return 'bg-[#ccff00]';
if (percentage >= 30) return 'bg-yellow-500';
return 'bg-orange-500';
};
const getScoreLabel = () => {
if (percentage >= 80) return 'Excellent!';
if (percentage >= 50) return 'Good';
if (percentage >= 30) return 'Basic';
return 'Getting started';
};
return (
<Card className={cn('bg-gradient-to-br from-[#ccff00]/5 to-transparent border-[#ccff00]/20', className)}>
<CardContent className="p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Zap className="h-4 w-4 text-[#ccff00]" />
<span className="text-sm font-bold uppercase text-gray-300">Earning Potential</span>
</div>
<div className="flex items-center gap-2">
<span className={cn('text-sm font-bold', getScoreColor())}>
{getScoreLabel()}
</span>
<span className="text-xs text-gray-500">
{percentage}%
</span>
</div>
</div>
{/* Progress Bar */}
<div className="relative h-3 bg-gray-800 rounded-full overflow-hidden mb-4">
<div
className={cn('h-full transition-all duration-500 ease-out rounded-full', getProgressColor())}
style={{ width: `${percentage}%` }}
/>
</div>
{/* Checklist */}
<div className="space-y-2">
{checks.map((check) => {
const Icon = check.icon;
return (
<div
key={check.id}
className={cn(
'flex items-center gap-2 text-xs transition-all',
check.completed ? 'text-green-400' : 'text-gray-500'
)}
>
<div className="flex-shrink-0">
{check.completed ? (
<Check className="h-3.5 w-3.5" />
) : (
<Circle className="h-3.5 w-3.5" />
)}
</div>
<Icon className="h-3 w-3 flex-shrink-0" />
<span className={check.completed ? 'line-through opacity-70' : ''}>
{check.label}
</span>
<span className={cn(
'ml-auto text-[10px] font-bold',
check.completed ? 'text-green-400' : 'text-gray-600'
)}>
+{check.points}%
</span>
</div>
);
})}
</div>
{/* Tip */}
<div className="mt-4 pt-3 border-t border-gray-800">
<p className="text-[10px] text-gray-500">
💡 Higher quality = More zaps from the community!
</p>
</div>
</CardContent>
</Card>
);
}