SkateSpots/src/components/QualityMeter.tsx

231 lines
6.4 KiB
TypeScript

import { useMemo } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import {
Zap,
Camera,
FileText,
MapPin,
Phone,
Clock,
Check,
Circle,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { useTranslation } from '@/contexts/language';
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 { t } = useTranslation();
const checks = useMemo(() => {
const items: QualityCheckItem[] = [
{
id: 'location',
label: t('miscUi.qualityLocationAdded'),
points: 20,
completed: hasLocation,
icon: MapPin,
},
{
id: 'photo1',
label: t('miscUi.qualityOnePhoto'),
points: 20,
completed: hasPhotos,
icon: Camera,
},
{
id: 'photo3',
label: t('miscUi.qualityThreePhotos'),
description: t('miscUi.qualityMoreAngles'),
points: 15,
completed: photoCount >= 3,
icon: Camera,
},
{
id: 'description',
label: t('miscUi.qualityDescription'),
points: 15,
completed: hasDescription && descriptionLength >= 20,
icon: FileText,
},
{
id: 'description-detailed',
label: t('miscUi.qualityDetailedDesc'),
points: 10,
completed: descriptionLength >= 100,
icon: FileText,
},
];
if (type === 'shop') {
items.push(
{
id: 'contact',
label: t('miscUi.qualityContact'),
description: t('miscUi.qualityPhoneOrWebsite'),
points: 10,
completed: !!hasPhone || !!hasWebsite,
icon: Phone,
},
{
id: 'hours',
label: t('miscUi.qualityHours'),
points: 10,
completed: !!hasHours,
icon: Clock,
}
);
} else {
items.push({
id: 'spot-type',
label: t('miscUi.qualitySpotType'),
points: 10,
completed: !!hasSpotType,
icon: MapPin,
});
}
return items;
}, [type, hasLocation, hasPhotos, photoCount, hasDescription, descriptionLength, hasPhone, hasWebsite, hasHours, hasSpotType, t]);
const { score, maxScore } = 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 t('miscUi.qualityExcellent');
if (percentage >= 50) return t('miscUi.qualityGood');
if (percentage >= 30) return t('miscUi.qualityBasic');
return t('miscUi.qualityGettingStarted');
};
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">{t('miscUi.earningPotential')}</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>
);
}