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

+3
View File
@@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
}
+41
View File
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
#database
/prisma/db
/prisma/migrations
+36
View File
@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
+113
View File
@@ -0,0 +1,113 @@
'use server'
import prisma from "@/lib/prisma";
import { revalidatePath } from 'next/cache';
export async function verificarLista(id: string, criar: boolean = false) {
try {
let lista = await prisma.lista.findUnique({
where: { id }
});
if (!lista && criar) {
lista = await prisma.lista.create({
data: { id }
});
}
return {
success: true,
exists: !!lista,
lista
};
} catch (error) {
console.error('Erro ao verificar lista:', error);
return { success: false, error: 'Erro ao processar solicitação' };
}
}
export async function criarItens(formData: FormData, listaId: string) {
try {
const produto = formData.get('produtoNome') as string;
const qtd = Number(formData.get('produtoQuantidade'));
const tipo = formData.get('produtoTipo') as string;
if (!produto || !qtd || !tipo) {
throw new Error('Todos os campos são obrigatórios');
}
const novoProduto = await prisma.produtos.create({
data: {
nome: produto,
qtd: qtd,
tipoUN: tipo,
listaId
}
});
revalidatePath(`/lista/${listaId}`);
return { success: true, data: novoProduto };
} catch (error) {
console.error('Erro ao criar produto:', error);
return { success: false, error: 'Erro ao criar produto' };
}
}
export async function listarProdutos(listaId: string) {
try {
const produtos = await prisma.produtos.findMany({
where: { listaId },
orderBy: {
id: 'desc'
}
});
return produtos;
} catch (error) {
console.error('Erro ao buscar produtos:', error);
return [];
}
}
export async function deletarProduto(id: number, listaId: string) {
try {
await prisma.produtos.delete({
where: {
id: id,
listaId: listaId
}
});
revalidatePath(`/lista/${listaId}`);
return { success: true, message: 'Produto removido com sucesso' };
} catch (error) {
console.error('Erro ao deletar produto:', error);
return { success: false, error: 'Erro ao remover produto' };
}
}
export async function toggleComprado(id: number, listaId: string) {
try {
const produto = await prisma.produtos.findUnique({
where: { id }
});
if (!produto) {
throw new Error('Produto não encontrado');
}
await prisma.produtos.update({
where: {
id,
listaId: listaId
},
data: {
comprado: !produto.comprado
}
});
revalidatePath(`/lista/${listaId}`);
return { success: true };
} catch (error) {
console.error('Erro ao atualizar status:', error);
return { success: false };
}
}
+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);
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+72
View File
@@ -0,0 +1,72 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 215 90% 50%;
/* Azul Royal (#0055FF) */
--primary-foreground: 210 40% 98%;
--secondary: 185 70% 45%;
/* Verde-azulado (#19A7A7) */
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 185 70% 95%;
/* Verde-azulado claro */
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 215 90% 50%;
--radius: 0.5rem;
}
.dark {
--background: 217 33% 17%;
/* Fundo escuro azulado */
--foreground: 210 40% 98%;
--card: 217 33% 17%;
--card-foreground: 210 40% 98%;
--popover: 217 33% 17%;
--popover-foreground: 210 40% 98%;
--primary: 215 90% 60%;
/* Azul Royal mais claro */
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 185 70% 40%;
/* Verde-azulado ajustado */
--secondary-foreground: 210 40% 98%;
--muted: 217 33% 25%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 185 70% 25%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217 33% 25%;
--input: 217 33% 25%;
--ring: 215 90% 60%;
}
}
+65
View File
@@ -0,0 +1,65 @@
import "./globals.css"
import { Metadata } from 'next'
import { Inter } from 'next/font/google'
import { ThemeProvider } from '@/app/providers'
// Use Inter como fallback já que é uma fonte similar à Geist
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
metadataBase: new URL('https://resenhaitapoa.com.br'),
title: {
default: 'Resenha Itapoá - Gerenciador de Listas de Compras',
template: '%s | Resenha Itapoá'
},
description: 'Gerencie suas listas de compras de forma simples e eficiente. Compartilhe listas, acompanhe itens em tempo real.',
keywords: ['lista de compras', 'gerenciador de compras', 'Itapoá', 'compras colaborativas', 'organização de compras'],
authors: [{ name: 'Evandro', url: 'https://evandro.dev.br' }],
creator: 'Evandro',
openGraph: {
type: 'website',
locale: 'pt_BR',
url: 'https://resenhaitapoa.com.br',
title: 'Resenha Itapoá - Gerenciador de Listas de Compras',
description: 'Gerencie suas listas de compras de forma simples e eficiente.',
siteName: 'Resenha Itapoá',
},
twitter: {
card: 'summary_large_image',
title: 'Resenha Itapoá - Gerenciador de Listas de Compras',
description: 'Gerencie suas listas de compras de forma simples e eficiente.',
creator: '@evandrodev',
},
manifest: '/manifest.json',
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html
lang="pt-BR"
suppressHydrationWarning
className={inter.className}
>
<body className="min-h-screen bg-background antialiased">
<ThemeProvider>
{children}
</ThemeProvider>
</body>
</html>
)
}
+46
View File
@@ -0,0 +1,46 @@
import { Suspense } from 'react';
import { Main } from '@/app/components/Main';
import { CardList } from '@/app/components/CardList';
import { ThemeToggle } from '@/components/theme-toggle';
import { ListHeader } from '@/app/components/ListHeader';
import { Footer } from '@/app/components/Footer';
import { verificarLista } from '@/app/actions';
import { redirect } from 'next/navigation';
interface ListaPageProps {
params: {
id: string
}
}
export default async function ListaPage({ params }: ListaPageProps) {
const { success, exists } = await verificarLista(params.id);
if (!success) {
redirect('/');
}
return (
<main className="min-h-screen bg-gray-50 dark:bg-gray-900 transition-colors duration-300">
<ListHeader listaId={params.id} />
<ThemeToggle />
<div className="container mx-auto px-4 pt-20 pb-8">
<div className="mb-8">
<Main listaId={params.id} />
</div>
<Suspense
fallback={
<div className="flex justify-center items-center p-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#1118A7] dark:border-[#212FCC]"></div>
</div>
}
>
<CardList listaId={params.id} />
</Suspense>
<Footer />
</div>
</main>
);
}
+5
View File
@@ -0,0 +1,5 @@
import { IdInput } from './components/IdInput';
export default function Home() {
return <IdInput />;
}
+19
View File
@@ -0,0 +1,19 @@
'use client'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
import { ReactNode } from 'react'
interface ProvidersProps {
children: ReactNode
}
export function ThemeProvider({ children }: ProvidersProps) {
return (
<NextThemesProvider
attribute="class"
defaultTheme="system"
enableSystem
>
{children}
</NextThemesProvider>
)
}
+12
View File
@@ -0,0 +1,12 @@
import { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: '/private/',
},
sitemap: 'https://resenhaitapoa.com.br/sitemap.xml',
}
}
+12
View File
@@ -0,0 +1,12 @@
import { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://resenhaitapoa.com.br',
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
}
]
}
+21
View File
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
+38
View File
@@ -0,0 +1,38 @@
"use client"
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export function ThemeToggle() {
const { setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Claro
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Escuro
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
Sistema
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
+141
View File
@@ -0,0 +1,141 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}
+57
View File
@@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
+122
View File
@@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
+138
View File
@@ -0,0 +1,138 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}
+22
View File
@@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }
+159
View File
@@ -0,0 +1,159 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}
+5
View File
@@ -0,0 +1,5 @@
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export default prisma;
+48
View File
@@ -0,0 +1,48 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
// Cache de performance
const perfCache = new Map<string, number>()
// Debounce utility
export function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout
return (...args: Parameters<T>) => {
clearTimeout(timeout)
timeout = setTimeout(() => func(...args), wait)
}
}
// Memoização para cálculos pesados
export function memoize<T extends (...args: any[]) => any>(
func: T,
resolver?: (...args: Parameters<T>) => string
): T {
const cache = new Map<string, ReturnType<T>>()
return ((...args: Parameters<T>) => {
const key = resolver ? resolver(...args) : JSON.stringify(args)
if (cache.has(key)) {
return cache.get(key)
}
const result = func(...args)
cache.set(key, result)
return result
}) as T
}
// Medição de performance
export function measurePerformance(key: string, callback: () => void) {
const start = performance.now()
callback()
const end = performance.now()
perfCache.set(key, end - start)
}
+4
View File
@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;
+7581
View File
File diff suppressed because it is too large. Load diff
+40
View File
@@ -0,0 +1,40 @@
{
"name": "resenhaitapoa2",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@prisma/client": "^5.22.0",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-slot": "^1.1.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^11.12.0",
"lucide-react": "^0.460.0",
"next": "14.2.18",
"next-themes": "^0.4.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@types/node": "^20.17.8",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"depcheck": "^1.4.7",
"eslint": "^8.57.1",
"eslint-config-next": "14.2.18",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.15",
"typescript": "^5.7.2"
}
}
+4681
View File
File diff suppressed because it is too large. Load diff
+8
View File
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;
+27
View File
@@ -0,0 +1,27 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model Lista {
id String @id
produtos Produtos[]
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
}
model Produtos {
id Int @id @default(autoincrement())
nome String
qtd Float
tipoUN String
comprado Boolean @default(false)
listaId String @default("default") // Valor padrão temporário
lista Lista @relation(fields: [listaId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
}
+21
View File
@@ -0,0 +1,21 @@
{
"name": "Resenha Itapoá",
"short_name": "Resenha",
"description": "Gerenciador de listas de compras",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#0055FF",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
+66
View File
@@ -0,0 +1,66 @@
import type { Config } from "tailwindcss";
const config: Config = {
darkMode: ["class"],
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
fontFamily: {
display: ['Poppins', 'sans-serif'],
},
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
}
}
},
plugins: [require("tailwindcss-animate")],
};
export default config;
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}