feat: complete NutriScan redesign
This commit is contained in:
1 parent
c1fe4af997
commit
f71cd167b3
163 files changed
+30802
-14169
No files matched your search
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface NutriScoreBadgeProps {
|
||||
@@ -10,126 +9,65 @@ interface NutriScoreBadgeProps {
|
||||
showTooltip?: boolean;
|
||||
}
|
||||
|
||||
const GRADE_INFO: Record<string, { desc: string; explanation: string }> = {
|
||||
a: { desc: "Muito bom", explanation: "Excelente qualidade nutricional" },
|
||||
b: { desc: "Bom", explanation: "Boa qualidade nutricional" },
|
||||
c: { desc: "Regular", explanation: "Qualidade nutricional moderada" },
|
||||
d: { desc: "Ruim", explanation: "Qualidade nutricional baixa" },
|
||||
e: { desc: "Muito ruim", explanation: "Qualidade nutricional muito baixa" },
|
||||
};
|
||||
|
||||
const GRADE_CLASS: Record<string, string> = {
|
||||
a: "grade-a", b: "grade-b", c: "grade-c", d: "grade-d", e: "grade-e",
|
||||
};
|
||||
|
||||
const SIZE_CLASS = {
|
||||
sm: "w-8 h-8 text-sm",
|
||||
md: "w-14 h-14 text-xl",
|
||||
lg: "w-20 h-20 text-2xl",
|
||||
};
|
||||
|
||||
export function NutriScoreBadge({
|
||||
grade,
|
||||
className,
|
||||
size = "md",
|
||||
showTooltip = true,
|
||||
}: NutriScoreBadgeProps) {
|
||||
const [showInfo, setShowInfo] = useState(false);
|
||||
|
||||
const getGradeInfo = (grade: string) => {
|
||||
const grades = {
|
||||
a: {
|
||||
label: "A",
|
||||
color: "bg-gradient-to-br from-green-400 to-green-600",
|
||||
textColor: "text-white",
|
||||
description: "Muito bom",
|
||||
explanation: "Produto com excelente qualidade nutricional",
|
||||
pulse: false,
|
||||
},
|
||||
b: {
|
||||
label: "B",
|
||||
color: "bg-gradient-to-br from-green-300 to-green-500",
|
||||
textColor: "text-white",
|
||||
description: "Bom",
|
||||
explanation: "Produto com boa qualidade nutricional",
|
||||
pulse: false,
|
||||
},
|
||||
c: {
|
||||
label: "C",
|
||||
color: "bg-gradient-to-br from-yellow-400 to-yellow-600",
|
||||
textColor: "text-white",
|
||||
description: "Regular",
|
||||
explanation: "Produto com qualidade nutricional regular",
|
||||
pulse: false,
|
||||
},
|
||||
d: {
|
||||
label: "D",
|
||||
color: "bg-gradient-to-br from-orange-400 to-orange-600",
|
||||
textColor: "text-white",
|
||||
description: "Ruim",
|
||||
explanation: "Produto com qualidade nutricional ruim",
|
||||
pulse: true,
|
||||
},
|
||||
e: {
|
||||
label: "E",
|
||||
color: "bg-gradient-to-br from-red-400 to-red-600",
|
||||
textColor: "text-white",
|
||||
description: "Muito ruim",
|
||||
explanation: "Produto com qualidade nutricional muito ruim",
|
||||
pulse: true,
|
||||
},
|
||||
};
|
||||
|
||||
return grades[grade.toLowerCase() as keyof typeof grades] || null;
|
||||
};
|
||||
|
||||
const gradeInfo = getGradeInfo(grade);
|
||||
if (!gradeInfo) return null;
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "w-8 h-8 text-sm",
|
||||
md: "w-16 h-16 text-xl",
|
||||
lg: "w-20 h-20 text-2xl",
|
||||
};
|
||||
const [hover, setHover] = useState(false);
|
||||
const key = grade?.toLowerCase();
|
||||
const info = GRADE_INFO[key];
|
||||
if (!info) return null;
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
<div className="relative inline-block">
|
||||
<div
|
||||
className={cn(
|
||||
"relative rounded-full flex items-center justify-center shadow-lg transition-all duration-300 transform hover:scale-105",
|
||||
gradeInfo.color,
|
||||
sizeClasses[size],
|
||||
gradeInfo.pulse && "animate-pulse",
|
||||
"nutriscore-hero font-sans font-bold rounded-full flex items-center justify-center cursor-default",
|
||||
GRADE_CLASS[key],
|
||||
SIZE_CLASS[size],
|
||||
className
|
||||
)}
|
||||
onMouseEnter={() => setShowInfo(true)}
|
||||
onMouseLeave={() => setShowInfo(false)}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
>
|
||||
<span className={cn("font-bold", gradeInfo.textColor)}>
|
||||
{gradeInfo.label}
|
||||
</span>
|
||||
|
||||
{/* Efeito de brilho para notas baixas */}
|
||||
{gradeInfo.pulse && (
|
||||
<div className="absolute inset-0 rounded-full bg-white opacity-20 animate-ping" />
|
||||
)}
|
||||
{key.toUpperCase()}
|
||||
</div>
|
||||
|
||||
{/* Tooltip informativo */}
|
||||
{showTooltip && showInfo && (
|
||||
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 z-50">
|
||||
<div className="bg-gray-900 text-white text-xs rounded-lg px-3 py-2 shadow-xl max-w-xs">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-semibold">
|
||||
Nutri-Score {gradeInfo.label}
|
||||
</span>
|
||||
{gradeInfo.pulse && (
|
||||
<AlertTriangle className="w-3 h-3 text-yellow-400" />
|
||||
)}
|
||||
</div>
|
||||
<p className="font-medium">{gradeInfo.description}</p>
|
||||
<p className="text-gray-300 mt-1">{gradeInfo.explanation}</p>
|
||||
|
||||
{/* Seta do tooltip */}
|
||||
<div className="absolute top-full left-1/2 transform -translate-x-1/2">
|
||||
<div className="w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-900" />
|
||||
</div>
|
||||
{showTooltip && hover && (
|
||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 z-50 whitespace-nowrap">
|
||||
<div className="bg-[var(--fg-primary)] text-[var(--bg-base)] text-xs rounded-lg px-3 py-2 shadow-xl">
|
||||
<p className="font-semibold">Nutri-Score {key.toUpperCase()}</p>
|
||||
<p className="opacity-70">{info.desc}</p>
|
||||
<p className="opacity-50 mt-0.5">{info.explanation}</p>
|
||||
<div className="absolute top-full left-1/2 -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-[var(--fg-primary)]" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Badge de alerta para notas baixas */}
|
||||
{gradeInfo.pulse && (
|
||||
<div className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 rounded-full flex items-center justify-center">
|
||||
<AlertTriangle className="w-2.5 h-2.5 text-white" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── NOVA Badge ───────────────────────────────────────────────────────────── */
|
||||
interface NovaBadgeProps {
|
||||
group: number;
|
||||
className?: string;
|
||||
@@ -137,112 +75,48 @@ interface NovaBadgeProps {
|
||||
showTooltip?: boolean;
|
||||
}
|
||||
|
||||
const NOVA_INFO: Record<number, { desc: string; explanation: string; bg: string; fg: string }> = {
|
||||
1: { desc: "Não processado", explanation: "Alimentos não ou minimamente processados", bg: "#1a7a4a", fg: "#fff" },
|
||||
2: { desc: "Ingrediente", explanation: "Ingredientes culinários processados", bg: "#e8c110", fg: "#1e1810" },
|
||||
3: { desc: "Processado", explanation: "Alimentos processados", bg: "#e07820", fg: "#fff" },
|
||||
4: { desc: "Ultraprocessado",explanation: "Alimentos ultraprocessados", bg: "#c0201a", fg: "#fff" },
|
||||
};
|
||||
|
||||
export function NovaBadge({
|
||||
group,
|
||||
className,
|
||||
size = "md",
|
||||
showTooltip = true,
|
||||
}: NovaBadgeProps) {
|
||||
const [showInfo, setShowInfo] = useState(false);
|
||||
|
||||
const getGroupInfo = (group: number) => {
|
||||
const groups = {
|
||||
1: {
|
||||
label: "1",
|
||||
color: "bg-gradient-to-br from-green-400 to-green-600",
|
||||
textColor: "text-white",
|
||||
description: "Não processado",
|
||||
explanation: "Alimentos não processados ou minimamente processados",
|
||||
pulse: false,
|
||||
},
|
||||
2: {
|
||||
label: "2",
|
||||
color: "bg-gradient-to-br from-yellow-400 to-yellow-600",
|
||||
textColor: "text-white",
|
||||
description: "Ingredientes",
|
||||
explanation: "Ingredientes culinários processados",
|
||||
pulse: false,
|
||||
},
|
||||
3: {
|
||||
label: "3",
|
||||
color: "bg-gradient-to-br from-orange-400 to-orange-600",
|
||||
textColor: "text-white",
|
||||
description: "Processado",
|
||||
explanation: "Alimentos processados",
|
||||
pulse: true,
|
||||
},
|
||||
4: {
|
||||
label: "4",
|
||||
color: "bg-gradient-to-br from-red-400 to-red-600",
|
||||
textColor: "text-white",
|
||||
description: "Ultraprocessado",
|
||||
explanation: "Alimentos ultraprocessados",
|
||||
pulse: true,
|
||||
},
|
||||
};
|
||||
|
||||
return groups[group as keyof typeof groups] || null;
|
||||
};
|
||||
|
||||
const groupInfo = getGroupInfo(group);
|
||||
if (!groupInfo) return null;
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "w-8 h-8 text-sm",
|
||||
md: "w-16 h-16 text-xl",
|
||||
lg: "w-20 h-20 text-2xl",
|
||||
};
|
||||
const [hover, setHover] = useState(false);
|
||||
const info = NOVA_INFO[group];
|
||||
if (!info) return null;
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
<div className="relative inline-block">
|
||||
<div
|
||||
className={cn(
|
||||
"relative rounded-full flex items-center justify-center shadow-lg transition-all duration-300 transform hover:scale-105",
|
||||
groupInfo.color,
|
||||
sizeClasses[size],
|
||||
groupInfo.pulse && "animate-pulse",
|
||||
"nutriscore-hero font-sans font-bold rounded-full flex items-center justify-center cursor-default",
|
||||
SIZE_CLASS[size],
|
||||
className
|
||||
)}
|
||||
onMouseEnter={() => setShowInfo(true)}
|
||||
onMouseLeave={() => setShowInfo(false)}
|
||||
style={{ background: info.bg, color: info.fg, animation: "none" }}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
>
|
||||
<span className={cn("font-bold", groupInfo.textColor)}>
|
||||
{groupInfo.label}
|
||||
</span>
|
||||
|
||||
{/* Efeito de brilho para grupos altos */}
|
||||
{groupInfo.pulse && (
|
||||
<div className="absolute inset-0 rounded-full bg-white opacity-20 animate-ping" />
|
||||
)}
|
||||
{group}
|
||||
</div>
|
||||
|
||||
{/* Tooltip informativo */}
|
||||
{showTooltip && showInfo && (
|
||||
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 z-50">
|
||||
<div className="bg-gray-900 text-white text-xs rounded-lg px-3 py-2 shadow-xl max-w-xs">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-semibold">NOVA {groupInfo.label}</span>
|
||||
{groupInfo.pulse && (
|
||||
<AlertTriangle className="w-3 h-3 text-yellow-400" />
|
||||
)}
|
||||
</div>
|
||||
<p className="font-medium">{groupInfo.description}</p>
|
||||
<p className="text-gray-300 mt-1">{groupInfo.explanation}</p>
|
||||
|
||||
{/* Seta do tooltip */}
|
||||
<div className="absolute top-full left-1/2 transform -translate-x-1/2">
|
||||
<div className="w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-900" />
|
||||
</div>
|
||||
{showTooltip && hover && (
|
||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 z-50 whitespace-nowrap">
|
||||
<div className="bg-[var(--fg-primary)] text-[var(--bg-base)] text-xs rounded-lg px-3 py-2 shadow-xl">
|
||||
<p className="font-semibold">NOVA {group}</p>
|
||||
<p className="opacity-70">{info.desc}</p>
|
||||
<p className="opacity-50 mt-0.5">{info.explanation}</p>
|
||||
<div className="absolute top-full left-1/2 -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-[var(--fg-primary)]" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Badge de alerta para grupos altos */}
|
||||
{groupInfo.pulse && (
|
||||
<div className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 rounded-full flex items-center justify-center">
|
||||
<AlertTriangle className="w-2.5 h-2.5 text-white" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user