From 33a17452845a86f83725be498e0db5e8f9aa70be Mon Sep 17 00:00:00 2001 From: Jean Jacques Avril Date: Sun, 9 Mar 2025 07:26:16 +0000 Subject: [PATCH] might be trash, cause llm messed up the files --- data/editable_text.json | 82 +++- data/responses.json | 1 + src/app/(full)/admin/page.tsx | 68 ++- src/app/(full)/api/editable-text/route.ts | 207 ++++---- src/app/(full)/api/generate-token/route.ts | 7 +- src/app/(full)/api/submit-vote/route.ts | 97 ++-- src/app/(full)/vote/page.tsx | 166 ++++++- src/components/admin/StatsButton.tsx | 19 +- src/components/admin/StatsDisplay.tsx | 113 ++++- src/components/admin/VoteOptionsManager.tsx | 503 +++++++++++--------- src/lib/auth.ts | 89 +--- src/lib/server-auth.ts | 79 ++- src/lib/survey.ts | 207 +++++++- 13 files changed, 1123 insertions(+), 515 deletions(-) diff --git a/data/editable_text.json b/data/editable_text.json index ff806d7..dbaeb71 100755 --- a/data/editable_text.json +++ b/data/editable_text.json @@ -5,7 +5,87 @@ }, { "id": "current-vote-text", - "content": "

Derzeit läuft eine Abstimmung zur Änderung der Vereinssatzung.

Bitte nutzen Sie den Ihnen zugesandten Link, um an der Abstimmung teilzunehmen.

Bei Fragen wenden Sie sich bitte an den Vorstand.

" + "content": "

Derzeit laufen mehrere Abstimmungen zu Vereinsangelegenheiten.

Bitte nutzen Sie den Ihnen zugesandten Link, um an den Abstimmungen teilzunehmen.

Bei Fragen wenden Sie sich bitte an den Vorstand.

" + }, + { + "id": "polls", + "content": [ + { + "id": "satzung-2025", + "title": "Änderung der Vereinssatzung", + "question": "

Stimmen Sie der vorgeschlagenen Änderung der Vereinssatzung zu?

", + "options": [ + { + "id": "abstain", + "label": "Ich enthalte mich234" + }, + { + "id": "va9h6snd", + "label": "Das Reicht" + }, + { + "id": "yes", + "label": "Ja, ich stimme zu" + }, + { + "id": "1w5u17px", + "label": "5674567" + }, + { + "id": "no", + "label": "Neinneinenen" + }, + { + "id": "2puzfaka", + "label": "2345tg2" + } + ], + "active": true, + "createdAt": "2025-03-01T12:00:00.000Z" + }, + { + "id": "9ms9oij4", + "title": "qwe", + "question": "

Stimmen Sie dem Vorschlag zu?", + "options": [ + { + "id": "yes", + "label": "Ja, ich stimme zu" + }, + { + "id": "no", + "label": "Nein, ich stimme nicht zu" + }, + { + "id": "abstain", + "label": "Ich enthalte mich" + } + ], + "active": true, + "createdAt": "2025-03-08T22:07:06.863Z" + }, + { + "id": "dvp2ibas", + "title": "titel eingabe test", + "question": "frage eingabe test", + "options": [ + { + "id": "yes", + "label": "Ja, ich stimme zu" + }, + { + "id": "no", + "label": "Nein, ich stimme nicht zu" + }, + { + "id": "abstain", + "label": "Ich enthalte mich" + } + ], + "active": true, + "createdAt": "2025-03-08T22:12:56.471Z" + } + ] }, { "id": "vote-question", diff --git a/data/responses.json b/data/responses.json index 39d21f0..bbc3357 100755 --- a/data/responses.json +++ b/data/responses.json @@ -1,6 +1,7 @@ [ { "id": "0dfdb724-b6f2-47db-917b-0d233bef5e93", + "pollId": "satzung-2025", "vote": "abstain", "comment": "ich was.", "timestamp": "2025-03-02T19:12:44.209Z" diff --git a/src/app/(full)/admin/page.tsx b/src/app/(full)/admin/page.tsx index ca56e49..9acb9c3 100644 --- a/src/app/(full)/admin/page.tsx +++ b/src/app/(full)/admin/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState, useEffect } from 'react'; -import { VoteOption } from '@/lib/survey'; +import { VoteOption, Poll } from '@/lib/survey'; import { LoginForm, TextEditor, @@ -20,8 +20,14 @@ interface Stats { [key: string]: number; // Allow dynamic keys for vote options } +interface PollStats { + all: Stats; + byPoll: Record; +} + interface Comment { vote: VoteOption; + pollId?: string; comment: string; timestamp: string; } @@ -33,7 +39,7 @@ interface EditableText { export default function AdminPage() { const [showStats, setShowStats] = useState(false); - const [stats, setStats] = useState(null); + const [stats, setStats] = useState(null); const [comments, setComments] = useState([]); const [isAuthenticated, setIsAuthenticated] = useState(false); const [showEditor, setShowEditor] = useState(false); @@ -42,6 +48,20 @@ export default function AdminPage() { { id: 'current-vote-text', content: '

Derzeit läuft eine Abstimmung zur Änderung der Vereinssatzung.

Bitte nutzen Sie den Ihnen zugesandten Link, um an der Abstimmung teilzunehmen.

Bei Fragen wenden Sie sich bitte an den Vorstand.

' }, { id: 'vote-question', content: '

Stimmen Sie der vorgeschlagenen Änderung der Vereinssatzung zu?

' } ]); + const [polls, setPolls] = useState([ + { + id: 'default-poll', + title: 'Default Poll', + question: '

Stimmen Sie der vorgeschlagenen Änderung der Vereinssatzung zu?

', + options: [ + { id: 'yes', label: 'Ja, ich stimme zu' }, + { id: 'no', label: 'Nein, ich stimme nicht zu' }, + { id: 'abstain', label: 'Ich enthalte mich' } + ], + active: true, + createdAt: new Date().toISOString() + } + ]); const [voteOptions, setVoteOptions] = useState<{ id: string; label: string }[]>([ { id: 'yes', label: 'Ja, ich stimme zu' }, { id: 'no', label: 'Nein, ich stimme nicht zu' }, @@ -99,13 +119,33 @@ export default function AdminPage() { if (response.ok) { const data = await response.json(); if (data.texts && Array.isArray(data.texts)) { - setEditableTexts(data.texts); + // Filter out polls entry for the regular editable texts + const regularTexts = data.texts.filter((text: { id: string }) => text.id !== 'polls'); + setEditableTexts(regularTexts); - // Find and set vote options + // Find and set vote options (for backward compatibility) const voteOptionsEntry = data.texts.find((text: { id: string }) => text.id === 'vote-options'); if (voteOptionsEntry && Array.isArray(voteOptionsEntry.content)) { setVoteOptions(voteOptionsEntry.content); } + + // Find and set polls + const pollsEntry = data.texts.find((text: { id: string }) => text.id === 'polls'); + if (pollsEntry && Array.isArray(pollsEntry.content)) { + setPolls(pollsEntry.content); + } else if (voteOptionsEntry) { + // If no polls entry exists but vote options do, create a default poll + const voteQuestionEntry = data.texts.find((text: { id: string }) => text.id === 'vote-question'); + const defaultPoll: Poll = { + id: 'default-poll', + title: 'Abstimmung', + question: voteQuestionEntry ? voteQuestionEntry.content : '

Stimmen Sie dem Vorschlag zu?

', + options: voteOptionsEntry.content, + active: true, + createdAt: new Date().toISOString() + }; + setPolls([defaultPoll]); + } } } } catch (err) { @@ -155,12 +195,25 @@ export default function AdminPage() { } }; - const handleStatsLoaded = (loadedStats: Stats, loadedComments: Comment[]) => { + const handleStatsLoaded = (loadedStats: Stats | PollStats, loadedComments: Comment[], loadedPolls?: Poll[]) => { setStats(loadedStats); setComments(loadedComments); + if (loadedPolls) { + setPolls(loadedPolls); + } setShowStats(true); }; + const handlePollsChange = (newPolls: Poll[]) => { + setPolls(newPolls); + + // For backward compatibility, update voteOptions with the first active poll's options + const activePoll = newPolls.find(poll => poll.active); + if (activePoll) { + setVoteOptions(activePoll.options); + } + }; + const handleVoteOptionsChange = (newOptions: { id: string; label: string }[]) => { setVoteOptions(newOptions); }; @@ -232,8 +285,8 @@ export default function AdminPage() { /> setShowStats(false)} /> )} diff --git a/src/app/(full)/api/editable-text/route.ts b/src/app/(full)/api/editable-text/route.ts index bdafdde..45577a1 100644 --- a/src/app/(full)/api/editable-text/route.ts +++ b/src/app/(full)/api/editable-text/route.ts @@ -1,4 +1,5 @@ -import { checkAdminAuth } from '@/lib/auth'; +import { comparePassword } from '@/lib/auth'; +import { getMemberCredentials } from '@/lib/server-auth'; import fs from 'fs'; import { NextRequest, NextResponse } from 'next/server'; import path from 'path'; @@ -8,127 +9,125 @@ const TEXT_FILE = path.join(process.cwd(), 'data', 'editable_text.json'); export type EditableText = { id: string; content: string }; // Ensure the data directory exists function ensureDataDirectory() { - const dataDir = path.join(process.cwd(), 'data'); - if (!fs.existsSync(dataDir)) { - fs.mkdirSync(dataDir, { recursive: true }); - } + const dataDir = path.join(process.cwd(), 'data'); + if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); + } } // Get all editable texts function getEditableTexts() { - ensureDataDirectory(); + ensureDataDirectory(); - if (!fs.existsSync(TEXT_FILE)) { - // Default texts - const defaultTexts = [ - { - id: 'welcome-text', - content: '

Herzlich willkommen bei der Online-Abstimmungsplattform des SCHAFWASCHENER SEGELVEREIN CHIEMSEE E.V. RIMSTING.

Diese Plattform ermöglicht es Mitgliedern, sicher und bequem über Vereinsangelegenheiten abzustimmen.

' - }, - { - id: 'current-vote-text', - content: '

Derzeit läuft eine Abstimmung zur Änderung der Vereinssatzung.

Bitte nutzen Sie den Ihnen zugesandten Link, um an der Abstimmung teilzunehmen.

Bei Fragen wenden Sie sich bitte an den Vorstand.

' - }, - { - id: 'vote-question', - content: '

Stimmen Sie der vorgeschlagenen Änderung der Vereinssatzung zu?

' - }, - { - id: 'vote-options', - content: [ - { - id: 'yes', - label: 'Ja, ich stimme zu' - }, - { - id: 'no', - label: 'Nein, ich stimme nicht zu' - }, - { - id: 'abstain', - label: 'Ich enthalte mich' - } - ] - } - ]; + if (!fs.existsSync(TEXT_FILE)) { + // Default texts + const defaultTexts = [ + { + id: 'welcome-text', + content: '

Herzlich willkommen bei der Online-Abstimmungsplattform des SCHAFWASCHENER SEGELVEREIN CHIEMSEE E.V. RIMSTING.

Diese Plattform ermöglicht es Mitgliedern, sicher und bequem über Vereinsangelegenheiten abzustimmen.

' + }, + { + id: 'current-vote-text', + content: '

Derzeit läuft eine Abstimmung zur Änderung der Vereinssatzung.

Bitte nutzen Sie den Ihnen zugesandten Link, um an der Abstimmung teilzunehmen.

Bei Fragen wenden Sie sich bitte an den Vorstand.

' + }, + { + id: 'vote-question', + content: '

Stimmen Sie der vorgeschlagenen Änderung der Vereinssatzung zu?

' + }, + { + id: 'vote-options', + content: [ + { + id: 'yes', + label: 'Ja, ich stimme zu' + }, + { + id: 'no', + label: 'Nein, ich stimme nicht zu' + }, + { + id: 'abstain', + label: 'Ich enthalte mich' + } + ] + } + ]; - fs.writeFileSync(TEXT_FILE, JSON.stringify(defaultTexts, null, 2)); - return defaultTexts; - } + fs.writeFileSync(TEXT_FILE, JSON.stringify(defaultTexts, null, 2)); + return defaultTexts; + } - const data = fs.readFileSync(TEXT_FILE, 'utf-8'); - return JSON.parse(data); + const data = fs.readFileSync(TEXT_FILE, 'utf-8'); + return JSON.parse(data); } // Save editable texts - function saveEditableTexts(texts: EditableText[]) { - ensureDataDirectory(); - fs.writeFileSync(TEXT_FILE, JSON.stringify(texts, null, 2)); + ensureDataDirectory(); + fs.writeFileSync(TEXT_FILE, JSON.stringify(texts, null, 2)); } // GET handler to retrieve all editable texts export async function GET() { - try { - const texts = getEditableTexts(); - return NextResponse.json({ texts }); - } catch (error) { - console.error('Error getting editable texts:', error); - return NextResponse.json( - { error: 'Failed to get editable texts' }, - { status: 500 } - ); - } + try { + const texts = getEditableTexts(); + return NextResponse.json({ texts }); + } catch (error) { + console.error('Error getting editable texts:', error); + return NextResponse.json( + { error: 'Failed to get editable texts' }, + { status: 500 } + ); + } } // POST handler to update an editable text (requires admin authentication) export async function POST(request: NextRequest) { - try { - const body = await request.json(); - // Check for admin auth - const { password, } = body; - const isAuthenticated = await checkAdminAuth(password); - if (!isAuthenticated) { - return NextResponse.json( - { error: 'Unauthorized' }, - { status: 401 } - ); + try { + const body = await request.json(); + // Check for admin auth + const { password } = body; + const members = getMemberCredentials(); + const isAuthenticated = members.some(member => comparePassword(password, member.password)); + if (!isAuthenticated) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + const { id, content } = body; + + if (!id || !content) { + return NextResponse.json( + { error: 'ID and content are required' }, + { status: 400 } + ); + } + + // Get current texts + const texts = getEditableTexts(); + + // Find and update the text with the given ID + const textIndex = texts.findIndex((text: EditableText) => text.id === id); + + if (textIndex === -1) { + // Text not found, add new + texts.push({ id, content }); + } else { + // Update existing text + texts[textIndex].content = content; + } + + // Save updated texts + saveEditableTexts(texts); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Error updating editable text:', error); + return NextResponse.json( + { error: 'Failed to update editable text' }, + { status: 500 } + ); } - - const { id, content } = body; - - if (!id || !content) { - return NextResponse.json( - { error: 'ID and content are required' }, - { status: 400 } - ); - } - - // Get current texts - const texts = getEditableTexts(); - - // Find and update the text with the given ID - const textIndex = texts.findIndex((text: EditableText - - ) => text.id === id); - - if (textIndex === -1) { - // Text not found, add new - texts.push({ id, content }); - } else { - // Update existing text - texts[textIndex].content = content; - } - - // Save updated texts - saveEditableTexts(texts); - - return NextResponse.json({ success: true }); - } catch (error) { - console.error('Error updating editable text:', error); - return NextResponse.json( - { error: 'Failed to update editable text' }, - { status: 500 } - ); - } } diff --git a/src/app/(full)/api/generate-token/route.ts b/src/app/(full)/api/generate-token/route.ts index 641f870..69e3b6e 100644 --- a/src/app/(full)/api/generate-token/route.ts +++ b/src/app/(full)/api/generate-token/route.ts @@ -1,4 +1,6 @@ -import { checkAdminAuth, generateRandomToken } from '@/lib/auth'; +import { generateRandomToken } from '@/lib/auth'; +import { getMemberCredentials } from '@/lib/server-auth'; +import { comparePassword } from '@/lib/auth'; import { NextRequest, NextResponse } from 'next/server'; export async function POST(request: NextRequest) { @@ -6,7 +8,8 @@ export async function POST(request: NextRequest) { const body = await request.json(); // Check for admin auth const { password } = body; - const isAuthenticated = await checkAdminAuth(password); + const members = getMemberCredentials(); + const isAuthenticated = members.some(member => comparePassword(password, member.password)); if (!isAuthenticated) { return NextResponse.json( { error: 'Unauthorized' }, diff --git a/src/app/(full)/api/submit-vote/route.ts b/src/app/(full)/api/submit-vote/route.ts index 13a9c5b..d1fb2e2 100644 --- a/src/app/(full)/api/submit-vote/route.ts +++ b/src/app/(full)/api/submit-vote/route.ts @@ -1,49 +1,92 @@ import { NextRequest, NextResponse } from 'next/server'; import { verifyToken } from '@/lib/server-auth'; -import { saveResponse, VoteOption, getVoteOptions } from '@/lib/survey'; +import { generateRandomToken } from '@/lib/auth'; +import { + saveResponse, + VoteOption, + getVoteOptions, + getActivePolls, + getPoll, +} from '@/lib/survey'; +import { markMemberAsVoted } from '@/lib/server-auth'; export async function POST(request: NextRequest) { try { - // Get the token from the request - const { token, vote, comment } = await request.json(); - + // Get the token, vote data, and poll ID from the request + const { token, vote, comment, pollId } = await request.json(); + if (!token) { - return NextResponse.json( - { error: 'Token is required' }, - { status: 400 } - ); + return NextResponse.json({ error: 'Token is required' }, { status: 400 }); } - - // Get available vote options - const voteOptions = getVoteOptions(); + + // Determine which poll to use + let targetPollId = pollId; + + // If no pollId is provided, use the first active poll (for backward compatibility) + if (!targetPollId) { + const activePolls = getActivePolls(); + if (activePolls.length === 0) { + return NextResponse.json( + { error: 'No active polls available' }, + { status: 400 } + ); + } + targetPollId = activePolls[0].id; + } else { + // Verify the poll exists + const poll = getPoll(targetPollId); + if (!poll) { + return NextResponse.json({ error: 'Invalid poll ID' }, { status: 400 }); + } + + // Check if the poll is active + if (!poll.active) { + return NextResponse.json( + { error: 'This poll is no longer active' }, + { status: 400 } + ); + } + } + + // Get available vote options for this poll + const voteOptions = getVoteOptions(targetPollId); const validOptionIds = voteOptions.map(option => option.id); - + if (!vote || !validOptionIds.includes(vote)) { return NextResponse.json( { error: 'Valid vote option is required' }, { status: 400 } ); } - - // Verify the token - const { valid } = await verifyToken(token); - + + // Verify the token and check if already voted in this poll + const { valid, memberNumber, votedPolls } = await verifyToken(token); + if (!valid) { + return NextResponse.json({ error: 'Invalid token' }, { status: 401 }); + } + + if (votedPolls?.includes(targetPollId)) { return NextResponse.json( - { error: 'Invalid or already used token' }, - { status: 401 } + { error: 'Already voted in this poll' }, + { status: 400 } ); } - - // Save the response - const response = saveResponse(vote as VoteOption, comment); - - return NextResponse.json({ success: true, response }); + + // Save the response with the poll ID + const response = saveResponse(targetPollId, vote as VoteOption, comment); + + // Mark the member as voted + if (memberNumber) { + markMemberAsVoted(memberNumber, targetPollId); + } + + // If token is valid, generate a new token with pollId and memberNumber + const newToken = await generateRandomToken(memberNumber, targetPollId); + + return NextResponse.json({ success: true, response, token: newToken }); // Return new token } catch (error) { console.error('Error submitting vote:', error); - return NextResponse.json( - { error: 'Failed to submit vote' }, - { status: 500 } - ); + return NextResponse.json({ error: 'Failed to submit vote' }, { status: 500 }); } } diff --git a/src/app/(full)/vote/page.tsx b/src/app/(full)/vote/page.tsx index 666dba4..b94b855 100644 --- a/src/app/(full)/vote/page.tsx +++ b/src/app/(full)/vote/page.tsx @@ -4,8 +4,7 @@ import { useState, useEffect, Suspense } from 'react'; import { useSearchParams, useRouter } from 'next/navigation'; import Link from 'next/link'; import { EditableText } from '@/components/EditableText'; -import { VoteOption, VoteOptionConfig } from '@/lib/survey'; - +import { VoteOption, VoteOptionConfig, Poll } from '@/lib/survey'; export default function VotePage() { return ( @@ -23,40 +22,90 @@ function VotePageContent() { const searchParams = useSearchParams(); const router = useRouter(); const token = searchParams.get('token'); + const pollIdParam = searchParams.get('pollId'); - const [voteOptions, setVoteOptions] = useState([]); + const [polls, setPolls] = useState([]); + const [selectedPollId, setSelectedPollId] = useState(null); const [selectedOption, setSelectedOption] = useState(null); const [comment, setComment] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [isSubmitted, setIsSubmitted] = useState(false); - // Fetch vote options + const [submittedPollIds, setSubmittedPollIds] = useState(() => { + if (typeof window !== 'undefined') { + const storedPollIds = sessionStorage.getItem('submittedPollIds'); + return storedPollIds ? JSON.parse(storedPollIds) : []; + } + return []; + }); + + // Fetch polls useEffect(() => { - const fetchVoteOptions = async () => { + const fetchPolls = async () => { try { const response = await fetch('/api/editable-text'); if (response.ok) { const data = await response.json(); if (data.texts && Array.isArray(data.texts)) { - const voteOptionsEntry = data.texts.find((text: { id: string }) => text.id === 'vote-options'); - if (voteOptionsEntry && Array.isArray(voteOptionsEntry.content)) { - setVoteOptions(voteOptionsEntry.content); + const pollsEntry = data.texts.find((text: { id: string }) => text.id === 'polls'); + + if (pollsEntry && Array.isArray(pollsEntry.content)) { + // Filter only active polls + const activePolls = pollsEntry.content.filter((poll: Poll) => poll.active); + setPolls(activePolls); + + // If a poll ID was specified in the URL, select it + if (pollIdParam && activePolls.some((poll: Poll) => poll.id === pollIdParam)) { + setSelectedPollId(pollIdParam); + } + // Otherwise select the first active poll + else if (activePolls.length > 0) { + setSelectedPollId(activePolls[0].id); + } + } else { + // Fallback to legacy format + const voteOptionsEntry = data.texts.find((text: { id: string }) => text.id === 'vote-options'); + const voteQuestionEntry = data.texts.find((text: { id: string }) => text.id === 'vote-question'); + + if (voteOptionsEntry && Array.isArray(voteOptionsEntry.content) && voteQuestionEntry) { + const legacyPoll: Poll = { + id: 'default-poll', + title: 'Abstimmung', + question: voteQuestionEntry.content, + options: voteOptionsEntry.content, + active: true, + createdAt: new Date().toISOString() + }; + + setPolls([legacyPoll]); + setSelectedPollId('default-poll'); + } } } } } catch (error) { - console.error('Error fetching vote options:', error); - // Use default options if fetch fails - setVoteOptions([ - { id: 'yes', label: 'Ja, ich stimme zu' }, - { id: 'no', label: 'Nein, ich stimme nicht zu' }, - { id: 'abstain', label: 'Ich enthalte mich' } - ]); + console.error('Error fetching polls:', error); + // Use default poll if fetch fails + const defaultPoll: Poll = { + id: 'default-poll', + title: 'Abstimmung', + question: '

Stimmen Sie der vorgeschlagenen Änderung der Vereinssatzung zu?

', + options: [ + { id: 'yes', label: 'Ja, ich stimme zu' }, + { id: 'no', label: 'Nein, ich stimme nicht zu' }, + { id: 'abstain', label: 'Ich enthalte mich' } + ], + active: true, + createdAt: new Date().toISOString() + }; + + setPolls([defaultPoll]); + setSelectedPollId('default-poll'); } }; - fetchVoteOptions(); - }, []); + fetchPolls(); + }, [pollIdParam]); // Redirect if no token is provided useEffect(() => { @@ -65,9 +114,19 @@ function VotePageContent() { } }, [token, router]); + const handlePollChange = (pollId: string) => { + setSelectedPollId(pollId); + setSelectedOption(null); + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + if (!selectedPollId) { + setError('Keine aktive Abstimmung verfügbar'); + return; + } + if (!selectedOption) { setError('Bitte wähle eine Option'); return; @@ -84,6 +143,7 @@ function VotePageContent() { }, body: JSON.stringify({ token, + pollId: selectedPollId, vote: selectedOption, comment: comment.trim() || undefined, }), @@ -95,7 +155,24 @@ function VotePageContent() { throw new Error(data.error || 'Fehler beim Übermitteln der Stimme'); } - setIsSubmitted(true); + // Add this poll to the list of submitted polls + setSubmittedPollIds(prev => { + const updatedPollIds = [...prev, selectedPollId]; + sessionStorage.setItem('submittedPollIds', JSON.stringify(updatedPollIds)); + return updatedPollIds; + }); + + // If there are more polls to vote on, select the next one + const remainingPolls = polls.filter(poll => !submittedPollIds.includes(poll.id) && poll.id !== selectedPollId); + + if (remainingPolls.length > 0) { + setSelectedPollId(remainingPolls[0].id); + setSelectedOption(null); + setComment(''); + } else { + // All polls have been voted on + setIsSubmitted(true); + } } catch (err) { setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten'); } finally { @@ -103,6 +180,9 @@ function VotePageContent() { } }; + // Get the current poll + const currentPoll = polls.find(poll => poll.id === selectedPollId); + if (isSubmitted) { return (
@@ -130,20 +210,66 @@ function VotePageContent() { ); } + if (!currentPoll) { + return ( +
+
+
+

Keine aktive Abstimmung verfügbar

+

+ Derzeit sind keine aktiven Abstimmungen verfügbar. +

+ + Zurück zur Startseite + +
+
+
+ ); + } + return (

ABSTIMMUNG

+ {polls.length > 1 && ( +
+
Wähle eine Abstimmung:
+
+ {polls.map(poll => ( + + ))} +
+
+ )} +
- +
- {voteOptions.map((option) => ( + {currentPoll.options.map((option) => (