feat: 🚀

This commit is contained in:
Evandro committed 2024-11-28 12:37:47 -03:00
commit e775435c9f
53 files changed
+14240

No files matched your search

+66
View File
@@ -0,0 +1,66 @@
import { listarProdutos } from "@/app/actions";
import { ProductCard } from "./ProductCard";
interface Produto {
id: number;
nome: string;
qtd: number;
tipoUN: string;
comprado: boolean;
listaId: string;
createdAt: Date;
updatedAt: Date;
}
interface CardListProps {
listaId: string;
}
export async function CardList({ listaId }: CardListProps) {
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);
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) => (
<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>
);
}
+71
View File
@@ -0,0 +1,71 @@
'use client'
import { Button } from "@/components/ui/button";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { deletarProduto } from "../actions";
import { useState } from "react";
import { Trash2 } from "lucide-react";
import { useRouter } from "next/navigation";
interface DeleteButtonProps {
produtoId: number;
produtoNome: string;
listaId: string;
}
export function DeleteButton({ produtoId, produtoNome, listaId }: DeleteButtonProps) {
const [isDeleting, setIsDeleting] = useState(false);
const router = useRouter();
async function handleDelete() {
try {
setIsDeleting(true);
await deletarProduto(produtoId, listaId);
router.refresh();
} finally {
setIsDeleting(false);
}
}
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="destructive"
size="icon"
className="text-white hover:bg-red-600"
>
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirmar exclusão</AlertDialogTitle>
<AlertDialogDescription>
Tem certeza que deseja excluir o produto "{produtoNome}"?
Esta ação não pode ser desfeita.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction
onClick={handleDelete}
className="bg-red-600 hover:bg-red-700"
disabled={isDeleting}
>
{isDeleting ? 'Excluindo...' : 'Excluir'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
+34
View File
@@ -0,0 +1,34 @@
import Link from "next/link";
export function Footer() {
return (
<footer className="mt-16 pb-6 text-center">
<Link
href="https://evandro.dev.br"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-4 py-2 rounded-full
bg-gradient-to-r from-slate-900 to-slate-700
dark:from-slate-200 dark:to-slate-400
text-white dark:text-slate-900
hover:shadow-lg transition-all duration-300
transform hover:scale-105 hover:-translate-y-0.5"
>
<span className="font-medium tracking-wide">evandro.dev.br</span>
<svg
className="w-4 h-4 transform transition-transform group-hover:translate-x-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M14 5l7 7m0 0l-7 7m7-7H3"
/>
</svg>
</Link>
</footer>
);
}
+76
View File
@@ -0,0 +1,76 @@
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { verificarLista } from '../actions'
export function IdInput() {
const [id, setId] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const router = useRouter()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError('')
if (!id.trim()) {
setError('Digite um ID válido')
setLoading(false)
return
}
try {
const result = await verificarLista(id.trim(), true) // Forçar criação se não existir
if (result.success) {
localStorage.setItem('listaId', id.trim())
router.push(`/lista/${id.trim()}`)
} else {
setError('Erro ao acessar/criar lista')
}
} catch (error) {
setError('Erro ao processar solicitação')
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
<div className="max-w-md w-full space-y-8 p-8 bg-white dark:bg-gray-800 rounded-xl shadow-lg">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900 dark:text-white">
Acesse sua lista
</h2>
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
Digite um ID para acessar ou criar uma nova lista
</p>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div>
<Input
type="text"
required
placeholder="Digite o ID da lista"
value={id}
onChange={(e) => setId(e.target.value)}
className="appearance-none rounded-lg relative block w-full px-3 py-2 border dark:bg-gray-700 dark:border-gray-600"
/>
</div>
{error && (
<p className="text-red-500 text-sm text-center">{error}</p>
)}
<Button
type="submit"
disabled={loading}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
{loading ? 'Carregando...' : 'Acessar/Criar Lista'}
</Button>
</form>
</div>
</div>
)
}
+124
View File
@@ -0,0 +1,124 @@
'use client'
import { Button } from "@/components/ui/button"
import { useRouter } from "next/navigation"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { RefreshCcw, Plus } from "lucide-react"
import { useState } from "react"
import { verificarLista } from "../actions"
import { ThemeToggle } from "@/components/theme-toggle"
interface ListHeaderProps {
listaId: string
}
export function ListHeader({ listaId }: ListHeaderProps) {
const router = useRouter()
const [novaLista, setNovaLista] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const [isRefreshing, setIsRefreshing] = useState(false)
const handleRefresh = () => {
setIsRefreshing(true)
router.refresh()
setTimeout(() => setIsRefreshing(false), 1000) // Visual feedback
}
const handleNovaLista = async () => {
if (!novaLista.trim()) {
setError('Digite um nome para a lista')
return
}
setLoading(true)
setError('')
try {
const result = await verificarLista(novaLista.trim())
if (result.exists) {
if (confirm(`A lista "${novaLista}" já existe. Deseja acessá-la?`)) {
router.push(`/lista/${novaLista.trim()}`)
}
} else {
if (confirm(`Tem certeza de que deseja criar uma nova lista chamada "${novaLista}"?`)) {
const createResult = await verificarLista(novaLista.trim(), true)
if (createResult.success) {
router.push(`/lista/${novaLista.trim()}`)
} else {
setError('Erro ao criar nova lista')
}
}
}
} catch (error) {
setError('Erro ao processar solicitação')
} finally {
setLoading(false)
}
}
return (
<div className="fixed top-0 left-0 right-0 z-50 bg-background border-b">
<div className="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-3">
<h2 className="text-lg font-semibold">
Lista Atual: {listaId}
</h2>
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size="icon">
<Plus className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Nova Lista</DialogTitle>
<DialogDescription>
Digite o nome da nova lista ou acesse uma existente
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<Input
placeholder="Nome da lista"
value={novaLista}
onChange={(e) => setNovaLista(e.target.value)}
/>
{error && (
<p className="text-destructive text-sm">{error}</p>
)}
</div>
<DialogFooter>
<Button
onClick={handleNovaLista}
disabled={loading}
>
{loading ? 'Processando...' : 'Continuar'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Button
variant="outline"
size="icon"
onClick={handleRefresh}
className={isRefreshing ? 'animate-spin' : ''}
>
<RefreshCcw className="h-4 w-4" />
</Button>
</div>
<div className="flex items-center gap-2">
<ThemeToggle />
</div>
</div>
</div>
)
}
+90
View File
@@ -0,0 +1,90 @@
'use client'
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { criarItens } from "../actions";
import { useTransition, useState } from 'react';
import { useRouter } from "next/navigation";
interface MainProps {
listaId: string;
}
export function Main({ listaId }: MainProps) {
const [isPending, startTransition] = useTransition();
const [error, setError] = useState('');
const router = useRouter();
const handleSubmit = async (formData: FormData) => {
const quantidade = Number(formData.get('produtoQuantidade'));
if (quantidade <= 0) {
setError('A quantidade deve ser maior que zero');
return;
}
setError('');
startTransition(async () => {
await criarItens(formData, listaId);
router.refresh();
const form = document.querySelector('form') as HTMLFormElement;
form.reset();
});
};
return (
<form className="flex flex-col space-y-4" action={handleSubmit}>
<div className="flex flex-wrap gap-4">
<Input
required
name="produtoNome"
className="flex-1 min-w-[200px] text-base px-4 py-2 border-[#383838] focus:border-[#1118A7]"
placeholder="Nome do produto"
/>
<Input
required
type="number"
min="0"
step="0.01"
name="produtoQuantidade"
className="w-32 text-base px-4 py-2 border-[#383838] focus:border-[#1118A7]"
placeholder="Quantidade"
/>
<Select name="produtoTipo" required>
<SelectTrigger className="w-32 text-base border-[#383838] focus:border-[#1118A7]">
<SelectValue placeholder="Tipo" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Tipos de unidades</SelectLabel>
<SelectItem value="un">un</SelectItem>
<SelectItem value="kg">kg</SelectItem>
<SelectItem value="g">g</SelectItem>
<SelectItem value="L">L</SelectItem>
<SelectItem value="ml">ml</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Button
className="bg-[#1118A7] hover:bg-[#080C94] text-white min-w-[100px]"
type="submit"
disabled={isPending}
>
{isPending ? 'Salvando...' : 'Adicionar'}
</Button>
</div>
{error && (
<p className="text-[#CC2121] text-sm">{error}</p>
)}
</form>
);
}
+34
View File
@@ -0,0 +1,34 @@
'use client'
import { motion } from 'framer-motion'
import { useTheme } from 'next-themes'
export function PageTitle() {
const { theme } = useTheme()
return (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="relative"
>
<h1 className="text-4xl md:text-5xl font-bold text-center">
<span className="relative inline-block">
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-secondary">
Resenha Itapoá
</span>
<motion.span
className="absolute -bottom-2 left-0 w-full h-1 bg-gradient-to-r from-primary to-secondary rounded-full"
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ delay: 0.5, duration: 0.5 }}
/>
</span>
</h1>
<p className="mt-4 text-center text-muted-foreground max-w-2xl mx-auto">
Gerencie suas listas de compras de forma simples e eficiente
</p>
</motion.div>
)
}
+71
View File
@@ -0,0 +1,71 @@
'use client'
import { Button } from "@/components/ui/button";
import { Check } from "lucide-react";
import { DeleteButton } from "./DeleteButton";
import { toggleComprado } from "../actions";
import { useRouter } from "next/navigation";
interface Produto {
id: number;
nome: string;
qtd: number;
tipoUN: string;
comprado: boolean;
listaId: string;
createdAt: Date;
updatedAt: Date;
}
interface ProductCardProps {
produto: Produto;
listaId: string;
}
export function ProductCard({ produto, listaId }: ProductCardProps) {
const router = useRouter();
const handleToggleComprado = async () => {
await toggleComprado(produto.id, listaId);
router.refresh();
};
return (
<div className={`card bg-white dark:bg-gray-800 shadow-lg rounded-lg hover:shadow-xl transition-duration-300
${produto.comprado ? 'border-2 border-green-500 dark:border-green-400 bg-green-50 dark:bg-green-900/20' : ''}`}
>
<div className="card-body p-6">
<div className="flex justify-between items-start">
<h2 className="card-title text-xl font-bold mb-2 dark:text-white">
{produto.nome}
</h2>
<div className="flex gap-2">
<Button
onClick={handleToggleComprado}
variant="outline"
size="icon"
className={`${produto.comprado
? 'bg-green-500 dark:bg-green-600 text-white hover:bg-green-600 dark:hover:bg-green-700'
: 'text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-900/30'
}`}
>
<Check className="h-4 w-4" />
</Button>
<DeleteButton
produtoId={produto.id}
produtoNome={produto.nome}
listaId={listaId}
/>
</div>
</div>
<div className="space-y-2">
<p className="text-gray-600 dark:text-gray-300">
Quantidade: <span className="font-semibold">{produto.qtd}</span>
</p>
<p className="text-gray-600 dark:text-gray-300">
Tipo: <span className="font-semibold">{produto.tipoUN}</span>
</p>
</div>
</div>
</div>
);
}
+30
View File
@@ -0,0 +1,30 @@
interface CardProps {
produto: {
id: number;
nome: string;
qtd: number;
tipoUN: string;
comprado: boolean;
listaId: string;
createdAt: Date;
updatedAt: Date;
}
}
export function Card({ produto }: CardProps) {
return (
<div className="card bg-white shadow-lg rounded-lg hover:shadow-xl transition-duration-300">
<div className="card-body p-6">
<h2 className="card-title text-xl font-bold mb-2">{produto.nome}</h2>
<div className="space-y-2">
<p className="text-gray-600">
Quantidade: <span className="font-semibold">{produto.qtd}</span>
</p>
<p className="text-gray-600">
Tipo: <span className="font-semibold">{produto.tipoUN}</span>
</p>
</div>
</div>
</div>
);
}
+9
View File
@@ -0,0 +1,9 @@
export function Header(){
return (
<div className="flex justify-center">
<p className="font-bold text-5xl">
RESENHA ITAPOA
</p>
</div>
)
}
+6
View File
@@ -0,0 +1,6 @@
import prisma from "@/lib/prisma";
export async function Items() {
const produtos = await prisma.produtos.findMany({});
console.log(produtos);
}