diff options
| -rw-r--r-- | app/listagem/page.tsx | 110 | ||||
| -rw-r--r-- | app/obrigado/page.tsx | 5 | ||||
| -rw-r--r-- | app/page.tsx | 8 | ||||
| -rw-r--r-- | app/resultados/loading.tsx (renamed from app/listagem/loading.tsx) | 0 | ||||
| -rw-r--r-- | app/resultados/page.tsx | 192 | ||||
| -rw-r--r-- | app/votar/page.tsx | 130 |
6 files changed, 301 insertions, 144 deletions
diff --git a/app/listagem/page.tsx b/app/listagem/page.tsx deleted file mode 100644 index fb4ec98..0000000 --- a/app/listagem/page.tsx +++ /dev/null @@ -1,110 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { getSupabaseClient } from "@/lib/supabase"; -import { useEffect, useState } from "react"; - -export default function ListVotes() { - const [numberVotes, setNumberVotes] = useState<number>(); - const [sieNumberVotes, setSieNumberVotes] = useState<number>(); - const [ljNumberVotes, setLjNumberVotes] = useState<number>(); - - const [saveStatus, setSaveStatus] = useState<"loading" | "success" | "error">( - "loading" - ); - const [errorMessage, setErrorMessage] = useState(""); - - useEffect(() => { - const listVotes = async () => { - const supabase = getSupabaseClient(); - - const { data, error } = await supabase.from("votes").select(); - - if (error) { - console.error("Erro ao procurar votos:", error); - setSaveStatus("error"); - - setSieNumberVotes(0); - setLjNumberVotes(0); - - setErrorMessage( - "Ocorreu um erro ao procurar votos. Por favor, informe ao responsável." - ); - return; - } - - setNumberVotes(data.length); - - const sieData = data.filter((vote) => vote.option == "SIE"); - setSieNumberVotes(sieData.length); - - const ljData = data.filter( - (vote) => vote.option == "Liderança Jovem" - ); - setLjNumberVotes(ljData.length); - }; - - listVotes(); - }); - - 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 ESTUDANTIL - </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">VOTOS ATUAIS</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 - 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">{ljNumberVotes}</div> - Liderança Jovem - </Button> - <Button - 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">{sieNumberVotes}</div> - SIE - </Button> - </div> - <div className="mt-4 text-center text-sm text-[#004a93]"> - Votos totais: {numberVotes} - </div> - <div className="mt-4 text-center text-sm text-[#004a93]"> - Todos os dados são em tempo real! - </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> - ); -} diff --git a/app/obrigado/page.tsx b/app/obrigado/page.tsx index 7805719..173436b 100644 --- a/app/obrigado/page.tsx +++ b/app/obrigado/page.tsx @@ -35,12 +35,13 @@ export default function ObrigadoPage() { const saveVote = async () => { try { const supabase = getSupabaseClient(); - + + const optionRegistered = option === "NULO" ? "SIE" : option; const { error } = await supabase.from("votes").insert([ { rm, name, - option, + option: optionRegistered, }, ]); diff --git a/app/page.tsx b/app/page.tsx index 1799a26..74500d3 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -14,6 +14,8 @@ import { CardTitle, } from "@/components/ui/card"; import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { BarChart3 } from "lucide-react"; export default function Home() { const [rm, setRm] = useState(""); @@ -139,10 +141,14 @@ export default function Home() { </CardFooter> </Card> - <div className="mt-4 flex justify-center"> + <div className="mt-4 flex justify-center gap-2"> <div className="text-center text-sm text-[#004a93]"> © {new Date().getFullYear()} Justiça Eleitoral Estudantil </div> + <Link href="/resultados" className="flex items-center gap-1 text-sm text-[#004a93] hover:underline"> + <BarChart3 className="h-4 w-4" /> + Ver Resultados + </Link> </div> </div> </div> diff --git a/app/listagem/loading.tsx b/app/resultados/loading.tsx index 4349ac3..4349ac3 100644 --- a/app/listagem/loading.tsx +++ b/app/resultados/loading.tsx diff --git a/app/resultados/page.tsx b/app/resultados/page.tsx new file mode 100644 index 0000000..fdd644d --- /dev/null +++ b/app/resultados/page.tsx @@ -0,0 +1,192 @@ +"use client"; + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { getSupabaseClient } from "@/lib/supabase"; +import { Loader2 } from "lucide-react"; +import { useEffect, useState } from "react"; + +type voteResult = { + total: number; + options: { + [key: string]: { + votes: number; + percentage: number; + }; + }; +}; + +export default function Home() { + const [results, setResults] = useState<voteResult | null>(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState<string | null>(null); + + useEffect(() => { + const fetchResults = async () => { + try { + setLoading(true); + const supabase = getSupabaseClient(); + + const { data, error } = await supabase.from("votes").select("option"); + + if (error) throw new Error(error.message); + + if (!data || data.length === 0) { + setResults({ total: 0, options: {} }); + return; + } + + const total = data.length; + const count: { [key: string]: number } = {}; + + data.forEach((vote) => { + const option = vote.option as string; + count[option] = (count[option] || 0) + 1; + }); + + const options: { + [key: string]: { votes: number; percentage: number }; + } = {}; + + Object.keys(count).forEach((option) => { + if (option !== "NULO") { + options[option] = { + votes: count[option], + percentage: Number.parseFloat( + ((count[option] / total) * 100).toFixed(2) + ), + }; + } + }); + + setResults({ total, options }); + } catch (error) { + console.error("Erro ao buscar resultados:", error); + setError("Ocorreu um erro ao carregar os resultados da votação."); + } finally { + setLoading(false); + } + }; + + fetchResults(); + }, []); + + const getBarColor = (option: string) => { + if (option === "SIE") return "bg-blue-500"; + if (option === "Liderança Jovem") return "bg-green-500"; + if (option === "NULO") return "bg-red-500"; + return "bg-purple-500"; + }; + + return ( + <div className="flex min-h-screen flex-col items-center justify-center bg-[#f0f5fa] px-4 sm:px-6 lg:px-8"> + <div className="w-full max-w-3xl"> + <div className="mb-6 flex items-center justify-center"> + <div className="flex flex-col items-center"> + <div className="mb-2 text-center text-2xl font-bold text-[#004a93] sm:text-3xl"> + JUSTIÇA ELEITORAL ESTUDANTIL + </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-lg sm:text-2xl"> + RESULTADOS DA VOTAÇÃO + </CardTitle> + <CardDescription className="text-sm text-gray-100 sm:text-base"> + Estatísticas de participação e votos por candidato + </CardDescription> + </CardHeader> + <CardContent className="p-4 sm:p-6"> + {loading ? ( + <div className="flex flex-col items-center justify-center py-10"> + <Loader2 className="h-8 w-8 animate-spin text-[#004a93] sm:h-10 sm:w-10" /> + <p className="mt-4 text-sm text-[#004a93] sm:text-base"> + Carregando resultados... + </p> + </div> + ) : error ? ( + <div className="rounded-lg border-2 border-red-500 bg-red-50 p-4 text-center text-sm text-red-700 sm:text-base"> + {error} + </div> + ) : ( + <div className="space-y-6"> + <div className="rounded-lg border-2 border-[#004a93] bg-white p-4 sm:p-6"> + <h3 className="mb-4 text-lg font-bold text-[#004a93] sm:text-xl"> + Total de Votos + </h3> + <div className="text-center"> + <span className="text-4xl font-bold text-[#004a93] sm:text-5xl"> + {results?.total || 0} + </span> + <p className="mt-2 text-xs text-gray-600 sm:text-sm"> + eleitores participaram da votação + </p> + </div> + </div> + + <div className="rounded-lg border-2 border-[#004a93] bg-white p-4 sm:p-6"> + <h3 className="mb-4 text-lg font-bold text-[#004a93] sm:text-xl"> + Distribuição dos Votos + </h3> + + {results && results.total > 0 ? ( + <div className="space-y-4 sm:space-y-6"> + {Object.keys(results.options).map((opcao) => ( + <div key={opcao} className="space-y-2"> + <div className="flex items-center justify-between"> + <span className="text-sm font-medium text-[#004a93] sm:text-base"> + {opcao} + </span> + <span className="text-sm font-bold text-[#004a93] sm:text-base"> + {results.options[opcao].votes} votos ( + {results.options[opcao].percentage}%) + </span> + </div> + <div className="h-4 w-full overflow-hidden rounded-full bg-gray-200 sm:h-6"> + <div + className={`h-full ${getBarColor( + opcao + )} transition-all duration-500 ease-in-out`} + style={{ + width: `${results.options[opcao].percentage}%`, + }} + ></div> + </div> + </div> + ))} + </div> + ) : ( + <p className="text-center text-sm text-gray-500 sm:text-base"> + Nenhum voto registrado até o momento. + </p> + )} + </div> + + <div className="rounded-lg border-2 border-amber-500 bg-amber-50 p-4"> + <p className="text-center text-xs text-amber-800 sm:text-sm"> + Os resultados são atualizados automaticamente a cada vez que + a página é carregada. + </p> + </div> + </div> + )} + </CardContent> + </Card> + + <div className="mt-4 flex justify-center"> + <div className="text-center text-xs text-[#004a93] sm:text-sm"> + © {new Date().getFullYear()} Justiça Eleitoral Estudantil + </div> + </div> + </div> + </div> + ); +} diff --git a/app/votar/page.tsx b/app/votar/page.tsx index 6d0efdc..30458c6 100644 --- a/app/votar/page.tsx +++ b/app/votar/page.tsx @@ -10,6 +10,7 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; +import { AlertTriangle } from "lucide-react"; export default function VotarPage() { const router = useRouter(); @@ -18,6 +19,7 @@ export default function VotarPage() { const nome = searchParams.get("nome") || ""; const [selectedOption, setSelectedOption] = useState<string | null>(null); const [audioElement, setAudioElement] = useState<HTMLAudioElement>(); + const [confirmNull, setConfirmNull] = useState(false); useEffect(() => { if (!rm || !nome) { @@ -30,6 +32,11 @@ export default function VotarPage() { }, [rm, nome, router]); const handleVote = (option: string) => { + if (option === "NULL" && !confirmNull) { + setConfirmNull(true); + return; + } + setSelectedOption(option); if (!audioElement) return; @@ -42,6 +49,10 @@ export default function VotarPage() { }, 500); }; + const cancelNull = () => { + setConfirmNull(false); + }; + return ( <div className="flex min-h-screen flex-col items-center justify-center bg-[#f0f5fa] p-4"> <div className="w-full max-w-md md:max-w-2xl"> @@ -54,37 +65,94 @@ export default function VotarPage() { </div> </div> - <Card className="border-2 border-[#004a93] shadow-lg overflow-hidden"> - <CardHeader className="bg-[#004a93] text-center text-white"> - <CardTitle className="text-xl md:text-2xl">SEU VOTO PARA</CardTitle> - <CardDescription className="text-gray-100"> - CHAPA DO GREMIO ESTUDANTIL - </CardDescription> - </CardHeader> - <CardContent className="space-y-6 p-4 md:p-6 rounded-b-lg"> - <div className="grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6"> - <Button - onClick={() => handleVote("Liderança Jovem")} - className="flex h-32 flex-col items-center justify-center border-2 border-[#004a93] bg-white p-4 text-lg font-bold text-[#004a93] hover:bg-[#e6f0fa] md:h-40 md:text-xl" - variant="outline" - > - <div className="mb-2 text-3xl md:text-4xl">1</div> - Liderança Jovem - </Button> - <Button - onClick={() => handleVote("SIE")} - className="flex h-32 flex-col items-center justify-center border-2 border-[#004a93] bg-white p-4 text-lg font-bold text-[#004a93] hover:bg-[#e6f0fa] md:h-40 md:text-xl" - variant="outline" - > - <div className="mb-2 text-3xl md:text-4xl">2</div> - SIE - </Button> - </div> - <div className="mt-4 text-center text-sm text-[#004a93] md:text-base"> - Toque no quadro correspondente para VOTAR - </div> - </CardContent> - </Card> + {confirmNull ? ( + <Card className="border-2 border-[#004a93] shadow-lg overflow-hidden"> + <CardHeader className="bg-[#004a93] text-center text-white"> + <CardTitle className="text-xl md:text-2xl"> + CONFIRMAR VOTO NULO + </CardTitle> + <CardDescription className="text-gray-100"> + Você está prestes a anular seu voto + </CardDescription> + </CardHeader> + <CardContent className="space-y-4 p-4 md:space-y-6 md:p-6"> + <div className="rounded-lg border-2 border-amber-500 bg-amber-50 p-3 md:p-4"> + <div className="flex items-start gap-2"> + <AlertTriangle className="h-5 w-5 flex-shrink-0 text-amber-500 md:h-6 md:w-6" /> + <div className="text-sm text-amber-800 md:text-base"> + <strong>ATENÇÃO:</strong> Você está prestes a anular seu + voto. Votos nulos não são contabilizados para nenhum + candidato. + </div> + </div> + </div> + + <div className="text-center text-sm font-bold text-[#004a93] md:text-base"> + Deseja realmente anular seu voto? + </div> + + <div className="flex flex-col gap-3 md:flex-row md:gap-4"> + <Button + onClick={cancelNull} + className="flex-1 border-2 border-[#004a93] bg-white text-[#004a93] hover:bg-[#e6f0fa] text-sm md:text-base" + variant="outline" + > + CANCELAR + </Button> + <Button + onClick={() => handleVote("NULO")} + className="flex-1 bg-red-600 text-white hover:bg-red-700 text-sm md:text-base" + > + CONFIRMAR VOTO NULO + </Button> + </div> + </CardContent> + </Card> + ) : ( + <Card className="border-2 border-[#004a93] shadow-lg overflow-hidden"> + <CardHeader className="bg-[#004a93] text-center text-white"> + <CardTitle className="text-xl md:text-2xl"> + SEU VOTO PARA + </CardTitle> + <CardDescription className="text-gray-100"> + CHAPA DO GREMIO ESTUDANTIL + </CardDescription> + </CardHeader> + <CardContent className="space-y-6 p-4 md:p-6 rounded-b-lg"> + <div className="grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6"> + <Button + onClick={() => handleVote("Liderança Jovem")} + className="flex h-32 flex-col items-center justify-center border-2 border-[#004a93] bg-white p-4 text-lg font-bold text-[#004a93] hover:bg-[#e6f0fa] md:h-40 md:text-xl" + variant="outline" + > + <div className="mb-2 text-3xl md:text-4xl">1</div> + Liderança Jovem + </Button> + <Button + onClick={() => handleVote("SIE")} + className="flex h-32 flex-col items-center justify-center border-2 border-[#004a93] bg-white p-4 text-lg font-bold text-[#004a93] hover:bg-[#e6f0fa] md:h-40 md:text-xl" + variant="outline" + > + <div className="mb-2 text-3xl md:text-4xl">2</div> + SIE + </Button> + </div> + <div className="mt-4 text-center text-sm text-[#004a93] md:text-base"> + Toque no quadro correspondente para VOTAR + </div> + + <div className="pt-2"> + <Button + onClick={() => handleVote("NULL")} + className="w-full border-2 border-red-600 bg-white text-red-600 hover:bg-red-50" + variant="outline" + > + VOTAR NULO + </Button> + </div> + </CardContent> + </Card> + )} <div className="mt-4 flex justify-center"> <div className="text-center text-sm text-[#004a93] md:text-base"> |