aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorzwlucas <lucas.oliveira1676@etec.sp.gov.br>2025-04-01 02:34:04 +0000
committerzwlucas <lucas.oliveira1676@etec.sp.gov.br>2025-04-01 02:34:04 +0000
commit79670b4c51ebbdd242b894a5f0678618054cc2ef (patch)
tree654b351c13016b7d83137409b56031110cb46628 /app
parent0d54368efc5e91bf1beea8961655fa77f51b3074 (diff)
downloadeleicoes-79670b4c51ebbdd242b894a5f0678618054cc2ef.tar.gz
eleicoes-79670b4c51ebbdd242b894a5f0678618054cc2ef.zip
create eletrocast-eleicoes
Diffstat (limited to 'app')
-rw-r--r--app/confirmar/loading.tsx4
-rw-r--r--app/confirmar/page.tsx140
-rw-r--r--app/favicon.icobin25931 -> 0 bytes
-rw-r--r--app/globals.css100
-rw-r--r--app/layout.tsx26
-rw-r--r--app/obrigado/loading.tsx3
-rw-r--r--app/obrigado/page.tsx169
-rw-r--r--app/page.tsx317
-rw-r--r--app/votar/loading.tsx3
-rw-r--r--app/votar/page.tsx113
10 files changed, 747 insertions, 128 deletions
diff --git a/app/confirmar/loading.tsx b/app/confirmar/loading.tsx
new file mode 100644
index 0000000..cec067b
--- /dev/null
+++ b/app/confirmar/loading.tsx
@@ -0,0 +1,4 @@
+export default function Loading() {
+ return null
+}
+
diff --git a/app/confirmar/page.tsx b/app/confirmar/page.tsx
new file mode 100644
index 0000000..61c7773
--- /dev/null
+++ b/app/confirmar/page.tsx
@@ -0,0 +1,140 @@
+"use client";
+
+import { useEffect } from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { AlertTriangle } from "lucide-react";
+
+export default function ConfirmPage() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const rm = searchParams.get("rm") || "";
+ const nome = searchParams.get("nome") || "";
+ const cpf = searchParams.get("cpf") || "";
+
+ useEffect(() => {
+ if (!rm || !nome || !cpf) {
+ router.push("/");
+ return;
+ }
+ }, [rm, nome, cpf, router]);
+
+ const handleConfirm = () => {
+ router.push(`/votar?rm=${rm}&nome=${encodeURIComponent(nome)}&cpf=${cpf}`);
+ };
+
+ const handleCancel = () => {
+ router.push("/");
+ };
+
+ const formatCPFDisplay = (cpf: string) => {
+ if (cpf.includes(".") || cpf.includes("-")) return cpf;
+
+ const cpfClean = cpf.replace(/\D/g, "");
+
+ if (cpfClean.length === 11) {
+ return `${cpfClean.substring(0, 3)}.${cpfClean.substring(
+ 3,
+ 6
+ )}.${cpfClean.substring(6, 9)}-${cpfClean.substring(9, 11)}`;
+ }
+
+ return cpf;
+ };
+
+ const maskCPF = (cpf: string) => {
+ const formatted = formatCPFDisplay(cpf);
+ const parts = formatted.split(".");
+ if (parts.length === 3) {
+ const lastPart = parts[2].split("-");
+ if (lastPart.length === 2) {
+ return `${parts[0]}.${parts[1]}.${"***"}-${lastPart[1]}`;
+ }
+ }
+ return formatted;
+ };
+
+ return (
+ <div className="flex min-h-screen flex-col items-center justify-center bg-[#f0f5fa]">
+ <div className="w-full max-w-md">
+ <div className="mb-6 flex items-center justify-center">
+ <div className="flex flex-col items-center">
+ <div className="mb-2 text-center text-3xl font-bold text-[#004a93]">
+ JUSTIÇA ELEITORAL
+ </div>
+ <div className="h-2 w-full bg-gradient-to-r from-[#009c3b] via-[#ffdf00] to-[#002776]"></div>
+ </div>
+ </div>
+
+ <Card className="border-2 border-[#004a93] shadow-lg overflow-hidden">
+ <CardHeader className="bg-[#004a93] text-center text-white">
+ <CardTitle className="text-2xl">CONFIRME SEUS DADOS</CardTitle>
+ <CardDescription className="text-gray-100">
+ Verifique se as informações estão corretas
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="space-y-4 pt-6">
+ <div className="rounded-lg border-2 border-[#004a93] bg-white p-4">
+ <div className="grid grid-cols-2 gap-2">
+ <div className="text-sm font-medium text-[#004a93]">RM:</div>
+ <div className="text-sm font-bold text-[#004a93]">{rm}</div>
+
+ <div className="text-sm font-medium text-[#004a93]">Nome:</div>
+ <div className="text-sm font-bold text-[#004a93]">{nome}</div>
+
+ <div className="text-sm font-medium text-[#004a93]">CPF:</div>
+ <div className="text-sm font-bold text-[#004a93]">
+ {maskCPF(cpf)}
+ </div>
+ </div>
+ </div>
+
+ <div className="rounded-lg border-2 border-amber-500 bg-amber-50 p-4">
+ <div className="flex items-start gap-2">
+ <AlertTriangle className="h-5 w-5 flex-shrink-0 text-amber-500" />
+ <div className="text-sm text-amber-800">
+ <strong>ATENÇÃO:</strong> Verifique se seus dados estão
+ corretos. Caso as informações estejam incorretas, seu voto não
+ será computado.
+ </div>
+ </div>
+ </div>
+
+ <div className="text-center text-sm font-bold text-[#004a93]">
+ Estas informações estão corretas?
+ </div>
+ </CardContent>
+ <CardFooter className="flex justify-between gap-4 border-t border-[#004a93] bg-[#f8f8f8] py-3 rounded-b-lg">
+ <Button
+ variant="outline"
+ onClick={handleCancel}
+ className="flex-1 border-2 border-[#004a93] text-[#004a93] hover:bg-[#e6f0fa] hover:text-[#003a73]"
+ >
+ CORRIGIR
+ </Button>
+ <Button
+ onClick={handleConfirm}
+ className="flex-1 bg-[#004a93] text-white hover:bg-[#003a73]"
+ >
+ CONFIRMAR
+ </Button>
+ </CardFooter>
+ </Card>
+
+ <div className="mt-4 flex justify-center">
+ <div className="text-center text-sm text-[#004a93]">
+ © {new Date().getFullYear()} Justiça Eleitoral Estudantil
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/app/favicon.ico b/app/favicon.ico
deleted file mode 100644
index 718d6fe..0000000
--- a/app/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/app/globals.css b/app/globals.css
index a2dc41e..ac68442 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1,26 +1,94 @@
-@import "tailwindcss";
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
-:root {
- --background: #ffffff;
- --foreground: #171717;
+body {
+ font-family: Arial, Helvetica, sans-serif;
}
-@theme inline {
- --color-background: var(--background);
- --color-foreground: var(--foreground);
- --font-sans: var(--font-geist-sans);
- --font-mono: var(--font-geist-mono);
+@layer utilities {
+ .text-balance {
+ text-wrap: balance;
+ }
}
-@media (prefers-color-scheme: dark) {
+@layer base {
:root {
- --background: #0a0a0a;
- --foreground: #ededed;
+ --background: 0 0% 100%;
+ --foreground: 0 0% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 0 0% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 0 0% 3.9%;
+ --primary: 0 0% 9%;
+ --primary-foreground: 0 0% 98%;
+ --secondary: 0 0% 96.1%;
+ --secondary-foreground: 0 0% 9%;
+ --muted: 0 0% 96.1%;
+ --muted-foreground: 0 0% 45.1%;
+ --accent: 0 0% 96.1%;
+ --accent-foreground: 0 0% 9%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 89.8%;
+ --input: 0 0% 89.8%;
+ --ring: 0 0% 3.9%;
+ --chart-1: 12 76% 61%;
+ --chart-2: 173 58% 39%;
+ --chart-3: 197 37% 24%;
+ --chart-4: 43 74% 66%;
+ --chart-5: 27 87% 67%;
+ --radius: 0.5rem;
+ --sidebar-background: 0 0% 98%;
+ --sidebar-foreground: 240 5.3% 26.1%;
+ --sidebar-primary: 240 5.9% 10%;
+ --sidebar-primary-foreground: 0 0% 98%;
+ --sidebar-accent: 240 4.8% 95.9%;
+ --sidebar-accent-foreground: 240 5.9% 10%;
+ --sidebar-border: 220 13% 91%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
+ }
+ .dark {
+ --background: 0 0% 3.9%;
+ --foreground: 0 0% 98%;
+ --card: 0 0% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 0 0% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary: 0 0% 98%;
+ --primary-foreground: 0 0% 9%;
+ --secondary: 0 0% 14.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 0 0% 14.9%;
+ --muted-foreground: 0 0% 63.9%;
+ --accent: 0 0% 14.9%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 14.9%;
+ --input: 0 0% 14.9%;
+ --ring: 0 0% 83.1%;
+ --chart-1: 220 70% 50%;
+ --chart-2: 160 60% 45%;
+ --chart-3: 30 80% 55%;
+ --chart-4: 280 65% 60%;
+ --chart-5: 340 75% 55%;
+ --sidebar-background: 240 5.9% 10%;
+ --sidebar-foreground: 240 4.8% 95.9%;
+ --sidebar-primary: 224.3 76.3% 48%;
+ --sidebar-primary-foreground: 0 0% 100%;
+ --sidebar-accent: 240 3.7% 15.9%;
+ --sidebar-accent-foreground: 240 4.8% 95.9%;
+ --sidebar-border: 240 3.7% 15.9%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
}
}
-body {
- background: var(--background);
- color: var(--foreground);
- font-family: Arial, Helvetica, sans-serif;
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
}
diff --git a/app/layout.tsx b/app/layout.tsx
index f7fa87e..cc5ccbe 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,20 +1,16 @@
+import type React from "react";
import type { Metadata } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
+import { Roboto } from "next/font/google";
import "./globals.css";
-const geistSans = Geist({
- variable: "--font-geist-sans",
- subsets: ["latin"],
-});
-
-const geistMono = Geist_Mono({
- variable: "--font-geist-mono",
+const roboto = Roboto({
+ weight: ["400", "500", "700", "900"],
subsets: ["latin"],
});
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: "Justiça Eleitoral - Sistema de Votação",
+ description: "Eleições Estudantis",
};
export default function RootLayout({
@@ -23,12 +19,10 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
- <html lang="en">
- <body
- className={`${geistSans.variable} ${geistMono.variable} antialiased`}
- >
- {children}
- </body>
+ <html lang="pt-BR">
+ <body className={roboto.className}>{children}</body>
</html>
);
}
+
+import "./globals.css";
diff --git a/app/obrigado/loading.tsx b/app/obrigado/loading.tsx
new file mode 100644
index 0000000..4349ac3
--- /dev/null
+++ b/app/obrigado/loading.tsx
@@ -0,0 +1,3 @@
+export default function Loading() {
+ return null;
+}
diff --git a/app/obrigado/page.tsx b/app/obrigado/page.tsx
new file mode 100644
index 0000000..718e122
--- /dev/null
+++ b/app/obrigado/page.tsx
@@ -0,0 +1,169 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import { useSearchParams } from "next/navigation";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { CheckCircle2, AlertTriangle } from "lucide-react";
+import { getSupabaseClient } from "@/lib/supabase";
+
+export default function ObrigadoPage() {
+ const searchParams = useSearchParams();
+ const rm = searchParams.get("rm") || "";
+ const name = searchParams.get("nome") || "";
+ const cpf = searchParams.get("cpf") || "";
+ const option = searchParams.get("option") || "";
+
+ const [countdown, setCountdown] = useState(5);
+ const [saveStatus, setSaveStatus] = useState<"loading" | "success" | "error">(
+ "loading"
+ );
+ const [errorMessage, setErrorMessage] = useState("");
+
+ const hasRun = useRef(false);
+
+ useEffect(() => {
+ if (!rm || !name || !cpf || !option) {
+ window.location.href = "/";
+ return;
+ }
+
+ const saveVote = async () => {
+ try {
+ const supabase = getSupabaseClient();
+
+ const { error } = await supabase.from("votes").insert([
+ {
+ rm,
+ name,
+ cpf: cpf.replace(/\D/g, ""),
+ option_voted: option,
+ },
+ ]);
+
+ if (error) {
+ console.error("Erro ao salvar voto:", error);
+ setSaveStatus("error");
+
+ if (error.code === "23505") {
+ setErrorMessage(
+ "Você já votou anteriormente. Cada eleitor pode votar apenas uma vez."
+ );
+ } else {
+ setErrorMessage(
+ "Ocorreu um erro ao registrar seu voto. Por favor, informe ao responsável."
+ );
+ }
+ return;
+ }
+
+ setSaveStatus("success");
+ } catch (error) {
+ console.error("Erro ao salvar voto:", error);
+ setSaveStatus("error");
+ setErrorMessage(
+ "Ocorreu um erro ao registrar seu voto. Por favor, informe ao responsável."
+ );
+ }
+ };
+
+ if (!hasRun.current) {
+ hasRun.current = true;
+ saveVote();
+ }
+
+ const timer = setInterval(() => {
+ setCountdown((prev) => {
+ if (prev <= 1) {
+ clearInterval(timer);
+ window.location.href = "/";
+ return 0;
+ }
+ return prev - 1;
+ });
+ }, 1000);
+
+ return () => clearInterval(timer);
+ }, [rm, name, cpf, option]);
+
+ return (
+ <div className="flex min-h-screen flex-col items-center justify-center bg-[#f0f5fa]">
+ <div className="w-full max-w-md">
+ <div className="mb-6 flex items-center justify-center">
+ <div className="flex flex-col items-center">
+ <div className="mb-2 text-center text-3xl font-bold text-[#004a93]">
+ JUSTIÇA ELEITORAL
+ </div>
+ <div className="h-2 w-full bg-gradient-to-r from-[#009c3b] via-[#ffdf00] to-[#002776]"></div>
+ </div>
+ </div>
+
+ <Card className="border-2 border-[#004a93] text-center shadow-lg overflow-hidden">
+ <CardHeader
+ className={`${
+ saveStatus === "error" ? "bg-red-600" : "bg-[#004a93]"
+ } text-white`}
+ >
+ <div className="mx-auto mb-4 rounded-full bg-white p-2">
+ {saveStatus === "loading" ? (
+ <div className="h-16 w-16 animate-spin rounded-full border-4 border-[#004a93] border-t-transparent"></div>
+ ) : saveStatus === "success" ? (
+ <CheckCircle2 className="h-16 w-16 text-[#009c3b]" />
+ ) : (
+ <AlertTriangle className="h-16 w-16 text-red-600" />
+ )}
+ </div>
+ <CardTitle className="text-2xl">
+ {saveStatus === "loading"
+ ? "PROCESSANDO SEU VOTO..."
+ : saveStatus === "success"
+ ? "VOTO REGISTRADO COM SUCESSO!"
+ : "ERRO AO REGISTRAR VOTO"}
+ </CardTitle>
+ <CardDescription className="text-gray-100">
+ {saveStatus === "success" ? "FIM" : ""}
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="pt-6 pb-6 rounded-b-lg">
+ {saveStatus === "loading" ? (
+ <p className="text-[#004a93]">
+ Aguarde enquanto registramos seu voto...
+ </p>
+ ) : saveStatus === "success" ? (
+ <>
+ <p className="text-lg font-bold text-[#004a93]">
+ Você votou em:{" "}
+ <span className="text-[#009c3b]">{option}</span>
+ </p>
+ <p className="mt-4 text-[#004a93]">
+ Obrigado por participar das eleições.
+ </p>
+ <p className="mt-4 text-sm text-[#004a93]">
+ Retornando à tela inicial em {countdown} segundos...
+ </p>
+ </>
+ ) : (
+ <>
+ <p className="text-lg font-bold text-red-600">{errorMessage}</p>
+ <p className="mt-4 text-sm text-[#004a93]">
+ Retornando à tela inicial em {countdown} segundos...
+ </p>
+ </>
+ )}
+ </CardContent>
+ </Card>
+
+ <div className="mt-4 flex justify-center">
+ <div className="text-center text-sm text-[#004a93]">
+ © {new Date().getFullYear()} Justiça Eleitoral Estudantil
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/app/page.tsx b/app/page.tsx
index 88f0cc9..56ac851 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,103 +1,228 @@
-import Image from "next/image";
+"use client";
+
+import type React from "react";
+
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { useRouter } from "next/navigation";
export default function Home() {
+ const [rm, setRm] = useState("");
+ const [nome, setNome] = useState("");
+ const [cpf, setCpf] = useState("");
+ const [errors, setErrors] = useState<{
+ rm?: string;
+ nome?: string;
+ cpf?: string;
+ }>({});
+ const router = useRouter();
+
+ function validateCPF(cpf: string): boolean {
+ cpf = cpf.replace(/\D/g, "");
+
+ if (cpf.length !== 11 || /^(\d)\1{10}$/.test(cpf)) return false;
+
+ const calc = (factor: number) =>
+ cpf
+ .split("")
+ .slice(0, factor - 1)
+ .reduce(
+ (sum, num, index) => sum + parseInt(num) * (factor - index),
+ 0
+ ) %
+ 11 <
+ 2
+ ? 0
+ : 11 -
+ (cpf
+ .split("")
+ .slice(0, factor - 1)
+ .reduce(
+ (sum, num, index) => sum + parseInt(num) * (factor - index),
+ 0
+ ) %
+ 11);
+
+ return calc(10) === parseInt(cpf[9]) && calc(11) === parseInt(cpf[10]);
+ }
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+
+ const newErrors: { rm?: string; nome?: string; cpf?: string } = {};
+
+ if (!/^\d{5}$/.test(rm)) {
+ newErrors.rm = "O RM deve conter exatamente 5 dígitos numéricos.";
+ }
+
+ if (!nome || nome.trim().length < 3) {
+ newErrors.nome = "Por favor, insira seu nome completo.";
+ }
+
+ if (!validateCPF(cpf)) {
+ newErrors.cpf = "CPF inválido. Insira um CPF válido com 11 dígitos.";
+ }
+
+ if (Object.keys(newErrors).length > 0) {
+ setErrors(newErrors);
+ return;
+ }
+
+ setErrors({});
+
+ router.push(
+ `/confirmar?rm=${rm}&nome=${encodeURIComponent(nome)}&cpf=${cpf}`
+ );
+ };
+
+ const formatCPF = (value: string) => {
+ const cpfClean = value.replace(/\D/g, "");
+ let formatted = cpfClean;
+
+ if (cpfClean.length > 3) {
+ formatted = cpfClean.substring(0, 3) + "." + cpfClean.substring(3);
+ }
+ if (cpfClean.length > 6) {
+ formatted = formatted.substring(0, 7) + "." + cpfClean.substring(6, 9);
+ }
+ if (cpfClean.length > 9) {
+ formatted = formatted.substring(0, 11) + "-" + cpfClean.substring(9, 11);
+ }
+
+ return formatted;
+ };
+
+ const handleCPFChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ const value = e.target.value;
+ const formatted = formatCPF(value);
+ setCpf(formatted);
+
+ if (errors.cpf) {
+ setErrors((prev) => ({ ...prev, cpf: undefined }));
+ }
+ };
+
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-[32px] row-start-2 items-center sm:items-start">
- <Image
- className="dark:invert"
- src="/next.svg"
- alt="Next.js logo"
- width={180}
- height={38}
- priority
- />
- <ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
- <li className="mb-2 tracking-[-.01em]">
- Get started by editing{" "}
- <code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
- app/page.tsx
- </code>
- .
- </li>
- <li className="tracking-[-.01em]">
- 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] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
- 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="/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 font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
- 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 className="flex min-h-screen flex-col items-center justify-center bg-[#f0f5fa]">
+ <div className="w-full max-w-md">
+ <div className="mb-6 flex items-center justify-center">
+ <div className="flex flex-col items-center">
+ <div className="mb-2 text-center text-3xl font-bold text-[#004a93]">
+ JUSTIÇA ELEITORAL
+ </div>
+ <div className="h-2 w-full bg-gradient-to-r from-[#009c3b] via-[#ffdf00] to-[#002776]"></div>
+ </div>
+ </div>
+
+ <Card className="border-2 border-[#004a93] shadow-lg overflow-hidden">
+ <CardHeader className="bg-[#004a93] text-center text-white">
+ <CardTitle className="text-2xl">ELEIÇÕES ESTUDANTIS</CardTitle>
+ <CardDescription className="text-gray-100">
+ Identificação do Eleitor
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="pt-6">
+ <form onSubmit={handleSubmit} className="space-y-4">
+ <div className="space-y-2">
+ <label
+ htmlFor="rm"
+ className="text-sm font-medium text-[#004a93]"
+ >
+ Digite seu RM:
+ </label>
+ <Input
+ id="rm"
+ type="text"
+ value={rm}
+ onChange={(e) => {
+ setRm(e.target.value);
+ if (errors.rm) {
+ setErrors((prev) => ({ ...prev, rm: undefined }));
+ }
+ }}
+ placeholder="Digite os 5 dígitos do seu RM"
+ className="border-2 border-[#004a93]"
+ maxLength={5}
+ />
+ {errors.rm && (
+ <p className="text-sm text-red-500">{errors.rm}</p>
+ )}
+ </div>
+
+ <div className="space-y-2">
+ <label
+ htmlFor="nome"
+ className="text-sm font-medium text-[#004a93]"
+ >
+ Digite seu nome completo:
+ </label>
+ <Input
+ id="nome"
+ type="text"
+ value={nome}
+ onChange={(e) => {
+ setNome(e.target.value);
+ if (errors.nome) {
+ setErrors((prev) => ({ ...prev, nome: undefined }));
+ }
+ }}
+ placeholder="Digite seu nome completo"
+ className="border-2 border-[#004a93]"
+ />
+ {errors.nome && (
+ <p className="text-sm text-red-500">{errors.nome}</p>
+ )}
+ </div>
+
+ <div className="space-y-2">
+ <label
+ htmlFor="cpf"
+ className="text-sm font-medium text-[#004a93]"
+ >
+ Digite seu CPF:
+ </label>
+ <Input
+ id="cpf"
+ type="text"
+ value={cpf}
+ onChange={handleCPFChange}
+ placeholder="Digite seu CPF"
+ className="border-2 border-[#004a93]"
+ maxLength={14}
+ />
+ {errors.cpf && (
+ <p className="text-sm text-red-500">{errors.cpf}</p>
+ )}
+ </div>
+
+ <Button
+ type="submit"
+ className="w-full bg-[#004a93] text-white hover:bg-[#003a73]"
+ >
+ CONFIRMAR
+ </Button>
+ </form>
+ </CardContent>
+ <CardFooter className="flex justify-center border-t border-[#004a93] bg-[#f8f8f8] py-3 text-sm text-[#004a93] rounded-b-lg">
+ Seu voto é secreto e seguro.
+ </CardFooter>
+ </Card>
+
+ <div className="mt-4 flex justify-center">
+ <div className="text-center text-sm text-[#004a93]">
+ © {new Date().getFullYear()} Justiça Eleitoral Estudantil
+ </div>
</div>
- </main>
- <footer className="row-start-3 flex gap-[24px] 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="/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="/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="/globe.svg"
- alt="Globe icon"
- width={16}
- height={16}
- />
- Go to nextjs.org →
- </a>
- </footer>
+ </div>
</div>
);
}
diff --git a/app/votar/loading.tsx b/app/votar/loading.tsx
new file mode 100644
index 0000000..4349ac3
--- /dev/null
+++ b/app/votar/loading.tsx
@@ -0,0 +1,3 @@
+export default function Loading() {
+ return null;
+}
diff --git a/app/votar/page.tsx b/app/votar/page.tsx
new file mode 100644
index 0000000..8946f73
--- /dev/null
+++ b/app/votar/page.tsx
@@ -0,0 +1,113 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+
+export default function VotarPage() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const rm = searchParams.get("rm") || "";
+ const nome = searchParams.get("nome") || "";
+ const cpf = searchParams.get("cpf") || "";
+ const [selectedOption, setSelectedOption] = useState<string | null>(null);
+ const [audioContext, setAudioContext] = useState<AudioContext | null>(null);
+
+ useEffect(() => {
+ if (!rm || !nome || !cpf) {
+ router.push("/");
+ return;
+ }
+
+ setAudioContext(
+ new (window.AudioContext || (window as any).webkitContext)()
+ );
+ }, [rm, nome, cpf, router]);
+
+ const handleVote = (option: string) => {
+ setSelectedOption(option);
+
+ if (!audioContext) return;
+
+ const oscillator = audioContext.createOscillator();
+ const gainNode = audioContext.createGain();
+
+ oscillator.type = "sine";
+ oscillator.frequency.setValueAtTime(1000, audioContext.currentTime);
+ gainNode.gain.setValueAtTime(0.5, audioContext.currentTime);
+
+ oscillator.connect(gainNode);
+ gainNode.connect(audioContext.destination);
+
+ oscillator.start();
+ oscillator.stop(audioContext.currentTime + 0.2);
+
+ setTimeout(() => {
+ router.push(
+ `/obrigado?rm=${rm}&nome=${encodeURIComponent(
+ nome
+ )}&cpf=${cpf}&option=${option}`
+ );
+ }, 500);
+ };
+
+ return (
+ <div className="flex min-h-screen flex-col items-center justify-center bg-[#f0f5fa]">
+ <div className="w-full max-w-md">
+ <div className="mb-6 flex items-center justify-center">
+ <div className="flex flex-col items-center">
+ <div className="mb-2 text-center text-3xl font-bold text-[#004a93]">
+ JUSTIÇA ELEITORAL
+ </div>
+ <div className="h-2 w-full bg-gradient-to-r from-[#009c3b] via-[#ffdf00] to-[#002776]"></div>
+ </div>
+ </div>
+
+ <Card className="border-2 border-[#004a93] shadow-lg overflow-hidden">
+ <CardHeader className="bg-[#004a93] text-center text-white">
+ <CardTitle className="text-2xl">SEU VOTO PARA</CardTitle>
+ <CardDescription className="text-gray-100">
+ CHAPA DO GREMIO ESTUDANTIL
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="space-y-6 p-6 rounded-b-lg">
+ <div className="grid grid-cols-2 gap-6">
+ <Button
+ onClick={() => handleVote("SIE")}
+ className="flex h-40 flex-col items-center justify-center border-2 border-[#004a93] bg-white p-4 text-xl font-bold text-[#004a93] hover:bg-[#e6f0fa]"
+ variant="outline"
+ >
+ <div className="mb-2 text-4xl">1</div>
+ SIE
+ </Button>
+ <Button
+ onClick={() => handleVote("Liderança Jovem")}
+ className="flex h-40 flex-col items-center justify-center border-2 border-[#004a93] bg-white p-4 text-xl font-bold text-[#004a93] hover:bg-[#e6f0fa]"
+ variant="outline"
+ >
+ <div className="mb-2 text-4xl">2</div>
+ Liderança Jovem
+ </Button>
+ </div>
+ <div className="mt-4 text-center text-sm text-[#004a93]">
+ Toque no quadro correspondente para VOTAR
+ </div>
+ </CardContent>
+ </Card>
+
+ <div className="mt-4 flex justify-center">
+ <div className="text-center text-sm text-[#004a93]">
+ © {new Date().getFullYear()} Justiça Eleitoral Estudantil
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}