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 => (
+
+ ))}
+
+
+ )}
+