Commit f5fa1ac0 authored by Paulo Avila's avatar Paulo Avila
Browse files

Versão 1.0 Petruz Task

parent 95bad46e
# Banco de dados — SQL Server existente
# O banco (DB_NAME) deve existir; as tabelas são criadas automaticamente.
DB_SERVER=localhost
DB_PORT=1433
DB_USER=sa
DB_PASSWORD=SuaSenhaForte!
DB_NAME=PetruzTasks
# Use "true" se o servidor exigir conexão criptografada (ex.: Azure SQL)
DB_ENCRYPT=false
# Use "false" apenas se o certificado do servidor for confiável
DB_TRUST_SERVER_CERTIFICATE=true
# Segredo para assinar o token de sessão (JWT).
# Gere um novo com: openssl rand -hex 32
AUTH_SECRET=troque-este-valor-por-um-segredo-aleatorio
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).
# Petruz Tasks
## Getting Started
Gerenciador de tarefas da empresa Petruz — um "ClickUp simplificado" com
espaços de trabalho, tarefas com status/responsável/prazos, alertas de
vencimento e painel geral de andamento.
First, run the development server:
## Stack
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
- **Next.js 14** (App Router) + **TypeScript** + **Node.js**
- **shadcn/ui** + Tailwind CSS — temas **claro e escuro** em tons de açaí (roxo)
- **SQL Server** (servidor existente) — as tabelas são **criadas
automaticamente** na primeira conexão
- Autenticação própria: senhas com hash **bcrypt** e sessão em cookie
**JWT (httpOnly)**
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
## Como rodar
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
1. Copie `.env.example` para `.env.local` e ajuste a conexão com o seu
SQL Server (o banco informado em `DB_NAME` precisa existir; as tabelas
são inicializadas pelo sistema). Gere um `AUTH_SECRET` próprio:
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.
```bash
openssl rand -hex 32
```
## Learn More
2. Instale e suba o servidor de desenvolvimento:
To learn more about Next.js, take a look at the following resources:
```bash
npm install
npm run dev
```
- [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.
3. Acesse <http://localhost:3000>, crie sua conta em **Cadastre-se** e
comece criando um **espaço de trabalho**.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
Para produção: `npm run build && npm start`.
## Deploy on Vercel
## Funcionalidades
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.
- **Usuários**: cadastro com e-mail + senha hasheada (bcrypt, custo 12);
sessão JWT assinada (HS256) em cookie httpOnly; middleware protege todas
as rotas do app.
- **Autorização**: acesso a um espaço restrito aos seus membros; gestão
(membros/exclusão) restrita a admins do espaço; o dono entra como admin.
**Edição/exclusão de tarefa e subtarefas** restritas ao **responsável**
(admins do espaço editam tudo; tarefas sem responsável: quem criou).
Os demais membros veem tudo em modo somente leitura.
- **Espaços de trabalho**: criação com cor e descrição; convite de membros
pelo e-mail cadastrado; papéis admin/membro.
- **Tarefas**: título, descrição, status (A Fazer, Em Andamento, Em Revisão,
Concluída), prioridade, responsável, data de início e vencimento; edição,
exclusão e mudança rápida de status; visualização em **lista**, **quadro**
e **por pessoa** — cada membro do espaço tem uma "pasta" com contadores
por status e % médio; clicar na pasta abre a página da pessoa com apenas
as tarefas dela (visão padrão).
- **Painel por workspace**: a rota Painel mostra um cartão por espaço com
contagens por status, barra de andamento e alertas de vencimento do espaço.
- **Progresso**: % de conclusão por tarefa — digitado manualmente (inline ou
no diálogo) ou calculado automaticamente pelas subtarefas quando existem;
tarefas concluídas contam como 100%.
- **Subtarefas**: checklist dentro de cada tarefa.
- **Acompanhamento**: cartões por status (A Fazer, Em Andamento, Em Revisão,
Concluída) e barra de andamento geral no topo de cada espaço.
- **Alertas de vencimento**: badges "Vencida / Vence hoje / Vence amanhã"
nas tarefas e seção de alertas no painel (vencidas + próximos 3 dias).
- **Painel**: totais por status, alertas de vencimento e andamento por
espaço com barras de progresso.
- **Tema**: claro/escuro (toggle na barra superior), paleta açaí.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
## Estrutura do banco (criada automaticamente)
| Tabela | Descrição |
| --- | --- |
| `Users` | usuários com e-mail único e hash de senha |
| `Workspaces` | espaços de trabalho (nome, cor, dono) |
| `WorkspaceMembers` | membros por espaço com papel `admin`/`member` |
| `Tasks` | tarefas com status, prioridade, responsável e prazos |
| `Subtasks` | subtarefas (checklist) de cada tarefa, base do % de conclusão |
Desenvolvimento de gerenciador de tarefas para a empresa Petruz
Segue especificação.
o sistema sera um gerenciador de tarefas parecido com click-up porem bem mais simplificado, tera os aspectos padroes como, lista de afazeres, status de tarefa,responsavel,vencimento, inicio, alerta de vencimentos. criação de tarefas será em espaços de trabalhos, e uma forma de vizualizar em paineis o andamento geral.
Especificação tecnica :
Sera desenvolvido com Next JS e TS, shadcnui com node
para banco de dados: SQL server com um servidor que já existe, o sistema inicializara as tabelas
o sistema deve possuir thema dark e tema light, cores devem remeter ao Açai (roxo)
Usuarios
os usuarios devem ter emails cadastrados e senhas hasheadas no banco de dados para segurança, e deve haver politicas para controle de autorização
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
\ No newline at end of file
/** @type {import('next').NextConfig} */
const nextConfig = {};
const nextConfig = {
experimental: {
// O driver mssql não pode ser empacotado pelo webpack (quebra a
// validação de tipos dos parâmetros); deixá-lo como dependência externa.
serverComponentsExternalPackages: ["mssql"],
},
};
export default nextConfig;
This diff is collapsed.
import Link from "next/link";
import { AlertTriangle, ArrowRight, FolderKanban } from "lucide-react";
import { requireUser } from "@/lib/authz";
import { getDueAlertsForUser, listWorkspacesForUser } from "@/lib/data";
import { getStatusCountsForUser } from "@/lib/data";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { Button } from "@/components/ui/button";
import { DueBadge } from "@/components/tasks/due-badge";
import { STATUS_CHIPS } from "@/components/tasks/task-by-person";
import { formatDate } from "@/lib/dates";
import { cn } from "@/lib/utils";
export const metadata = { title: "Painel — Petruz Tasks" };
export default async function DashboardPage() {
const user = await requireUser();
const [workspaces, statusCounts, dueAlerts] = await Promise.all([
listWorkspacesForUser(user.id),
getStatusCountsForUser(user.id),
getDueAlertsForUser(user.id, 3),
]);
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold">Painel</h1>
<p className="text-sm text-muted-foreground">
Andamento de cada espaço de trabalho.
</p>
</div>
{workspaces.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center gap-2 py-12 text-center">
<p className="font-medium">
Você ainda não participa de nenhum espaço.
</p>
<Button asChild variant="link">
<Link href="/workspaces">Criar meu primeiro espaço</Link>
</Button>
</CardContent>
</Card>
) : (
<div className="grid gap-5 lg:grid-cols-2">
{workspaces.map((ws) => {
const wsCounts = statusCounts.filter(
(c) => c.WorkspaceId === ws.Id
);
const total = wsCounts.reduce((sum, c) => sum + c.Count, 0);
const done = wsCounts.find((c) => c.Status === "done")?.Count ?? 0;
const percent = total > 0 ? Math.round((done / total) * 100) : 0;
const wsAlerts = dueAlerts.filter(
(t) => t.WorkspaceId === ws.Id
);
return (
<Card key={ws.Id}>
<CardHeader className="pb-3">
<div className="flex items-center justify-between gap-2">
<CardTitle className="flex items-center gap-2 text-lg">
<span
className="h-3 w-3 rounded-full"
style={{ backgroundColor: ws.Color }}
/>
{ws.Name}
</CardTitle>
<Button asChild variant="ghost" size="sm">
<Link href={`/workspaces/${ws.Id}`}>
Abrir
<ArrowRight className="ml-1 h-4 w-4" />
</Link>
</Button>
</div>
{ws.Description && (
<CardDescription className="line-clamp-1">
{ws.Description}
</CardDescription>
)}
</CardHeader>
<CardContent className="space-y-4">
{total === 0 ? (
<p className="flex items-center gap-2 py-3 text-sm text-muted-foreground">
<FolderKanban className="h-4 w-4" />
Nenhuma tarefa neste espaço ainda.
</p>
) : (
<>
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
{STATUS_CHIPS.map((chip) => {
const count =
wsCounts.find((c) => c.Status === chip.value)
?.Count ?? 0;
const Icon = chip.icon;
return (
<div
key={chip.value}
className="rounded-md border p-2 text-center"
>
<span
className={cn(
"flex items-center justify-center gap-1 text-xs font-medium",
chip.className
)}
>
<Icon className="h-3.5 w-3.5" />
{chip.label}
</span>
<p className="mt-1 text-xl font-bold">{count}</p>
</div>
);
})}
</div>
<div className="flex items-center gap-3">
<span className="whitespace-nowrap text-sm font-medium">
Andamento
</span>
<Progress value={percent} className="h-2.5" />
<span className="whitespace-nowrap text-sm font-semibold text-primary">
{percent}%
</span>
</div>
{wsAlerts.length > 0 && (
<div className="space-y-2">
<p className="flex items-center gap-1.5 text-sm font-medium text-destructive">
<AlertTriangle className="h-4 w-4" />
Alertas de vencimento
</p>
<ul className="space-y-1.5">
{wsAlerts.slice(0, 5).map((task) => (
<li key={task.Id}>
<Link
href={`/workspaces/${ws.Id}`}
className="flex items-center justify-between gap-2 rounded-md border px-3 py-2 transition hover:border-primary/60"
>
<div className="min-w-0">
<p className="truncate text-sm font-medium">
{task.Title}
</p>
<p className="text-xs text-muted-foreground">
{task.AssigneeName ?? "Sem responsável"} ·{" "}
{formatDate(task.DueDate)}
</p>
</div>
<DueBadge
dueDate={task.DueDate}
status={task.Status}
/>
</Link>
</li>
))}
</ul>
</div>
)}
</>
)}
</CardContent>
</Card>
);
})}
</div>
)}
</div>
);
}
import { requireUser } from "@/lib/authz";
import { listWorkspacesForUser } from "@/lib/data";
import { Sidebar } from "@/components/layout/sidebar";
import { Topbar } from "@/components/layout/topbar";
export default async function AppLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
const user = await requireUser();
let workspaces: Awaited<ReturnType<typeof listWorkspacesForUser>> = [];
try {
workspaces = await listWorkspacesForUser(user.id);
} catch (err) {
console.error("Falha ao carregar workspaces:", err);
}
return (
<div className="flex min-h-screen">
<Sidebar workspaces={workspaces} />
<div className="flex min-w-0 flex-1 flex-col">
<Topbar user={user} />
<main className="flex-1 p-4 md:p-6">{children}</main>
</div>
</div>
);
}
import Link from "next/link";
import { notFound } from "next/navigation";
import { ArrowLeft, FolderOpen, Plus, UserRound } from "lucide-react";
import { requireUser, requireWorkspaceMember } from "@/lib/authz";
import {
getWorkspace,
listSubtasksByWorkspace,
listTasksByWorkspace,
listWorkspaceMembers,
type Subtask,
} from "@/lib/data";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { StatusSummary } from "@/components/tasks/status-summary";
import { TaskDialog } from "@/components/tasks/task-dialog";
import { TaskTable } from "@/components/tasks/task-table";
function initials(name: string): string {
return name
.split(" ")
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase())
.join("");
}
export default async function MemberFolderPage({
params,
}: {
params: { id: string; memberId: string };
}) {
const workspaceId = Number(params.id);
const memberId = Number(params.memberId);
if (!Number.isInteger(workspaceId) || !Number.isInteger(memberId)) {
notFound();
}
const user = await requireUser();
const role = await requireWorkspaceMember(workspaceId, user);
const workspace = await getWorkspace(workspaceId);
if (!workspace) notFound();
const [allTasks, members, allSubtasks] = await Promise.all([
listTasksByWorkspace(workspaceId),
listWorkspaceMembers(workspaceId),
listSubtasksByWorkspace(workspaceId),
]);
// memberId 0 = pasta "Sem responsável"
const member =
memberId === 0 ? null : members.find((m) => m.UserId === memberId);
if (memberId !== 0 && !member) notFound();
const tasks = allTasks.filter((t) =>
member ? t.AssigneeId === member.UserId : t.AssigneeId === null
);
const taskIds = new Set(tasks.map((t) => t.Id));
const subtasksByTask = allSubtasks
.filter((s) => taskIds.has(s.TaskId))
.reduce<Record<number, Subtask[]>>((acc, subtask) => {
(acc[subtask.TaskId] ??= []).push(subtask);
return acc;
}, {});
const folderName = member?.Name ?? "Sem responsável";
const isOwnFolder = member?.UserId === user.id;
return (
<div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-3">
<Button variant="ghost" size="icon" asChild title="Voltar ao espaço">
<Link href={`/workspaces/${workspaceId}`}>
<ArrowLeft className="h-5 w-5" />
</Link>
</Button>
<FolderOpen className="h-6 w-6 text-primary" />
<Avatar className="h-10 w-10">
<AvatarFallback
className={
member
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground"
}
>
{member ? (
initials(member.Name)
) : (
<UserRound className="h-5 w-5" />
)}
</AvatarFallback>
</Avatar>
<div>
<h1 className="text-2xl font-bold">
{isOwnFolder ? "Minha pasta" : `Pasta de ${folderName}`}
</h1>
<p className="text-sm text-muted-foreground">
{workspace.Name} ·{" "}
{tasks.length === 0
? "nenhuma tarefa"
: `${tasks.length} tarefa${tasks.length > 1 ? "s" : ""}`}
</p>
</div>
</div>
<TaskDialog
workspaceId={workspaceId}
members={members}
defaultAssigneeId={member?.UserId}
trigger={
<Button>
<Plus className="mr-2 h-4 w-4" />
Nova tarefa
</Button>
}
/>
</div>
<StatusSummary tasks={tasks} />
<TaskTable
tasks={tasks}
members={members}
workspaceId={workspaceId}
subtasksByTask={subtasksByTask}
currentUserId={user.id}
isAdmin={role === "admin"}
/>
</div>
);
}
import { notFound } from "next/navigation";
import { Plus } from "lucide-react";
import { requireUser, requireWorkspaceMember } from "@/lib/authz";
import {
getWorkspace,
listSubtasksByWorkspace,
listTasksByWorkspace,
listWorkspaceMembers,
type Subtask,
} from "@/lib/data";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { MembersDialog } from "@/components/workspaces/members-dialog";
import { WorkspaceDeleteButton } from "@/components/workspaces/workspace-delete-button";
import { TaskDialog } from "@/components/tasks/task-dialog";
import { TaskTable } from "@/components/tasks/task-table";
import { TaskBoard } from "@/components/tasks/task-board";
import { TaskByPerson } from "@/components/tasks/task-by-person";
import { StatusSummary } from "@/components/tasks/status-summary";
export default async function WorkspacePage({
params,
}: {
params: { id: string };
}) {
const workspaceId = Number(params.id);
if (!Number.isInteger(workspaceId)) notFound();
const user = await requireUser();
const role = await requireWorkspaceMember(workspaceId, user);
const workspace = await getWorkspace(workspaceId);
if (!workspace) notFound();
const [tasks, members, subtasks] = await Promise.all([
listTasksByWorkspace(workspaceId),
listWorkspaceMembers(workspaceId),
listSubtasksByWorkspace(workspaceId),
]);
const subtasksByTask = subtasks.reduce<Record<number, Subtask[]>>(
(acc, subtask) => {
(acc[subtask.TaskId] ??= []).push(subtask);
return acc;
},
{}
);
const isAdmin = role === "admin";
return (
<div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-3">
<span
className="h-4 w-4 rounded-full"
style={{ backgroundColor: workspace.Color }}
/>
<div>
<h1 className="text-2xl font-bold">{workspace.Name}</h1>
{workspace.Description && (
<p className="text-sm text-muted-foreground">
{workspace.Description}
</p>
)}
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<MembersDialog
workspaceId={workspaceId}
ownerId={workspace.OwnerId}
members={members}
isAdmin={isAdmin}
/>
{isAdmin && (
<WorkspaceDeleteButton
workspaceId={workspaceId}
workspaceName={workspace.Name}
/>
)}
<TaskDialog
workspaceId={workspaceId}
members={members}
trigger={
<Button>
<Plus className="mr-2 h-4 w-4" />
Nova tarefa
</Button>
}
/>
</div>
</div>
<StatusSummary tasks={tasks} />
<Tabs defaultValue="people">
<TabsList>
<TabsTrigger value="people">Por pessoa</TabsTrigger>
<TabsTrigger value="list">Lista</TabsTrigger>
<TabsTrigger value="board">Quadro</TabsTrigger>
</TabsList>
<TabsContent value="people" className="mt-4">
<TaskByPerson
tasks={tasks}
members={members}
workspaceId={workspaceId}
/>
</TabsContent>
<TabsContent value="list" className="mt-4">
<TaskTable
tasks={tasks}
members={members}
workspaceId={workspaceId}
subtasksByTask={subtasksByTask}
currentUserId={user.id}
isAdmin={isAdmin}
/>
</TabsContent>
<TabsContent value="board" className="mt-4">
<TaskBoard
tasks={tasks}
members={members}
workspaceId={workspaceId}
subtasksByTask={subtasksByTask}
currentUserId={user.id}
isAdmin={isAdmin}
/>
</TabsContent>
</Tabs>
</div>
);
}
import Link from "next/link";
import { Users } from "lucide-react";
import { requireUser } from "@/lib/authz";
import { listWorkspacesForUser } from "@/lib/data";
import { WorkspaceCreateDialog } from "@/components/workspaces/workspace-create-dialog";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { Badge } from "@/components/ui/badge";
export const metadata = { title: "Espaços de trabalho — Petruz Tasks" };
export default async function WorkspacesPage({
searchParams,
}: {
searchParams: { new?: string };
}) {
const user = await requireUser();
const workspaces = await listWorkspacesForUser(user.id);
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Espaços de trabalho</h1>
<p className="text-sm text-muted-foreground">
Crie espaços para organizar as tarefas de cada equipe ou projeto.
</p>
</div>
<WorkspaceCreateDialog defaultOpen={searchParams.new === "1"} />
</div>
{workspaces.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center gap-2 py-12 text-center">
<p className="font-medium">Você ainda não participa de nenhum espaço.</p>
<p className="text-sm text-muted-foreground">
Crie seu primeiro espaço de trabalho para começar.
</p>
</CardContent>
</Card>
) : (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{workspaces.map((ws) => {
const progress =
ws.TaskCount > 0
? Math.round((ws.DoneCount / ws.TaskCount) * 100)
: 0;
return (
<Link key={ws.Id} href={`/workspaces/${ws.Id}`}>
<Card className="h-full transition hover:border-primary/60 hover:shadow-md">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<span
className="h-3 w-3 rounded-full"
style={{ backgroundColor: ws.Color }}
/>
<Badge variant="secondary" className="gap-1">
<Users className="h-3 w-3" />
{ws.MemberRole === "admin" ? "Admin" : "Membro"}
</Badge>
</div>
<CardTitle className="text-lg">{ws.Name}</CardTitle>
{ws.Description && (
<CardDescription className="line-clamp-2">
{ws.Description}
</CardDescription>
)}
</CardHeader>
<CardContent className="space-y-2">
<div className="flex justify-between text-sm text-muted-foreground">
<span>
{ws.DoneCount}/{ws.TaskCount} tarefas concluídas
</span>
<span>{progress}%</span>
</div>
<Progress value={progress} />
</CardContent>
</Card>
</Link>
);
})}
</div>
)}
</div>
);
}
import Image from "next/image";
export default function AuthLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-br from-[#2e1065] via-[#4c1d95] to-[#1e1b4b] p-4">
<div className="mb-6 flex flex-col items-center gap-2">
<Image
src="/petruz.png"
alt="Petruz"
width={120}
height={120}
priority
/>
<p className="text-sm text-purple-200">
Gerenciador de tarefas da Petruz
</p>
</div>
{children}
</div>
);
}
import { LoginForm } from "@/components/auth/login-form";
export const metadata = { title: "Entrar — Petruz Tasks" };
export default function LoginPage() {
return <LoginForm />;
}
import { RegisterForm } from "@/components/auth/register-form";
export const metadata = { title: "Criar conta — Petruz Tasks" };
export default function RegisterPage() {
return <RegisterForm />;
}
......@@ -2,26 +2,86 @@
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
@layer base {
:root {
--background: #0a0a0a;
--foreground: #ededed;
/* Tema claro — tons de açaí (roxo) */
--background: 270 40% 99%;
--foreground: 271 30% 12%;
--card: 0 0% 100%;
--card-foreground: 271 30% 12%;
--popover: 0 0% 100%;
--popover-foreground: 271 30% 12%;
--primary: 271 65% 40%;
--primary-foreground: 270 40% 98%;
--secondary: 270 30% 94%;
--secondary-foreground: 271 50% 25%;
--muted: 270 25% 95%;
--muted-foreground: 270 12% 42%;
--accent: 278 40% 93%;
--accent-foreground: 271 55% 28%;
--destructive: 0 72% 51%;
--destructive-foreground: 0 0% 98%;
--border: 270 25% 89%;
--input: 270 25% 87%;
--ring: 271 65% 40%;
--chart-1: 271 65% 40%;
--chart-2: 290 60% 55%;
--chart-3: 255 60% 60%;
--chart-4: 310 55% 50%;
--chart-5: 240 50% 55%;
--radius: 0.625rem;
--sidebar: 271 45% 16%;
--sidebar-foreground: 270 30% 92%;
--sidebar-primary: 271 65% 55%;
--sidebar-primary-foreground: 270 40% 98%;
--sidebar-accent: 271 40% 24%;
--sidebar-accent-foreground: 270 30% 95%;
--sidebar-border: 271 35% 24%;
--sidebar-ring: 271 65% 55%;
}
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
.dark {
/* Tema escuro — açaí profundo */
--background: 272 38% 7%;
--foreground: 270 25% 95%;
--card: 271 32% 11%;
--card-foreground: 270 25% 95%;
--popover: 271 32% 10%;
--popover-foreground: 270 25% 95%;
--primary: 271 75% 68%;
--primary-foreground: 272 45% 10%;
--secondary: 271 28% 18%;
--secondary-foreground: 270 25% 92%;
--muted: 271 24% 17%;
--muted-foreground: 270 12% 65%;
--accent: 271 32% 23%;
--accent-foreground: 270 30% 95%;
--destructive: 0 65% 52%;
--destructive-foreground: 0 0% 98%;
--border: 271 24% 20%;
--input: 271 24% 23%;
--ring: 271 75% 68%;
--chart-1: 271 75% 68%;
--chart-2: 290 65% 65%;
--chart-3: 255 65% 70%;
--chart-4: 310 60% 62%;
--chart-5: 240 55% 68%;
--sidebar: 272 40% 9%;
--sidebar-foreground: 270 25% 92%;
--sidebar-primary: 271 75% 68%;
--sidebar-primary-foreground: 272 45% 10%;
--sidebar-accent: 271 30% 18%;
--sidebar-accent-foreground: 270 25% 95%;
--sidebar-border: 271 26% 16%;
--sidebar-ring: 271 75% 68%;
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
import type { Metadata } from "next";
import localFont from "next/font/local";
import { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/sonner";
import "./globals.css";
const geistSans = localFont({
......@@ -7,15 +9,10 @@ const geistSans = localFont({
variable: "--font-geist-sans",
weight: "100 900",
});
const geistMono = localFont({
src: "./fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
weight: "100 900",
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Petruz Tasks",
description: "Gerenciador de tarefas da empresa Petruz",
};
export default function RootLayout({
......@@ -24,11 +21,17 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<html lang="pt-BR" suppressHydrationWarning>
<body className={`${geistSans.variable} font-sans antialiased`}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
<Toaster richColors position="top-right" />
</ThemeProvider>
</body>
</html>
);
......
import Image from "next/image";
import { redirect } from "next/navigation";
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="https://nextjs.org/icons/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
src/app/page.tsx
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="https://nextjs.org/icons/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="https://nextjs.org/icons/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="https://nextjs.org/icons/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="https://nextjs.org/icons/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org →
</a>
</footer>
</div>
);
redirect("/dashboard");
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment