feat(api): implement product caching mechanism

- Add NODE_ENV=production to .env for environment configuration
- Enhance product API route with JSON cache manager for local storage and retrieval
- Introduce cache-only mode and detailed logging for better performance and offline support
- Update UI to display cache status and storage statistics
This commit is contained in:
Evandro Fonseca Junior committed 2025-10-09 04:30:46 -03:00
1 parent bddf19e5c8
commit c173fec668
19 files changed
+9240 -175

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
"use client";
import { useState, useEffect } from "react";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { Database, Globe } from "lucide-react";
export function CacheOnlySwitch() {
const [cacheOnly, setCacheOnly] = useState(false);
const isProd = process.env.NODE_ENV === "production";
// Carregar preferência do localStorage
useEffect(() => {
const saved = localStorage.getItem("cache_only_mode");
if (saved) {
setCacheOnly(saved === "true");
}
}, []);
// Salvar preferência
const handleToggle = (checked: boolean) => {
setCacheOnly(checked);
localStorage.setItem("cache_only_mode", checked.toString());
};
// Só exibir em produção
if (!isProd) return null;
return (
<div className="flex items-center gap-2 p-3 bg-gray-100 dark:bg-gray-800 rounded-lg">
{cacheOnly ? (
<Database className="w-4 h-4 text-blue-600" />
) : (
<Globe className="w-4 h-4 text-green-600" />
)}
<Label htmlFor="cache-only" className="cursor-pointer">
{cacheOnly ? "Modo Cache Local" : "Modo Online"}
</Label>
<Switch
id="cache-only"
checked={cacheOnly}
onCheckedChange={handleToggle}
/>
</div>
);
}
+142 -6
View File
@@ -1,7 +1,20 @@
"use client";
import { useState } from "react";
import { Filter, X, AlertTriangle, Zap } from "lucide-react";
import {
Filter,
X,
AlertTriangle,
Zap,
Grid,
List,
RefreshCw,
ArrowUpDown,
LayoutGrid,
Tag,
Beaker,
BarChart3,
} from "lucide-react";
import * as LucideIcons from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -17,6 +30,8 @@ import {
} from "@/components/ui/accordion";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { SortDropdown } from "@/app/components/SortDropdown";
import { FilterBadges } from "@/app/components/FilterBadge";
import {
ProductFilters,
NUTRITION_GRADES,
@@ -32,6 +47,14 @@ interface FilterSidebarProps {
isLoading?: boolean;
totalResults?: number;
className?: string;
sortBy?: string;
onSortChange?: (sortBy: string) => void;
viewMode?: "grid" | "list";
onViewModeChange?: (mode: "grid" | "list") => void;
onRefresh?: () => void;
onClearCache?: () => void;
isCached?: boolean;
onRemoveFilter?: (key: string, value?: string) => void;
}
export function FilterSidebar({
@@ -41,6 +64,14 @@ export function FilterSidebar({
isLoading = false,
totalResults = 0,
className = "",
sortBy = "relevance",
onSortChange,
viewMode = "grid",
onViewModeChange,
onRefresh,
onClearCache,
isCached = false,
onRemoveFilter,
}: FilterSidebarProps) {
const [searchBrands, setSearchBrands] = useState("");
const [searchCategories, setSearchCategories] = useState("");
@@ -117,11 +148,116 @@ export function FilterSidebar({
</div>
)}
{/* Controles de Visualização e Cache */}
<div className="space-y-4">
{/* Modo de Visualização */}
{onViewModeChange && (
<div className="space-y-2">
<div className="text-sm font-medium">Modo de visualização</div>
<div className="flex items-center border border-gray-100 dark:border-gray-800 rounded-lg">
<Button
variant={viewMode === "grid" ? "default" : "ghost"}
size="sm"
onClick={() => onViewModeChange("grid")}
className="rounded-r-none flex-1"
>
<Grid className="w-4 h-4 mr-2" />
Grade
</Button>
<Button
variant={viewMode === "list" ? "default" : "ghost"}
size="sm"
onClick={() => onViewModeChange("list")}
className="rounded-l-none flex-1"
>
<List className="w-4 h-4 mr-2" />
Lista
</Button>
</div>
</div>
)}
{/* Controles de Cache */}
{(onRefresh || onClearCache) && (
<div className="space-y-2">
<div className="text-sm font-medium">Controles</div>
<div className="flex gap-2">
{onRefresh && (
<Button
variant="outline"
size="sm"
onClick={onRefresh}
disabled={isLoading}
className="flex-1"
title="Atualizar busca"
>
<RefreshCw
className={`w-4 h-4 mr-2 ${
isLoading ? "animate-spin" : ""
}`}
/>
Atualizar
</Button>
)}
{onClearCache && isCached && (
<Button
variant="outline"
size="sm"
onClick={onClearCache}
className="flex-1"
title="Limpar cache e refazer busca"
>
<X className="w-4 h-4 mr-2" />
Limpar Cache
</Button>
)}
</div>
</div>
)}
</div>
{/* Filtros Ativos */}
{onRemoveFilter && hasActiveFilters && (
<div className="space-y-2">
<div className="text-sm font-medium">Filtros ativos</div>
<FilterBadges
filters={filters}
onRemoveFilter={onRemoveFilter}
onClearAll={onClearFilters}
/>
</div>
)}
<Accordion
type="multiple"
defaultValue={["nutrition", "categories"]}
defaultValue={["sorting"]}
className="space-y-4"
>
{/* Ordenação */}
<AccordionItem
value="sorting"
className="border border-gray-100 dark:border-gray-800 rounded-lg"
>
<AccordionTrigger className="px-4 py-3 hover:no-underline">
<div className="flex items-center gap-2">
<ArrowUpDown className="h-4 w-4 text-indigo-500" />
<span className="font-medium">Ordenação</span>
</div>
</AccordionTrigger>
<AccordionContent className="px-4 pb-4">
<div className="space-y-3">
<div className="text-sm font-medium mb-3">Ordenar por</div>
{onSortChange && (
<SortDropdown
value={sortBy}
onValueChange={onSortChange}
className="w-full"
/>
)}
</div>
</AccordionContent>
</AccordionItem>
{/* Nutri-Score */}
<AccordionItem
value="nutrition"
@@ -219,7 +355,7 @@ export function FilterSidebar({
>
<AccordionTrigger className="px-4 py-3 hover:no-underline">
<div className="flex items-center gap-2">
<div className="h-4 w-4 rounded bg-blue-500" />
<LayoutGrid className="h-4 w-4 text-blue-500" />
<span className="font-medium">Categorias</span>
</div>
</AccordionTrigger>
@@ -286,7 +422,7 @@ export function FilterSidebar({
>
<AccordionTrigger className="px-4 py-3 hover:no-underline">
<div className="flex items-center gap-2">
<div className="h-4 w-4 rounded bg-purple-500" />
<Tag className="h-4 w-4 text-purple-500" />
<span className="font-medium">Marcas</span>
</div>
</AccordionTrigger>
@@ -360,7 +496,7 @@ export function FilterSidebar({
>
<AccordionTrigger className="px-4 py-3 hover:no-underline">
<div className="flex items-center gap-2">
<div className="h-4 w-4 rounded bg-orange-500" />
<Beaker className="h-4 w-4 text-orange-500" />
<span className="font-medium">Aditivos</span>
</div>
</AccordionTrigger>
@@ -393,7 +529,7 @@ export function FilterSidebar({
>
<AccordionTrigger className="px-4 py-3 hover:no-underline">
<div className="flex items-center gap-2">
<div className="h-4 w-4 rounded bg-green-500" />
<BarChart3 className="h-4 w-4 text-green-500" />
<span className="font-medium">Valores Nutricionais</span>
</div>
</AccordionTrigger>
+58
View File
@@ -0,0 +1,58 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Search } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
interface SearchBarProps {
initialQuery?: string;
className?: string;
placeholder?: string;
}
export function SearchBar({
initialQuery = "",
className = "",
placeholder = "Buscar produtos...",
}: SearchBarProps) {
const [query, setQuery] = useState(initialQuery);
const router = useRouter();
const handleSearch = () => {
if (query.trim()) {
router.push(`/resultados?q=${encodeURIComponent(query.trim())}`);
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
handleSearch();
}
};
return (
<div className={`flex items-center gap-2 ${className}`}>
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
type="text"
placeholder={placeholder}
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyPress={handleKeyPress}
className="pl-10 pr-4 h-10"
/>
</div>
<Button
onClick={handleSearch}
disabled={!query.trim()}
className="h-10 px-4"
>
<Search className="h-4 w-4 mr-2" />
Buscar
</Button>
</div>
);
}
+41 -34
View File
@@ -48,12 +48,14 @@ interface SortDropdownProps {
value: string;
onValueChange: (value: string) => void;
className?: string;
align?: "start" | "center" | "end";
}
export function SortDropdown({
value,
onValueChange,
className = "",
align = "start",
}: SortDropdownProps) {
const selectedOption =
SORT_OPTIONS.find((option) => option.value === value) || SORT_OPTIONS[0];
@@ -73,43 +75,48 @@ export function SortDropdown({
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-72 border-gray-100 dark:border-gray-800"
align={align}
side="bottom"
sideOffset={4}
avoidCollisions={false}
className="w-64 max-w-[calc(100vw-2rem)] border-gray-100 dark:border-gray-800 z-50 p-0"
>
{SORT_OPTIONS.map((option) => {
const IconComponent = SORT_ICONS[option.value];
return (
<DropdownMenuItem
key={option.value}
onClick={() => onValueChange(option.value)}
className={`flex items-start gap-3 p-3 ${
value === option.value ? "bg-accent" : ""
}`}
>
{/* Ícone */}
<div className="mt-0.5">
{IconComponent && (
<IconComponent className="w-4 h-4 text-muted-foreground" />
)}
</div>
{/* Conteúdo */}
<div className="flex-1">
<div className="flex items-center justify-between">
<span className="font-medium">{option.label}</span>
{value === option.value && (
<Check className="w-4 h-4 text-primary" />
<div className="max-h-80 overflow-y-auto">
{SORT_OPTIONS.map((option) => {
const IconComponent = SORT_ICONS[option.value];
return (
<DropdownMenuItem
key={option.value}
onClick={() => onValueChange(option.value)}
className={`flex items-start gap-3 p-3 ${
value === option.value ? "bg-accent" : ""
}`}
>
{/* Ícone */}
<div className="mt-0.5">
{IconComponent && (
<IconComponent className="w-4 h-4 text-muted-foreground" />
)}
</div>
{option.description && (
<span className="text-xs text-muted-foreground mt-1 block">
{option.description}
</span>
)}
</div>
</DropdownMenuItem>
);
})}
{/* Conteúdo */}
<div className="flex-1">
<div className="flex items-center justify-between">
<span className="font-medium">{option.label}</span>
{value === option.value && (
<Check className="w-4 h-4 text-primary" />
)}
</div>
{option.description && (
<span className="text-xs text-muted-foreground mt-1 block">
{option.description}
</span>
)}
</div>
</DropdownMenuItem>
);
})}
</div>
</DropdownMenuContent>
</DropdownMenu>
);