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

+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,
}
]
}