refactor(CardList): replace ProductCard with ListaProductCard
Updated CardList component to use ListaProductCard instead of ProductCard for rendering products, improving component specificity for list views. Also adjusted indentation for consistency.
This commit is contained in:
1 parent
9a4e4c762e
commit
166abeb64e
24 files changed
+554
-716
No files matched your search
+54
-54
@@ -1,66 +1,66 @@
|
|||||||
import { listarProdutos } from "@/app/actions";
|
import { listarProdutos } from "@/app/actions";
|
||||||
import { ProductCard } from "./ProductCard";
|
import { ListaProductCard } from "./ListaProductCard";
|
||||||
|
|
||||||
interface Produto {
|
interface Produto {
|
||||||
id: number;
|
id: number;
|
||||||
nome: string;
|
nome: string;
|
||||||
qtd: number;
|
qtd: number;
|
||||||
tipoUN: string;
|
tipoUN: string;
|
||||||
comprado: boolean;
|
comprado: boolean;
|
||||||
listaId: string;
|
listaId: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CardListProps {
|
interface CardListProps {
|
||||||
listaId: string;
|
listaId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function CardList({ listaId }: CardListProps) {
|
export async function CardList({ listaId }: CardListProps) {
|
||||||
const produtos = await listarProdutos(listaId);
|
const produtos = await listarProdutos(listaId);
|
||||||
|
|
||||||
if (produtos.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="text-center p-8 text-gray-500">
|
|
||||||
Nenhum produto cadastrado ainda.
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const produtosNaoComprados = produtos.filter((p: Produto) => !p.comprado);
|
|
||||||
const produtosComprados = produtos.filter((p: Produto) => p.comprado);
|
|
||||||
|
|
||||||
|
if (produtos.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="text-center p-8 text-gray-500">
|
||||||
<section>
|
Nenhum produto cadastrado ainda.
|
||||||
<h2 className="text-2xl font-bold mb-4">Produtos Pendentes</h2>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
{produtosNaoComprados.map((produto: Produto) => (
|
|
||||||
<ProductCard
|
|
||||||
key={produto.id}
|
|
||||||
produto={produto}
|
|
||||||
listaId={listaId}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{produtosComprados.length > 0 && (
|
|
||||||
<section>
|
|
||||||
<h2 className="text-2xl font-bold mb-4 text-green-700">
|
|
||||||
Produtos Comprados
|
|
||||||
</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
{produtosComprados.map((produto: Produto) => (
|
|
||||||
<ProductCard
|
|
||||||
key={produto.id}
|
|
||||||
produto={produto}
|
|
||||||
listaId={listaId}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const produtosNaoComprados = produtos.filter((p: Produto) => !p.comprado);
|
||||||
|
const produtosComprados = produtos.filter((p: Produto) => p.comprado);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
<section>
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Produtos Pendentes</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{produtosNaoComprados.map((produto: Produto) => (
|
||||||
|
<ListaProductCard
|
||||||
|
key={produto.id}
|
||||||
|
produto={produto}
|
||||||
|
listaId={listaId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{produtosComprados.length > 0 && (
|
||||||
|
<section>
|
||||||
|
<h2 className="text-2xl font-bold mb-4 text-green-700">
|
||||||
|
Produtos Comprados
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{produtosComprados.map((produto: Produto) => (
|
||||||
|
<ListaProductCard
|
||||||
|
key={produto.id}
|
||||||
|
produto={produto}
|
||||||
|
listaId={listaId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState } from "react";
|
||||||
import { Search, Filter, X, AlertTriangle, Zap } from "lucide-react";
|
import { Filter, X, AlertTriangle, Zap } from "lucide-react";
|
||||||
import * as LucideIcons from "lucide-react";
|
import * as LucideIcons from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -15,17 +15,14 @@ import {
|
|||||||
AccordionItem,
|
AccordionItem,
|
||||||
AccordionTrigger,
|
AccordionTrigger,
|
||||||
} from "@/components/ui/accordion";
|
} from "@/components/ui/accordion";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import {
|
import {
|
||||||
ProductFilters,
|
ProductFilters,
|
||||||
NUTRITION_GRADES,
|
NUTRITION_GRADES,
|
||||||
NOVA_GROUPS,
|
NOVA_GROUPS,
|
||||||
POPULAR_CATEGORIES,
|
|
||||||
POPULAR_CATEGORIES_WITH_ICONS,
|
POPULAR_CATEGORIES_WITH_ICONS,
|
||||||
COMMON_ALLERGENS,
|
COMMON_ALLERGENS,
|
||||||
COMMON_ADDITIVES,
|
|
||||||
} from "@/lib/types";
|
} from "@/lib/types";
|
||||||
|
|
||||||
interface FilterSidebarProps {
|
interface FilterSidebarProps {
|
||||||
@@ -49,7 +46,7 @@ export function FilterSidebar({
|
|||||||
const [searchCategories, setSearchCategories] = useState("");
|
const [searchCategories, setSearchCategories] = useState("");
|
||||||
const [searchAllergens, setSearchAllergens] = useState("");
|
const [searchAllergens, setSearchAllergens] = useState("");
|
||||||
|
|
||||||
const updateFilter = (key: keyof ProductFilters, value: any) => {
|
const updateFilter = (key: keyof ProductFilters, value: unknown) => {
|
||||||
onFiltersChange({ ...filters, [key]: value });
|
onFiltersChange({ ...filters, [key]: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -244,8 +241,9 @@ export function FilterSidebar({
|
|||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(searchCategories.toLowerCase())
|
.includes(searchCategories.toLowerCase())
|
||||||
).map((category) => {
|
).map((category) => {
|
||||||
const IconComponent =
|
const IconComponent = LucideIcons[
|
||||||
LucideIcons[category.icon as keyof typeof LucideIcons];
|
category.icon as keyof typeof LucideIcons
|
||||||
|
] as React.ComponentType<{ className?: string }> | undefined;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={category.value}
|
key={category.value}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export function Footer() {
|
|||||||
<ul className="space-y-3">
|
<ul className="space-y-3">
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#como-funciona"
|
href="/#como-funciona"
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
>
|
>
|
||||||
Como funciona
|
Como funciona
|
||||||
@@ -49,7 +49,7 @@ export function Footer() {
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#recursos"
|
href="/#recursos"
|
||||||
className="text-gray-400 hover:text-white transition-colors"
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
>
|
>
|
||||||
Recursos
|
Recursos
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { Check, X, Edit, Trash2 } from "lucide-react";
|
||||||
|
import { toggleComprado } from "@/app/actions";
|
||||||
|
|
||||||
|
interface Produto {
|
||||||
|
id: number;
|
||||||
|
nome: string;
|
||||||
|
qtd: number;
|
||||||
|
tipoUN: string;
|
||||||
|
comprado: boolean;
|
||||||
|
listaId: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListaProductCardProps {
|
||||||
|
produto: Produto;
|
||||||
|
listaId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListaProductCard({ produto, listaId }: ListaProductCardProps) {
|
||||||
|
const handleToggleComprado = async () => {
|
||||||
|
try {
|
||||||
|
await toggleComprado(produto.id, listaId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao alterar status do produto:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// listaId é usado na função handleToggleComprado
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={`group relative overflow-hidden hover:shadow-lg transition-all duration-300 hover:-translate-y-1 ${
|
||||||
|
produto.comprado
|
||||||
|
? "bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800"
|
||||||
|
: "bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3
|
||||||
|
className={`font-semibold text-lg leading-tight ${
|
||||||
|
produto.comprado
|
||||||
|
? "text-green-700 dark:text-green-300 line-through"
|
||||||
|
: "text-gray-900 dark:text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{produto.nome}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-700"
|
||||||
|
>
|
||||||
|
{produto.qtd} {produto.tipoUN}
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
{produto.comprado && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 border-green-200 dark:border-green-700"
|
||||||
|
>
|
||||||
|
Comprado
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleToggleComprado}
|
||||||
|
variant={produto.comprado ? "outline" : "default"}
|
||||||
|
size="sm"
|
||||||
|
className={`flex-1 ${
|
||||||
|
produto.comprado
|
||||||
|
? "hover:bg-red-50 hover:text-red-700 hover:border-red-300"
|
||||||
|
: "hover:bg-green-50 hover:text-green-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{produto.comprado ? (
|
||||||
|
<>
|
||||||
|
<X className="w-4 h-4 mr-2" />
|
||||||
|
Desmarcar
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Check className="w-4 h-4 mr-2" />
|
||||||
|
Marcar como Comprado
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="px-3 hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="px-3 hover:bg-red-50 hover:text-red-700 hover:border-red-300"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,16 +4,9 @@ import { useRouter } from "next/navigation";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import Image from "next/image";
|
||||||
import { Product } from "@/lib/openFoodFactsApi";
|
import { Product } from "@/lib/openFoodFactsApi";
|
||||||
import {
|
import { Heart, AlertTriangle, Zap, Plus, Eye } from "lucide-react";
|
||||||
Heart,
|
|
||||||
AlertTriangle,
|
|
||||||
Zap,
|
|
||||||
Star,
|
|
||||||
Plus,
|
|
||||||
Eye,
|
|
||||||
TrendingUp,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { formatNutritionData } from "@/lib/openFoodFactsApi";
|
import { formatNutritionData } from "@/lib/openFoodFactsApi";
|
||||||
|
|
||||||
interface ProductCardProps {
|
interface ProductCardProps {
|
||||||
@@ -111,15 +104,19 @@ export function ProductCard({ product, className = "" }: ProductCardProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className={`group relative overflow-hidden hover:shadow-2xl transition-all duration-300 hover:-translate-y-2 bg-white dark:bg-gray-900 border border-gray-100 dark:border-gray-800 ${className}`}
|
className={`product-card group relative overflow-hidden hover:shadow-2xl transition-all duration-300 hover:-translate-y-2 bg-white dark:bg-gray-900 border border-gray-100 dark:border-gray-800 ${className}`}
|
||||||
>
|
>
|
||||||
{/* Imagem do Produto */}
|
{/* Imagem do Produto */}
|
||||||
<div className="relative h-56 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-700 overflow-hidden">
|
<div className="relative h-56 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-700 overflow-hidden">
|
||||||
<img
|
<Image
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt={product.product_name || "Produto"}
|
alt={product.product_name || "Produto"}
|
||||||
className="w-full h-full object-contain p-6 group-hover:scale-110 transition-transform duration-500"
|
fill
|
||||||
|
className="object-contain p-6 group-hover:scale-110 transition-transform duration-500"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||||
|
placeholder="blur"
|
||||||
|
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k="
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Overlay com badges */}
|
{/* Overlay com badges */}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
Zap,
|
Zap,
|
||||||
AlphabeticalOrder,
|
SortAsc,
|
||||||
Building2,
|
Building2,
|
||||||
Flame,
|
Flame,
|
||||||
Droplet,
|
Droplet,
|
||||||
@@ -23,13 +23,16 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { SORT_OPTIONS, SortOption } from "@/lib/types";
|
import { SORT_OPTIONS } from "@/lib/types";
|
||||||
|
|
||||||
// Mapear ícones para cada opção
|
// Mapear ícones para cada opção
|
||||||
const SORT_ICONS: Record<string, any> = {
|
const SORT_ICONS: Record<
|
||||||
|
string,
|
||||||
|
React.ComponentType<{ className?: string }>
|
||||||
|
> = {
|
||||||
relevance: TrendingUp,
|
relevance: TrendingUp,
|
||||||
nutrition_grade: Zap,
|
nutrition_grade: Zap,
|
||||||
name: AlphabeticalOrder,
|
name: SortAsc,
|
||||||
brand: Building2,
|
brand: Building2,
|
||||||
energy: Flame,
|
energy: Flame,
|
||||||
fat: Droplet,
|
fat: Droplet,
|
||||||
@@ -60,7 +63,7 @@ export function SortDropdown({
|
|||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={`flex items-center gap-2 ${className}`}
|
className={`flex items-center gap-2 border-gray-100 dark:border-gray-800 ${className}`}
|
||||||
>
|
>
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
Ordenar por: {selectedOption.label}
|
Ordenar por: {selectedOption.label}
|
||||||
@@ -69,7 +72,10 @@ export function SortDropdown({
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
<DropdownMenuContent align="end" className="w-72">
|
<DropdownMenuContent
|
||||||
|
align="end"
|
||||||
|
className="w-72 border-gray-100 dark:border-gray-800"
|
||||||
|
>
|
||||||
{SORT_OPTIONS.map((option) => {
|
{SORT_OPTIONS.map((option) => {
|
||||||
const IconComponent = SORT_ICONS[option.value];
|
const IconComponent = SORT_ICONS[option.value];
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { Sun, Moon, Menu, X, ScanLine, Info } from "lucide-react";
|
import { Sun, Moon, Menu, X, ScanLine } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
@@ -43,13 +43,13 @@ export function Header() {
|
|||||||
{/* Desktop Navigation */}
|
{/* Desktop Navigation */}
|
||||||
<nav className="hidden md:flex items-center gap-8">
|
<nav className="hidden md:flex items-center gap-8">
|
||||||
<a
|
<a
|
||||||
href="#como-funciona"
|
href="/"
|
||||||
className="text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
|
className="text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
|
||||||
>
|
>
|
||||||
Como funciona
|
Como funciona
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#recursos"
|
href="/#recursos"
|
||||||
className="text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
|
className="text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
|
||||||
>
|
>
|
||||||
Recursos
|
Recursos
|
||||||
@@ -108,14 +108,14 @@ export function Header() {
|
|||||||
<div className="md:hidden border-t border-gray-200 dark:border-gray-700 py-4">
|
<div className="md:hidden border-t border-gray-200 dark:border-gray-700 py-4">
|
||||||
<nav className="flex flex-col gap-4">
|
<nav className="flex flex-col gap-4">
|
||||||
<a
|
<a
|
||||||
href="#como-funciona"
|
href="/"
|
||||||
className="text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors py-2"
|
className="text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors py-2"
|
||||||
onClick={() => setIsMenuOpen(false)}
|
onClick={() => setIsMenuOpen(false)}
|
||||||
>
|
>
|
||||||
Como funciona
|
Como funciona
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#recursos"
|
href="/#recursos"
|
||||||
className="text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors py-2"
|
className="text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors py-2"
|
||||||
onClick={() => setIsMenuOpen(false)}
|
onClick={() => setIsMenuOpen(false)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,237 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
|
||||||
import { X, Camera, AlertCircle, CheckCircle } from "lucide-react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
|
|
||||||
interface BarcodeScannerProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onScan: (barcode: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BarcodeScanner({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
onScan,
|
|
||||||
}: BarcodeScannerProps) {
|
|
||||||
const [isScanning, setIsScanning] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [success, setSuccess] = useState(false);
|
|
||||||
const scannerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const qrCodeScanner = useRef<any>(null);
|
|
||||||
|
|
||||||
// Verificar se é mobile
|
|
||||||
const isMobile =
|
|
||||||
typeof window !== "undefined" &&
|
|
||||||
/iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
|
||||||
|
|
||||||
// Verificar se é HTTPS ou localhost
|
|
||||||
const isSecure =
|
|
||||||
typeof window !== "undefined" &&
|
|
||||||
(window.location.protocol === "https:" ||
|
|
||||||
window.location.hostname === "localhost");
|
|
||||||
|
|
||||||
const startScanner = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
setError(null);
|
|
||||||
setIsScanning(true);
|
|
||||||
|
|
||||||
// Dynamic import para carregar apenas quando necessário
|
|
||||||
const { Html5QrcodeScanner } = await import("html5-qrcode");
|
|
||||||
|
|
||||||
if (!scannerRef.current) return;
|
|
||||||
|
|
||||||
qrCodeScanner.current = new Html5QrcodeScanner(
|
|
||||||
scannerRef.current.id,
|
|
||||||
{
|
|
||||||
fps: 10,
|
|
||||||
qrbox: { width: 250, height: 250 },
|
|
||||||
aspectRatio: 1.0,
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
qrCodeScanner.current.render(
|
|
||||||
(decodedText: string) => {
|
|
||||||
// Validar se é um código de barras válido
|
|
||||||
if (isValidBarcode(decodedText)) {
|
|
||||||
setSuccess(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
onScan(decodedText);
|
|
||||||
onClose();
|
|
||||||
}, 1000);
|
|
||||||
} else {
|
|
||||||
setError("Código de barras inválido. Tente novamente.");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(error: string) => {
|
|
||||||
// Ignorar erros de "No QR code found" para não spam
|
|
||||||
if (!error.includes("No QR code found")) {
|
|
||||||
console.warn("Erro do scanner:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Erro ao iniciar scanner:", err);
|
|
||||||
setError("Erro ao acessar a câmera. Verifique as permissões.");
|
|
||||||
setIsScanning(false);
|
|
||||||
}
|
|
||||||
}, [onScan, onClose]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen && isMobile && isSecure) {
|
|
||||||
startScanner();
|
|
||||||
} else if (isOpen && !isMobile) {
|
|
||||||
setError("Scanner de câmera disponível apenas em dispositivos móveis");
|
|
||||||
} else if (isOpen && !isSecure) {
|
|
||||||
setError("Scanner de câmera requer conexão segura (HTTPS)");
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
stopScanner();
|
|
||||||
};
|
|
||||||
}, [isOpen, isMobile, isSecure, startScanner]);
|
|
||||||
|
|
||||||
const stopScanner = () => {
|
|
||||||
if (qrCodeScanner.current) {
|
|
||||||
qrCodeScanner.current.clear().catch((err: unknown) => {
|
|
||||||
console.warn("Erro ao parar scanner:", err);
|
|
||||||
});
|
|
||||||
qrCodeScanner.current = null;
|
|
||||||
}
|
|
||||||
setIsScanning(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isValidBarcode = (code: string): boolean => {
|
|
||||||
const cleanCode = code.replace(/\D/g, "");
|
|
||||||
return cleanCode.length >= 8 && cleanCode.length <= 13;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
stopScanner();
|
|
||||||
setError(null);
|
|
||||||
setSuccess(false);
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4">
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-2xl w-full max-w-md mx-auto overflow-hidden">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Camera className="w-5 h-5 text-blue-600" />
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
||||||
Scanner de Código de Barras
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleClose}
|
|
||||||
className="w-8 h-8 p-0"
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="p-4">
|
|
||||||
{!isMobile && (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<AlertCircle className="w-12 h-12 text-yellow-500 mx-auto mb-4" />
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
|
||||||
Scanner de câmera disponível apenas em dispositivos móveis
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
Use o campo de busca para digitar o código de barras manualmente
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isSecure && (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<AlertCircle className="w-12 h-12 text-red-500 mx-auto mb-4" />
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
|
||||||
Scanner de câmera requer conexão segura (HTTPS)
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
Use o campo de busca para digitar o código de barras manualmente
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isMobile && isSecure && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{error && (
|
|
||||||
<div className="flex items-center gap-2 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
|
||||||
<AlertCircle className="w-4 h-4 text-red-500 flex-shrink-0" />
|
|
||||||
<p className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{success && (
|
|
||||||
<div className="flex items-center gap-2 p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
|
||||||
<CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0" />
|
|
||||||
<p className="text-sm text-green-600 dark:text-green-400">
|
|
||||||
Código de barras detectado com sucesso!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-4">
|
|
||||||
Aponte a câmera para o código de barras do produto
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Scanner Container */}
|
|
||||||
<div
|
|
||||||
id="barcode-scanner"
|
|
||||||
ref={scannerRef}
|
|
||||||
className="w-full h-64 bg-gray-100 dark:bg-gray-700 rounded-lg flex items-center justify-center"
|
|
||||||
>
|
|
||||||
{!isScanning && !error && (
|
|
||||||
<div className="text-center">
|
|
||||||
<Camera className="w-8 h-8 text-gray-400 mx-auto mb-2" />
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
Iniciando câmera...
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
|
||||||
Mantenha o código de barras dentro da área destacada
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div className="flex gap-3 mt-6">
|
|
||||||
<Button variant="outline" onClick={handleClose} className="flex-1">
|
|
||||||
Cancelar
|
|
||||||
</Button>
|
|
||||||
{isMobile && isSecure && (
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setError(null);
|
|
||||||
setSuccess(false);
|
|
||||||
startScanner();
|
|
||||||
}}
|
|
||||||
className="flex-1"
|
|
||||||
disabled={isScanning}
|
|
||||||
>
|
|
||||||
{isScanning ? "Escaneando..." : "Tentar Novamente"}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Search, ScanLine, Sparkles, Camera, Globe } from "lucide-react";
|
import { Search, ScanLine, Sparkles, Globe } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { BarcodeScanner } from "./BarcodeScanner";
|
|
||||||
import { detectUserCountry } from "@/lib/geolocation";
|
import { detectUserCountry } from "@/lib/geolocation";
|
||||||
import { detectSearchType, isValidBarcode } from "@/lib/openFoodFactsApi";
|
import { detectSearchType, isValidBarcode } from "@/lib/openFoodFactsApi";
|
||||||
|
|
||||||
@@ -12,7 +11,6 @@ export function Hero() {
|
|||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [isScannerOpen, setIsScannerOpen] = useState(false);
|
|
||||||
const [userCountry, setUserCountry] = useState<string | null>(null);
|
const [userCountry, setUserCountry] = useState<string | null>(null);
|
||||||
const [searchType, setSearchType] = useState<"auto" | "barcode" | "name">(
|
const [searchType, setSearchType] = useState<"auto" | "barcode" | "name">(
|
||||||
"auto"
|
"auto"
|
||||||
@@ -29,9 +27,7 @@ export function Hero() {
|
|||||||
// Forçar nova detecção para garantir país correto
|
// Forçar nova detecção para garantir país correto
|
||||||
const country = await detectUserCountry(true);
|
const country = await detectUserCountry(true);
|
||||||
setUserCountry(country.code);
|
setUserCountry(country.code);
|
||||||
console.log("País detectado:", country.code, country.name);
|
} catch {
|
||||||
} catch (error) {
|
|
||||||
console.warn("Erro ao detectar país:", error);
|
|
||||||
setUserCountry("br"); // Fallback para Brasil
|
setUserCountry("br"); // Fallback para Brasil
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -54,7 +50,7 @@ export function Hero() {
|
|||||||
setError("");
|
setError("");
|
||||||
|
|
||||||
if (!searchQuery.trim()) {
|
if (!searchQuery.trim()) {
|
||||||
setError("Digite um código de barras ou nome do produto");
|
setError("Digite nome do produto");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,30 +65,15 @@ export function Hero() {
|
|||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
// Redirecionar para página do produto
|
// Redirecionar para página do produto
|
||||||
setTimeout(() => {
|
router.push(`/produto/${query}`);
|
||||||
router.push(`/produto/${query}`);
|
|
||||||
}, 1000);
|
|
||||||
} else {
|
} else {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
// Redirecionar para página de resultados
|
// Redirecionar para página de resultados
|
||||||
const countryParam = userCountry ? `&country=${userCountry}` : "";
|
const countryParam = userCountry ? `&country=${userCountry}` : "";
|
||||||
setTimeout(() => {
|
router.push(`/resultados?q=${encodeURIComponent(query)}${countryParam}`);
|
||||||
router.push(
|
|
||||||
`/resultados?q=${encodeURIComponent(query)}${countryParam}`
|
|
||||||
);
|
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleScannerResult = (barcode: string) => {
|
|
||||||
setSearchQuery(barcode);
|
|
||||||
setIsScannerOpen(false);
|
|
||||||
// Disparar busca automaticamente
|
|
||||||
setTimeout(() => {
|
|
||||||
router.push(`/produto/${barcode}`);
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPlaceholder = () => {
|
const getPlaceholder = () => {
|
||||||
if (searchType === "barcode") {
|
if (searchType === "barcode") {
|
||||||
return "Digite o código de barras (ex: 7896004713588)";
|
return "Digite o código de barras (ex: 7896004713588)";
|
||||||
@@ -110,13 +91,13 @@ export function Hero() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-white to-green-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 overflow-hidden">
|
<section className="relative min-h-[calc(100vh-4rem)] flex items-center justify-center bg-gradient-to-br from-blue-50 via-white to-green-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 overflow-hidden py-12 sm:py-16 lg:py-20">
|
||||||
{/* Background Pattern */}
|
{/* Background Pattern */}
|
||||||
<div className="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg%20width%3D%2260%22%20height%3D%2260%22%20viewBox%3D%220%200%2060%2060%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Cg%20fill%3D%22%230055ff%22%20fill-opacity%3D%220.05%22%3E%3Ccircle%20cx%3D%2230%22%20cy%3D%2230%22%20r%3D%222%22/%3E%3C/g%3E%3C/g%3E%3C/svg%3E')] opacity-40"></div>
|
<div className="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg%20width%3D%2260%22%20height%3D%2260%22%20viewBox%3D%220%200%2060%2060%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Cg%20fill%3D%22%230055ff%22%20fill-opacity%3D%220.05%22%3E%3Ccircle%20cx%3D%2230%22%20cy%3D%2230%22%20r%3D%222%22/%3E%3C/g%3E%3C/g%3E%3C/svg%3E')] opacity-40"></div>
|
||||||
|
|
||||||
<div className="relative z-10 max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
<div className="relative z-10 max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||||
{/* Badge */}
|
{/* Badge */}
|
||||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-sm font-medium mb-8">
|
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-sm font-medium mb-6 sm:mb-8">
|
||||||
<Sparkles className="w-4 h-4" />
|
<Sparkles className="w-4 h-4" />
|
||||||
Desenvolvido por{" "}
|
Desenvolvido por{" "}
|
||||||
<a
|
<a
|
||||||
@@ -131,7 +112,7 @@ export function Hero() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Heading */}
|
{/* Main Heading */}
|
||||||
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900 dark:text-white mb-6 leading-tight">
|
<h1 className="text-3xl sm:text-4xl lg:text-5xl xl:text-6xl font-bold text-gray-900 dark:text-white mb-4 sm:mb-6 leading-tight">
|
||||||
Descubra tudo sobre seus{" "}
|
Descubra tudo sobre seus{" "}
|
||||||
<span className="bg-gradient-to-r from-blue-600 to-green-600 bg-clip-text text-transparent">
|
<span className="bg-gradient-to-r from-blue-600 to-green-600 bg-clip-text text-transparent">
|
||||||
alimentos
|
alimentos
|
||||||
@@ -139,45 +120,37 @@ export function Hero() {
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* Subtitle */}
|
{/* Subtitle */}
|
||||||
<p className="text-xl sm:text-2xl text-gray-600 dark:text-gray-300 mb-12 max-w-3xl mx-auto leading-relaxed">
|
<p className="text-lg sm:text-xl lg:text-2xl text-gray-600 dark:text-gray-300 mb-8 sm:mb-12 max-w-3xl mx-auto leading-relaxed">
|
||||||
Consulte informações nutricionais completas, ingredientes e
|
Consulte informações nutricionais completas, ingredientes e
|
||||||
classificações de qualidade de milhares de produtos alimentícios com
|
classificações de qualidade de milhares de produtos alimentícios.
|
||||||
apenas um código de barras.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Search Form */}
|
{/* Search Form */}
|
||||||
<form onSubmit={handleSearch} className="max-w-2xl mx-auto mb-12">
|
<form
|
||||||
|
onSubmit={handleSearch}
|
||||||
|
className="max-w-2xl mx-auto mb-10 sm:mb-12"
|
||||||
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="flex items-center bg-white dark:bg-gray-800 rounded-2xl shadow-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
|
<div className="flex items-center bg-white dark:bg-gray-800 rounded-2xl shadow-xl overflow-hidden focus-within:ring-2 focus-within:ring-blue-500 transition-all duration-200">
|
||||||
<div className="flex-1 relative">
|
<div className="flex-1 relative">
|
||||||
{searchType === "barcode" ? (
|
{searchType === "barcode" ? (
|
||||||
<ScanLine className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
<ScanLine className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5 pointer-events-none" />
|
||||||
) : (
|
) : (
|
||||||
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5 pointer-events-none" />
|
||||||
)}
|
)}
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={getPlaceholder()}
|
placeholder={getPlaceholder()}
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="pl-12 pr-4 py-4 text-lg border-0 bg-transparent focus:ring-0 placeholder:text-gray-400"
|
className="pl-12 pr-4 py-4 text-lg border-0 bg-transparent focus:ring-0 focus:outline-none focus:border-transparent focus:shadow-none focus-visible:ring-0 placeholder:text-gray-400 w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scanner Button (Mobile only) */}
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsScannerOpen(true)}
|
|
||||||
className="m-2 px-3 py-4 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-600 dark:text-gray-300 rounded-xl transition-all duration-200"
|
|
||||||
title="Escanear código de barras"
|
|
||||||
>
|
|
||||||
<Camera className="w-5 h-5" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="m-2 px-8 py-4 bg-gradient-to-r from-blue-600 to-green-600 hover:from-blue-700 hover:to-green-700 text-white font-semibold rounded-xl transition-all duration-200 transform hover:scale-105 disabled:opacity-50"
|
className="m-2 px-6 sm:px-8 py-4 bg-gradient-to-r from-blue-600 to-green-600 hover:from-blue-700 hover:to-green-700 text-white font-semibold rounded-xl transition-all duration-200 transform hover:scale-105 disabled:opacity-50 flex-shrink-0"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -223,17 +196,10 @@ export function Hero() {
|
|||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* Barcode Scanner Modal */}
|
|
||||||
<BarcodeScanner
|
|
||||||
isOpen={isScannerOpen}
|
|
||||||
onClose={() => setIsScannerOpen(false)}
|
|
||||||
onScan={handleScannerResult}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Quick Stats */}
|
{/* Quick Stats */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-8 max-w-4xl mx-auto">
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6 sm:gap-8 max-w-4xl mx-auto">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400 mb-2">
|
<div className="text-2xl sm:text-3xl font-bold text-blue-600 dark:text-blue-400 mb-2">
|
||||||
2M+
|
2M+
|
||||||
</div>
|
</div>
|
||||||
<div className="text-gray-600 dark:text-gray-400">
|
<div className="text-gray-600 dark:text-gray-400">
|
||||||
@@ -241,7 +207,7 @@ export function Hero() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-3xl font-bold text-green-600 dark:text-green-400 mb-2">
|
<div className="text-2xl sm:text-3xl font-bold text-green-600 dark:text-green-400 mb-2">
|
||||||
100+
|
100+
|
||||||
</div>
|
</div>
|
||||||
<div className="text-gray-600 dark:text-gray-400">
|
<div className="text-gray-600 dark:text-gray-400">
|
||||||
@@ -249,7 +215,7 @@ export function Hero() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-3xl font-bold text-purple-600 dark:text-purple-400 mb-2">
|
<div className="text-2xl sm:text-3xl font-bold text-purple-600 dark:text-purple-400 mb-2">
|
||||||
24/7
|
24/7
|
||||||
</div>
|
</div>
|
||||||
<div className="text-gray-600 dark:text-gray-400">
|
<div className="text-gray-600 dark:text-gray-400">
|
||||||
@@ -260,7 +226,7 @@ export function Hero() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scroll Indicator */}
|
{/* Scroll Indicator */}
|
||||||
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 animate-bounce">
|
<div className="absolute bottom-4 sm:bottom-8 left-1/2 transform -translate-x-1/2 animate-bounce mt-12 sm:mt-16">
|
||||||
<div className="w-6 h-10 border-2 border-gray-400 dark:border-gray-500 rounded-full flex justify-center">
|
<div className="w-6 h-10 border-2 border-gray-400 dark:border-gray-500 rounded-full flex justify-center">
|
||||||
<div className="w-1 h-3 bg-gray-400 dark:bg-gray-500 rounded-full mt-2 animate-pulse"></div>
|
<div className="w-1 h-3 bg-gray-400 dark:bg-gray-500 rounded-full mt-2 animate-pulse"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import {
|
import { ScanLine, Search, BarChart3, CheckCircle } from "lucide-react";
|
||||||
ScanLine,
|
|
||||||
Search,
|
|
||||||
BarChart3,
|
|
||||||
CheckCircle,
|
|
||||||
ArrowRight,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
{
|
{
|
||||||
@@ -100,8 +94,22 @@ export function HowItWorks() {
|
|||||||
<div key={index} className="relative">
|
<div key={index} className="relative">
|
||||||
{/* Arrow (desktop only) */}
|
{/* Arrow (desktop only) */}
|
||||||
{index < steps.length - 1 && (
|
{index < steps.length - 1 && (
|
||||||
<div className="hidden lg:block absolute top-16 left-full w-8 h-0.5 bg-gray-300 dark:bg-gray-600 transform translate-x-4">
|
<div className="hidden lg:flex absolute top-1/2 -right-4 transform -translate-y-1/2 z-10">
|
||||||
<ArrowRight className="absolute -right-2 -top-1.5 w-3 h-3 text-gray-400" />
|
<svg
|
||||||
|
width="32"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 32 24"
|
||||||
|
fill="none"
|
||||||
|
className="text-gray-400 dark:text-gray-600"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0 12H30M30 12L20 2M30 12L20 22"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
// useState removido - não utilizado
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
interface BillingToggleProps {
|
interface BillingToggleProps {
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import { CheckCircle2, XCircle } from "lucide-react";
|
||||||
CheckCircle2,
|
|
||||||
XCircle,
|
|
||||||
Zap,
|
|
||||||
BarChart3,
|
|
||||||
GitCompare,
|
|
||||||
Download,
|
|
||||||
Code,
|
|
||||||
MessageSquare,
|
|
||||||
Shield,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||||
@@ -25,10 +15,22 @@ interface Feature {
|
|||||||
interface PricingCardProps {
|
interface PricingCardProps {
|
||||||
plan: "basic" | "premium" | "free";
|
plan: "basic" | "premium" | "free";
|
||||||
isAnnual?: boolean;
|
isAnnual?: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
isSelected?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const planData = {
|
const planData: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
monthlyPrice: number;
|
||||||
|
annualPrice: number;
|
||||||
|
description: string;
|
||||||
|
badge: string | null;
|
||||||
|
features: Feature[];
|
||||||
|
}
|
||||||
|
> = {
|
||||||
basic: {
|
basic: {
|
||||||
name: "Básico",
|
name: "Básico",
|
||||||
monthlyPrice: 5,
|
monthlyPrice: 5,
|
||||||
@@ -91,15 +93,7 @@ const planData = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const featureIcons: Record<string, any> = {
|
// featureIcons removido - não utilizado
|
||||||
consultas: Zap,
|
|
||||||
filtros: BarChart3,
|
|
||||||
comparação: GitCompare,
|
|
||||||
exportação: Download,
|
|
||||||
api: Code,
|
|
||||||
suporte: MessageSquare,
|
|
||||||
beta: Shield,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function PricingCard({
|
export function PricingCard({
|
||||||
plan,
|
plan,
|
||||||
@@ -111,16 +105,7 @@ export function PricingCard({
|
|||||||
const isPopular = plan === "premium";
|
const isPopular = plan === "premium";
|
||||||
const isFree = plan === "free";
|
const isFree = plan === "free";
|
||||||
|
|
||||||
const getFeatureIcon = (featureText: string) => {
|
// getFeatureIcon removido - não utilizado
|
||||||
if (featureText.includes("consultas") || featureText.includes("API"))
|
|
||||||
return Zap;
|
|
||||||
if (featureText.includes("filtros")) return BarChart3;
|
|
||||||
if (featureText.includes("comparação")) return GitCompare;
|
|
||||||
if (featureText.includes("exportação")) return Download;
|
|
||||||
if (featureText.includes("suporte")) return MessageSquare;
|
|
||||||
if (featureText.includes("Beta")) return Shield;
|
|
||||||
return CheckCircle2;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@@ -229,7 +214,6 @@ export function PricingCard({
|
|||||||
{/* Features */}
|
{/* Features */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{data.features.map((feature, index) => {
|
{data.features.map((feature, index) => {
|
||||||
const IconComponent = getFeatureIcon(feature.text);
|
|
||||||
return (
|
return (
|
||||||
<div key={index} className="flex items-start space-x-3">
|
<div key={index} className="flex items-start space-x-3">
|
||||||
<div className="flex-shrink-0 mt-0.5">
|
<div className="flex-shrink-0 mt-0.5">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ChevronDown } from "lucide-react";
|
// ChevronDown removido - não utilizado
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionContent,
|
AccordionContent,
|
||||||
|
|||||||
+37
-1
@@ -2,6 +2,42 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* Otimizações de performance para animações */
|
||||||
|
@layer utilities {
|
||||||
|
.animate-in {
|
||||||
|
animation: fadeInUp 0.6s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in-0 {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-in-from-bottom-4 {
|
||||||
|
transform: translateY(1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(1rem);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Otimização para listas grandes */
|
||||||
|
.product-grid {
|
||||||
|
contain: layout style paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card {
|
||||||
|
will-change: transform;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
@@ -69,4 +105,4 @@
|
|||||||
--input: 217 33% 25%;
|
--input: 217 33% 25%;
|
||||||
--ring: 215 90% 60%;
|
--ring: 215 90% 60%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+12
-8
@@ -4,14 +4,14 @@ import { useState } from "react";
|
|||||||
import { Header } from "@/app/components/header";
|
import { Header } from "@/app/components/header";
|
||||||
import { Footer } from "@/app/components/Footer";
|
import { Footer } from "@/app/components/Footer";
|
||||||
import { PricingHero } from "@/app/components/pricing/PricingHero";
|
import { PricingHero } from "@/app/components/pricing/PricingHero";
|
||||||
import { BillingToggle } from "@/app/components/pricing/BillingToggle";
|
// BillingToggle removido - não utilizado
|
||||||
import { PricingCard } from "@/app/components/pricing/PricingCard";
|
import { PricingCard } from "@/app/components/pricing/PricingCard";
|
||||||
import { PricingFAQ } from "@/app/components/pricing/PricingFAQ";
|
import { PricingFAQ } from "@/app/components/pricing/PricingFAQ";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ArrowRight, CheckCircle2 } from "lucide-react";
|
import { ArrowRight, CheckCircle2 } from "lucide-react";
|
||||||
|
|
||||||
export default function PricingPage() {
|
export default function PricingPage() {
|
||||||
const [isAnnual, setIsAnnual] = useState(false);
|
const [isAnnual] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white dark:bg-gray-900">
|
<div className="min-h-screen bg-white dark:bg-gray-900">
|
||||||
@@ -25,14 +25,15 @@ export default function PricingPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Billing Toggle */}
|
{/* Beta Message */}
|
||||||
<section className="pb-12">
|
<section className="pb-12">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<BillingToggle
|
<div className="text-center mb-8">
|
||||||
isAnnual={isAnnual}
|
<div className="inline-flex items-center gap-2 px-4 py-2 bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300 rounded-full text-sm font-medium">
|
||||||
onToggle={setIsAnnual}
|
<CheckCircle2 className="w-4 h-4" />
|
||||||
className="mb-8"
|
Atualmente em fase beta com acesso gratuito
|
||||||
/>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -43,16 +44,19 @@ export default function PricingPage() {
|
|||||||
<PricingCard
|
<PricingCard
|
||||||
plan="basic"
|
plan="basic"
|
||||||
isAnnual={isAnnual}
|
isAnnual={isAnnual}
|
||||||
|
isDisabled={true}
|
||||||
className="md:order-1"
|
className="md:order-1"
|
||||||
/>
|
/>
|
||||||
<PricingCard
|
<PricingCard
|
||||||
plan="premium"
|
plan="premium"
|
||||||
isAnnual={isAnnual}
|
isAnnual={isAnnual}
|
||||||
|
isDisabled={true}
|
||||||
className="md:order-2"
|
className="md:order-2"
|
||||||
/>
|
/>
|
||||||
<PricingCard
|
<PricingCard
|
||||||
plan="free"
|
plan="free"
|
||||||
isAnnual={isAnnual}
|
isAnnual={isAnnual}
|
||||||
|
isSelected={true}
|
||||||
className="md:order-3"
|
className="md:order-3"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { ArrowLeft, Loader2, AlertCircle, Globe, Star } from "lucide-react";
|
import { ArrowLeft, Loader2, AlertCircle, Star } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import Image from "next/image";
|
||||||
import {
|
import {
|
||||||
searchByBarcode,
|
searchByBarcode,
|
||||||
Product,
|
Product,
|
||||||
@@ -19,13 +20,7 @@ export default function ProdutoPage() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
const searchProduct = useCallback(async () => {
|
||||||
if (barcode) {
|
|
||||||
searchProduct();
|
|
||||||
}
|
|
||||||
}, [barcode]);
|
|
||||||
|
|
||||||
const searchProduct = async () => {
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -41,7 +36,13 @@ export default function ProdutoPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [barcode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (barcode) {
|
||||||
|
searchProduct();
|
||||||
|
}
|
||||||
|
}, [barcode, searchProduct]);
|
||||||
|
|
||||||
const getProductImageUrl = (product: Product): string | null => {
|
const getProductImageUrl = (product: Product): string | null => {
|
||||||
return (
|
return (
|
||||||
@@ -206,10 +207,15 @@ export default function ProdutoPage() {
|
|||||||
<div className="relative w-full max-w-2xl mx-auto bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-lg">
|
<div className="relative w-full max-w-2xl mx-auto bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-lg">
|
||||||
<div className="aspect-square bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-700 p-8">
|
<div className="aspect-square bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-700 p-8">
|
||||||
{imageUrl ? (
|
{imageUrl ? (
|
||||||
<img
|
<Image
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt={product.product_name}
|
alt={product.product_name || "Produto"}
|
||||||
className="w-full h-full object-contain"
|
fill
|
||||||
|
className="object-contain"
|
||||||
|
priority
|
||||||
|
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||||
|
placeholder="blur"
|
||||||
|
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k="
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full flex items-center justify-center text-gray-400">
|
<div className="w-full h-full flex items-center justify-center text-gray-400">
|
||||||
|
|||||||
+83
-82
@@ -1,11 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { useSearchParams, useRouter } from "next/navigation";
|
import { useSearchParams, useRouter } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
Search,
|
Search,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Loader2,
|
Loader2,
|
||||||
Filter,
|
|
||||||
Grid,
|
Grid,
|
||||||
List,
|
List,
|
||||||
SlidersHorizontal,
|
SlidersHorizontal,
|
||||||
@@ -53,8 +52,7 @@ export default function ResultadosPage() {
|
|||||||
try {
|
try {
|
||||||
const country = await detectUserCountry(true);
|
const country = await detectUserCountry(true);
|
||||||
setDetectedCountry(country.code);
|
setDetectedCountry(country.code);
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.warn("Erro ao detectar país:", error);
|
|
||||||
setDetectedCountry("br");
|
setDetectedCountry("br");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -62,88 +60,88 @@ export default function ResultadosPage() {
|
|||||||
detectCountry();
|
detectCountry();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const loadProducts = useCallback(
|
||||||
|
async (pageNum: number) => {
|
||||||
|
try {
|
||||||
|
if (pageNum === 1) {
|
||||||
|
setLoading(true);
|
||||||
|
} else {
|
||||||
|
setLoadingMore(true);
|
||||||
|
}
|
||||||
|
setError(null);
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const countryToUse = detectedCountry || countryParam || "br";
|
||||||
|
|
||||||
|
const result = await searchByName(query, countryToUse, pageNum, 20);
|
||||||
|
|
||||||
|
// Aplicar ordenação se especificada
|
||||||
|
let sortedProducts = result.products;
|
||||||
|
if (filters.sortBy) {
|
||||||
|
sortedProducts = sortProducts(
|
||||||
|
result.products,
|
||||||
|
filters.sortBy,
|
||||||
|
filters.sortOrder || "asc"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setProducts((prevProducts) =>
|
||||||
|
pageNum === 1 ? sortedProducts : [...prevProducts, ...sortedProducts]
|
||||||
|
);
|
||||||
|
setHasMore(result.products.length === 20);
|
||||||
|
setPage(pageNum);
|
||||||
|
setTotalResults(result.count || result.products.length);
|
||||||
|
} catch (err) {
|
||||||
|
setError(
|
||||||
|
err instanceof Error ? err.message : "Erro ao buscar produtos"
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
setLoadingMore(false);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
query,
|
||||||
|
detectedCountry,
|
||||||
|
countryParam,
|
||||||
|
filters.sortBy,
|
||||||
|
filters.sortOrder,
|
||||||
|
setIsLoading,
|
||||||
|
setTotalResults,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (query) {
|
if (query) {
|
||||||
loadProducts(1);
|
loadProducts(1);
|
||||||
}
|
}
|
||||||
}, [query, countryParam, detectedCountry, filters]);
|
}, [query, countryParam, detectedCountry, loadProducts]);
|
||||||
|
|
||||||
const loadProducts = async (pageNum: number) => {
|
const handleLoadMore = useCallback(() => {
|
||||||
try {
|
|
||||||
if (pageNum === 1) {
|
|
||||||
setLoading(true);
|
|
||||||
} else {
|
|
||||||
setLoadingMore(true);
|
|
||||||
}
|
|
||||||
setError(null);
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
const countryToUse = detectedCountry || countryParam || "br";
|
|
||||||
|
|
||||||
// Construir parâmetros de busca com filtros
|
|
||||||
const searchParams = new URLSearchParams({
|
|
||||||
q: query,
|
|
||||||
country: countryToUse,
|
|
||||||
page: pageNum.toString(),
|
|
||||||
page_size: "20",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Adicionar filtros à URL
|
|
||||||
Object.entries(filters).forEach(([key, value]) => {
|
|
||||||
if (value !== undefined && value !== null && value !== "") {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
if (value.length > 0) {
|
|
||||||
searchParams.set(key, value.join(","));
|
|
||||||
}
|
|
||||||
} else if (typeof value === "object" && value !== null) {
|
|
||||||
Object.entries(value).forEach(([rangeKey, rangeValue]) => {
|
|
||||||
if (rangeValue?.min !== undefined) {
|
|
||||||
searchParams.set(`${rangeKey}_min`, rangeValue.min.toString());
|
|
||||||
}
|
|
||||||
if (rangeValue?.max !== undefined) {
|
|
||||||
searchParams.set(`${rangeKey}_max`, rangeValue.max.toString());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
searchParams.set(key, value.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await searchByName(query, countryToUse, pageNum, 20);
|
|
||||||
|
|
||||||
// Aplicar ordenação se especificada
|
|
||||||
let sortedProducts = result.products;
|
|
||||||
if (filters.sortBy) {
|
|
||||||
sortedProducts = sortProducts(
|
|
||||||
result.products,
|
|
||||||
filters.sortBy,
|
|
||||||
filters.sortOrder || "asc"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setProducts(
|
|
||||||
pageNum === 1 ? sortedProducts : [...products, ...sortedProducts]
|
|
||||||
);
|
|
||||||
setHasMore(result.products.length === 20);
|
|
||||||
setPage(pageNum);
|
|
||||||
setTotalResults(result.count || result.products.length);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : "Erro ao buscar produtos");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
setLoadingMore(false);
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLoadMore = () => {
|
|
||||||
loadProducts(page + 1);
|
loadProducts(page + 1);
|
||||||
};
|
}, [loadProducts, page]);
|
||||||
|
|
||||||
const handleSortChange = (sortBy: string) => {
|
const handleSortChange = useCallback(
|
||||||
updateFilters({ sortBy: sortBy as any });
|
(sortBy: string) => {
|
||||||
};
|
updateFilters({
|
||||||
|
sortBy: sortBy as
|
||||||
|
| "relevance"
|
||||||
|
| "nutrition_grade"
|
||||||
|
| "name"
|
||||||
|
| "brand"
|
||||||
|
| "energy"
|
||||||
|
| "fat"
|
||||||
|
| "sugars"
|
||||||
|
| "completeness"
|
||||||
|
| "with_images"
|
||||||
|
| "with_nutrition"
|
||||||
|
| "eco_score"
|
||||||
|
| "nova_group",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[updateFilters]
|
||||||
|
);
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return (
|
return (
|
||||||
@@ -181,7 +179,7 @@ export default function ResultadosPage() {
|
|||||||
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
Resultados para "{query}"
|
Resultados para "{query}"
|
||||||
</h1>
|
</h1>
|
||||||
{!loading && products.length > 0 && (
|
{!loading && products.length > 0 && (
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
@@ -342,7 +340,7 @@ export default function ResultadosPage() {
|
|||||||
{!loading && products.length > 0 && (
|
{!loading && products.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`grid gap-6 mb-8 ${
|
className={`product-grid grid gap-6 mb-8 ${
|
||||||
viewMode === "grid"
|
viewMode === "grid"
|
||||||
? "grid-cols-1 sm:grid-cols-2 xl:grid-cols-3"
|
? "grid-cols-1 sm:grid-cols-2 xl:grid-cols-3"
|
||||||
: "grid-cols-1"
|
: "grid-cols-1"
|
||||||
@@ -352,7 +350,10 @@ export default function ResultadosPage() {
|
|||||||
<div
|
<div
|
||||||
key={product.code}
|
key={product.code}
|
||||||
className="animate-in fade-in-0 slide-in-from-bottom-4"
|
className="animate-in fade-in-0 slide-in-from-bottom-4"
|
||||||
style={{ animationDelay: `${index * 50}ms` }}
|
style={{
|
||||||
|
animationDelay: `${Math.min(index * 50, 500)}ms`,
|
||||||
|
animationFillMode: "both",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ProductCard product={product} />
|
<ProductCard product={product} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Avatar = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AvatarPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
|
||||||
|
|
||||||
const AvatarImage = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AvatarPrimitive.Image
|
|
||||||
ref={ref}
|
|
||||||
className={cn("aspect-square h-full w-full", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
|
||||||
|
|
||||||
const AvatarFallback = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AvatarPrimitive.Fallback
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
|
||||||
|
|
||||||
export { Avatar, AvatarImage, AvatarFallback }
|
|
||||||
+63
-56
@@ -42,68 +42,75 @@ export function useProductFilters(options: UseProductFiltersOptions = {}) {
|
|||||||
}
|
}
|
||||||
}, [filters, persistToLocalStorage, localStorageKey]);
|
}, [filters, persistToLocalStorage, localStorageKey]);
|
||||||
|
|
||||||
// Sincronizar filtros com URL
|
// Sincronizar filtros com URL (debounced)
|
||||||
useEffect(() => {
|
const debouncedSyncURL = useCallback(
|
||||||
const url = new URL(window.location.href);
|
debounce((filters: ProductFilters) => {
|
||||||
const currentParams = new URLSearchParams(url.search);
|
const url = new URL(window.location.href);
|
||||||
|
const currentParams = new URLSearchParams(url.search);
|
||||||
|
|
||||||
// Remover parâmetros de filtro existentes
|
// Remover parâmetros de filtro existentes
|
||||||
const filterParams = [
|
const filterParams = [
|
||||||
"nutrition_grades",
|
"nutrition_grades",
|
||||||
"nova_groups",
|
"nova_groups",
|
||||||
"categories",
|
"categories",
|
||||||
"brands",
|
"brands",
|
||||||
"allergens",
|
"allergens",
|
||||||
"exclude_allergens",
|
"exclude_allergens",
|
||||||
"additives",
|
"additives",
|
||||||
"exclude_additives",
|
"exclude_additives",
|
||||||
"no_additives",
|
"no_additives",
|
||||||
"countries",
|
"countries",
|
||||||
"sort_by",
|
"sort_by",
|
||||||
"sort_order",
|
"sort_order",
|
||||||
"energy_min",
|
"energy_min",
|
||||||
"energy_max",
|
"energy_max",
|
||||||
"fat_min",
|
"fat_min",
|
||||||
"fat_max",
|
"fat_max",
|
||||||
"sugars_min",
|
"sugars_min",
|
||||||
"sugars_max",
|
"sugars_max",
|
||||||
"sodium_min",
|
"sodium_min",
|
||||||
"sodium_max",
|
"sodium_max",
|
||||||
];
|
];
|
||||||
|
|
||||||
filterParams.forEach((param) => {
|
filterParams.forEach((param) => {
|
||||||
currentParams.delete(param);
|
currentParams.delete(param);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Adicionar novos parâmetros de filtro
|
// Adicionar novos parâmetros de filtro
|
||||||
Object.entries(filters).forEach(([key, value]) => {
|
Object.entries(filters).forEach(([key, value]) => {
|
||||||
if (value !== undefined && value !== null && value !== "") {
|
if (value !== undefined && value !== null && value !== "") {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
if (value.length > 0) {
|
if (value.length > 0) {
|
||||||
currentParams.set(key, value.join(","));
|
currentParams.set(key, value.join(","));
|
||||||
|
}
|
||||||
|
} else if (typeof value === "object" && value !== null) {
|
||||||
|
// Para nutritionRanges
|
||||||
|
Object.entries(value).forEach(([rangeKey, rangeValue]) => {
|
||||||
|
if (rangeValue?.min !== undefined) {
|
||||||
|
currentParams.set(`${rangeKey}_min`, rangeValue.min.toString());
|
||||||
|
}
|
||||||
|
if (rangeValue?.max !== undefined) {
|
||||||
|
currentParams.set(`${rangeKey}_max`, rangeValue.max.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
currentParams.set(key, value.toString());
|
||||||
}
|
}
|
||||||
} else if (typeof value === "object" && value !== null) {
|
|
||||||
// Para nutritionRanges
|
|
||||||
Object.entries(value).forEach(([rangeKey, rangeValue]) => {
|
|
||||||
if (rangeValue?.min !== undefined) {
|
|
||||||
currentParams.set(`${rangeKey}_min`, rangeValue.min.toString());
|
|
||||||
}
|
|
||||||
if (rangeValue?.max !== undefined) {
|
|
||||||
currentParams.set(`${rangeKey}_max`, rangeValue.max.toString());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
currentParams.set(key, value.toString());
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Atualizar URL sem recarregar a página
|
// Atualizar URL sem recarregar a página
|
||||||
const newUrl = `${url.pathname}?${currentParams.toString()}`;
|
const newUrl = `${url.pathname}?${currentParams.toString()}`;
|
||||||
if (newUrl !== window.location.pathname + window.location.search) {
|
if (newUrl !== window.location.pathname + window.location.search) {
|
||||||
router.replace(newUrl, { scroll: false });
|
router.replace(newUrl, { scroll: false });
|
||||||
}
|
}
|
||||||
}, [filters, router]);
|
}, 500),
|
||||||
|
[router]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
debouncedSyncURL(filters);
|
||||||
|
}, [filters, debouncedSyncURL]);
|
||||||
|
|
||||||
// Debounced update dos filtros
|
// Debounced update dos filtros
|
||||||
const debouncedUpdateFilters = useCallback(
|
const debouncedUpdateFilters = useCallback(
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ interface CountryInfo {
|
|||||||
|
|
||||||
// Cache para evitar múltiplas requisições
|
// Cache para evitar múltiplas requisições
|
||||||
const CACHE_KEY = "nutriscan_user_country";
|
const CACHE_KEY = "nutriscan_user_country";
|
||||||
const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 horas
|
const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 dias
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifica se o cache ainda é válido
|
* Verifica se o cache ainda é válido
|
||||||
|
|||||||
+9
-1
@@ -41,8 +41,16 @@ export interface ProductFilters {
|
|||||||
| "brand"
|
| "brand"
|
||||||
| "energy"
|
| "energy"
|
||||||
| "fat"
|
| "fat"
|
||||||
| "sugars";
|
| "sugars"
|
||||||
|
| "completeness"
|
||||||
|
| "with_images"
|
||||||
|
| "with_nutrition"
|
||||||
|
| "eco_score"
|
||||||
|
| "nova_group";
|
||||||
sortOrder?: "asc" | "desc";
|
sortOrder?: "asc" | "desc";
|
||||||
|
|
||||||
|
// Assinatura de índice para compatibilidade
|
||||||
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FilterOption {
|
export interface FilterOption {
|
||||||
|
|||||||
+54
-1
@@ -1,4 +1,57 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {};
|
const nextConfig = {
|
||||||
|
// Otimizações de performance
|
||||||
|
swcMinify: true,
|
||||||
|
compress: true,
|
||||||
|
|
||||||
|
// Configurações de imagens
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "static.openfoodfacts.org",
|
||||||
|
port: "",
|
||||||
|
pathname: "/images/products/**",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "images.openfoodfacts.org",
|
||||||
|
port: "",
|
||||||
|
pathname: "/images/products/**",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
formats: ["image/webp", "image/avif"],
|
||||||
|
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
||||||
|
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Headers de segurança e performance
|
||||||
|
async headers() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: "/(.*)",
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: "X-DNS-Prefetch-Control",
|
||||||
|
value: "on",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "X-Frame-Options",
|
||||||
|
value: "DENY",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "X-Content-Type-Options",
|
||||||
|
value: "nosniff",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
// Otimizações de bundle
|
||||||
|
experimental: {
|
||||||
|
optimizeCss: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
"@prisma/client": "^5.22.0",
|
"@prisma/client": "^5.22.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.12",
|
"@radix-ui/react-accordion": "^1.2.12",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.2",
|
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.2",
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||||
@@ -29,13 +28,11 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^11.12.0",
|
"framer-motion": "^11.12.0",
|
||||||
"html5-qrcode": "^2.3.8",
|
|
||||||
"lucide-react": "^0.460.0",
|
"lucide-react": "^0.460.0",
|
||||||
"next": "14.2.18",
|
"next": "14.2.18",
|
||||||
"next-themes": "^0.4.3",
|
"next-themes": "^0.4.3",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-icons": "^5.5.0",
|
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+9
-75
@@ -17,9 +17,6 @@ importers:
|
|||||||
'@radix-ui/react-alert-dialog':
|
'@radix-ui/react-alert-dialog':
|
||||||
specifier: ^1.1.2
|
specifier: ^1.1.2
|
||||||
version: 1.1.2(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
version: 1.1.2(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||||
'@radix-ui/react-avatar':
|
|
||||||
specifier: ^1.1.10
|
|
||||||
version: 1.1.10(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
|
||||||
'@radix-ui/react-checkbox':
|
'@radix-ui/react-checkbox':
|
||||||
specifier: ^1.3.3
|
specifier: ^1.3.3
|
||||||
version: 1.3.3(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
version: 1.3.3(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||||
@@ -74,9 +71,6 @@ importers:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^18.3.1
|
specifier: ^18.3.1
|
||||||
version: 18.3.1([email protected])
|
version: 18.3.1([email protected])
|
||||||
react-icons:
|
|
||||||
specifier: ^5.5.0
|
|
||||||
version: 5.5.0([email protected])
|
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^2.5.5
|
specifier: ^2.5.5
|
||||||
version: 2.5.5
|
version: 2.5.5
|
||||||
@@ -377,19 +371,6 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/[email protected]':
|
|
||||||
resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '*'
|
|
||||||
'@types/react-dom': '*'
|
|
||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
'@types/react-dom':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@radix-ui/[email protected]':
|
'@radix-ui/[email protected]':
|
||||||
resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==}
|
resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -838,15 +819,6 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/[email protected]':
|
|
||||||
resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '*'
|
|
||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@radix-ui/[email protected]':
|
'@radix-ui/[email protected]':
|
||||||
resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
|
resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2193,11 +2165,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.3.1
|
react: ^18.3.1
|
||||||
|
|
||||||
[email protected]:
|
|
||||||
resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==}
|
|
||||||
peerDependencies:
|
|
||||||
react: '*'
|
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
|
|
||||||
@@ -2526,11 +2493,6 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
[email protected]:
|
|
||||||
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
|
|
||||||
peerDependencies:
|
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
@@ -2848,19 +2810,6 @@ snapshots:
|
|||||||
'@types/react': 18.3.12
|
'@types/react': 18.3.12
|
||||||
'@types/react-dom': 18.3.1
|
'@types/react-dom': 18.3.1
|
||||||
|
|
||||||
'@radix-ui/[email protected](@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])':
|
|
||||||
dependencies:
|
|
||||||
'@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
|
|
||||||
'@radix-ui/react-primitive': 2.1.3(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/[email protected])([email protected])
|
|
||||||
'@radix-ui/react-use-is-hydrated': 0.1.0(@types/[email protected])([email protected])
|
|
||||||
'@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
|
|
||||||
react: 18.3.1
|
|
||||||
react-dom: 18.3.1([email protected])
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 18.3.12
|
|
||||||
'@types/react-dom': 18.3.1
|
|
||||||
|
|
||||||
'@radix-ui/[email protected](@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])':
|
'@radix-ui/[email protected](@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.3
|
'@radix-ui/primitive': 1.1.3
|
||||||
@@ -3311,13 +3260,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.3.12
|
'@types/react': 18.3.12
|
||||||
|
|
||||||
'@radix-ui/[email protected](@types/[email protected])([email protected])':
|
|
||||||
dependencies:
|
|
||||||
react: 18.3.1
|
|
||||||
use-sync-external-store: 1.6.0([email protected])
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/react': 18.3.12
|
|
||||||
|
|
||||||
'@radix-ui/[email protected](@types/[email protected])([email protected])':
|
'@radix-ui/[email protected](@types/[email protected])([email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
@@ -3955,8 +3897,8 @@ snapshots:
|
|||||||
'@typescript-eslint/parser': 8.16.0([email protected])([email protected])
|
'@typescript-eslint/parser': 8.16.0([email protected])([email protected])
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected])([email protected])
|
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected]))([email protected])
|
||||||
eslint-plugin-import: 2.31.0(@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected])
|
eslint-plugin-import: 2.31.0(@typescript-eslint/[email protected]([email protected])([email protected]))([email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected]))([email protected]))([email protected])
|
||||||
eslint-plugin-jsx-a11y: 6.10.2([email protected])
|
eslint-plugin-jsx-a11y: 6.10.2([email protected])
|
||||||
eslint-plugin-react: 7.37.2([email protected])
|
eslint-plugin-react: 7.37.2([email protected])
|
||||||
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705([email protected])
|
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705([email protected])
|
||||||
@@ -3975,37 +3917,37 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
[email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected])([email protected]):
|
[email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected]))([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nolyfill/is-core-module': 1.0.39
|
'@nolyfill/is-core-module': 1.0.39
|
||||||
debug: 4.3.7
|
debug: 4.3.7
|
||||||
enhanced-resolve: 5.17.1
|
enhanced-resolve: 5.17.1
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-module-utils: 2.12.0(@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected])([email protected])
|
eslint-module-utils: 2.12.0(@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected]))([email protected]))([email protected])
|
||||||
fast-glob: 3.3.2
|
fast-glob: 3.3.2
|
||||||
get-tsconfig: 4.8.1
|
get-tsconfig: 4.8.1
|
||||||
is-bun-module: 1.3.0
|
is-bun-module: 1.3.0
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
eslint-plugin-import: 2.31.0(@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected])
|
eslint-plugin-import: 2.31.0(@typescript-eslint/[email protected]([email protected])([email protected]))([email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected]))([email protected]))([email protected])
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@typescript-eslint/parser'
|
- '@typescript-eslint/parser'
|
||||||
- eslint-import-resolver-node
|
- eslint-import-resolver-node
|
||||||
- eslint-import-resolver-webpack
|
- eslint-import-resolver-webpack
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
[email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected])([email protected]):
|
[email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected]))([email protected]))([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/parser': 8.16.0([email protected])([email protected])
|
'@typescript-eslint/parser': 8.16.0([email protected])([email protected])
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected])([email protected])
|
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected]))([email protected])
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
[email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected]):
|
[email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected]))([email protected]))([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rtsao/scc': 1.1.0
|
'@rtsao/scc': 1.1.0
|
||||||
array-includes: 3.1.8
|
array-includes: 3.1.8
|
||||||
@@ -4016,7 +3958,7 @@ snapshots:
|
|||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-module-utils: 2.12.0(@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected])([email protected])
|
eslint-module-utils: 2.12.0(@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected]))([email protected]))([email protected])
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
is-core-module: 2.15.1
|
is-core-module: 2.15.1
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
@@ -4834,10 +4776,6 @@ snapshots:
|
|||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
scheduler: 0.23.2
|
scheduler: 0.23.2
|
||||||
|
|
||||||
[email protected]([email protected]):
|
|
||||||
dependencies:
|
|
||||||
react: 18.3.1
|
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected](@types/[email protected])([email protected]):
|
[email protected](@types/[email protected])([email protected]):
|
||||||
@@ -5217,10 +5155,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.3.12
|
'@types/react': 18.3.12
|
||||||
|
|
||||||
[email protected]([email protected]):
|
|
||||||
dependencies:
|
|
||||||
react: 18.3.1
|
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
|
|||||||
Reference in new issue
Block a user