Compare commits

..

2 Commits

Author SHA1 Message Date
2561446907 optimized iframe view 2025-03-02 21:43:19 +00:00
db195a4ae2 improvements 2025-03-02 21:09:39 +00:00
24 changed files with 738 additions and 30 deletions

View File

@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSVC Rimsting Abstimmung - Iframe Beispiel</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
h1 {
color: #0057a6;
margin-top: 0;
}
.iframe-container {
width: 100%;
margin-top: 20px;
border: 1px solid #ddd;
border-radius: 4px;
overflow: hidden;
}
iframe {
width: 100%;
height: 600px;
border: none;
}
.code-example {
background-color: #f0f7ff;
padding: 15px;
border-radius: 4px;
font-family: monospace;
margin-top: 20px;
overflow-x: auto;
}
.note {
margin-top: 20px;
padding: 10px;
background-color: #fffde7;
border-left: 4px solid #ffd600;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="container">
<h1>SSVC Rimsting Abstimmung - Iframe Beispiel</h1>
<p>Diese Seite zeigt, wie die Abstimmungskomponente als Iframe in eine externe Webseite eingebunden werden kann.
</p>
<h2>Live Beispiel</h2>
<div class="iframe-container">
<iframe src="/iframe" title="SSVC Rimsting Abstimmung" frameborder="0"></iframe>
</div>
<h2>HTML Code zum Einbinden</h2>
<div class="code-example">
&lt;iframe
src="https://ihre-domain.de/iframe"
width="100%"
height="600"
frameborder="0"
style="border:none;"
title="SSVC Rimsting Abstimmung"&gt;
&lt;/iframe&gt;
</div>
<div class="note">
<strong>Hinweis:</strong> Ersetzen Sie "https://ihre-domain.de/iframe" mit der tatsächlichen URL Ihrer
Abstimmungsseite.
<br><br>
Sie können auch einen Token direkt in der URL übergeben, um den Login-Schritt zu überspringen:
<br>
<code>https://ihre-domain.de/iframe?token=IHR_TOKEN</code>
</div>
</div>
</body>
</html>

View File

@ -1,7 +1,7 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import Link from "next/link"; import Link from "next/link";
import "./globals.css"; import "../globals.css";
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@ -55,8 +55,17 @@ export default function RootLayout({
<main> <main>
{children} {children}
</main> </main>
<footer className="ssvc-footer"> <footer className="ssvc-footer border-t border-gray-200 text-gray-700 py-3 mt-8">
<p>© {new Date().getFullYear()} SCHAFWASCHENER SEGELVEREIN CHIEMSEE E.V. RIMSTING. Alle Rechte vorbehalten.</p> <div className="container mx-auto px-4">
<div className="flex flex-col md:flex-row justify-between items-center">
<div className="mb-4 md:mb-0">
<p className="text-center md:text-left">© {new Date().getFullYear()} SCHAFWASCHENER SEGELVEREIN CHIEMSEE E.V. RIMSTING. Alle Rechte vorbehalten.</p>
</div>
<div className="text-center md:text-right">
<p className="whitespace-nowrap">Erstellt von <a href="https://jeanavril.com" target="_blank" rel="noopener noreferrer" className="font-medium text-gray-800 hover:text-blue-600 hover:underline transition-colors no-underline">Jean Jacques Avril</a></p>
</div>
</div>
</div>
</footer> </footer>
</body> </body>
</html> </html>

View File

@ -69,7 +69,7 @@ function VotePageContent() {
e.preventDefault(); e.preventDefault();
if (!selectedOption) { if (!selectedOption) {
setError('Bitte wählen Sie eine Option'); setError('Bitte wähle eine Option');
return; return;
} }
@ -115,7 +115,7 @@ function VotePageContent() {
</div> </div>
<h2 className="text-2xl font-bold text-[#0057a6] mb-2">Vielen Dank!</h2> <h2 className="text-2xl font-bold text-[#0057a6] mb-2">Vielen Dank!</h2>
<p className="text-gray-600"> <p className="text-gray-600">
Ihre Stimme wurde erfolgreich übermittelt. Deine Stimme wurde erfolgreich übermittelt.
</p> </p>
</div> </div>
@ -162,23 +162,26 @@ function VotePageContent() {
</div> </div>
</div> </div>
<div> <div className="p-4 bg-[#f0f7ff] border border-[#cce0ff] rounded-lg">
<label htmlFor="comment" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="comment" className="flex items-center text-base font-medium text-[#0057a6] mb-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
</svg>
Kommentare oder Vorschläge (Optional) Kommentare oder Vorschläge (Optional)
</label> </label>
<div className="mt-1"> <div>
<textarea <textarea
id="comment" id="comment"
name="comment" name="comment"
rows={4} rows={4}
value={comment} value={comment}
onChange={(e) => setComment(e.target.value)} onChange={(e) => setComment(e.target.value)}
placeholder="Bitte geben Sie keine persönlichen Daten an" placeholder="Dein Kommentar..."
className="w-full px-4 py-2 border border-gray-300 focus:outline-none focus:border-[#0057a6]" className="w-full px-4 py-2 border-2 border-gray-300 rounded-md bg-[#e6f0fa] focus:outline-none focus:border-[#0057a6] focus:ring-2 focus:ring-[#0057a6]/20 transition-all duration-200 shadow-sm"
/> />
</div> </div>
<p className="mt-1 text-sm text-gray-500"> <p className="mt-1 text-sm text-gray-500">
Bitte geben Sie keine persönlichen Daten in Ihren Kommentaren an. Bitte kurz fassen und auf konkretes Feedback beschränken.
</p> </p>
</div> </div>

View File

@ -0,0 +1,23 @@
import { ReactNode } from 'react';
import '@/app/globals.css';
import type { Metadata } from 'next';
// Define metadata for the iframe page
export const metadata: Metadata = {
title: 'SSVC Rimsting Abstimmung',
description: 'Abstimmungsseite des Schafwaschener Segelverein Rimsting',
};
// This is a minimal layout for iframe embedding without headers or footers
export default function IframeLayout({ children }: { children: ReactNode }) {
return (
<html lang="de">
<body className="bg-white">
<div className="iframe-content">
{children}
</div>
</body>
</html>
);
}

View File

@ -0,0 +1,328 @@
'use client';
import { useState, useEffect } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
import { EditableText } from '@/components/EditableText';
import { VoteOption, VoteOptionConfig } from '@/lib/survey';
// Special layout for iframe embedding - no header/footer
export default function IframePage() {
const router = useRouter();
const searchParams = useSearchParams();
const token = searchParams.get('token');
// Login state
const [memberNumber, setMemberNumber] = useState('');
const [password, setPassword] = useState('');
const [isLoginLoading, setIsLoginLoading] = useState(false);
const [loginError, setLoginError] = useState<string | null>(null);
const [isLoginEnabled, setIsLoginEnabled] = useState(true);
const [isLoggedIn, setIsLoggedIn] = useState(false);
// Vote state
const [voteOptions, setVoteOptions] = useState<VoteOptionConfig[]>([]);
const [selectedOption, setSelectedOption] = useState<VoteOption | null>(null);
const [comment, setComment] = useState('');
const [isVoteLoading, setIsVoteLoading] = useState(false);
const [voteError, setVoteError] = useState<string | null>(null);
const [isSubmitted, setIsSubmitted] = useState(false);
const [voteToken, setVoteToken] = useState<string | null>(token);
// Check if member authentication is enabled
useEffect(() => {
const checkSettings = async () => {
try {
const response = await fetch('/api/member-login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ memberNumber: '', password: '' }),
});
if (response.status === 403) {
setIsLoginEnabled(false);
}
} catch {
// Silently fail, assume enabled
}
};
checkSettings();
}, []);
// Fetch vote options
useEffect(() => {
const fetchVoteOptions = 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);
}
}
}
} 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' }
]);
}
};
fetchVoteOptions();
}, []);
// If token is provided in URL, consider user as logged in
useEffect(() => {
if (token) {
setIsLoggedIn(true);
setVoteToken(token);
}
}, [token]);
const handleLoginSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!memberNumber || !password) {
setLoginError('Bitte geben Sie Ihre Mitgliedsnummer und Ihr Passwort ein');
return;
}
setIsLoginLoading(true);
setLoginError(null);
try {
const response = await fetch('/api/member-login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ memberNumber, password }),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Anmeldung fehlgeschlagen');
}
// Set the token and mark as logged in
setVoteToken(data.token);
setIsLoggedIn(true);
} catch (err) {
setLoginError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten');
} finally {
setIsLoginLoading(false);
}
};
const handleVoteSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!selectedOption) {
setVoteError('Bitte wähle eine Option');
return;
}
setIsVoteLoading(true);
setVoteError(null);
try {
const response = await fetch('/api/submit-vote', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
token: voteToken,
vote: selectedOption,
comment: comment.trim() || undefined,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Fehler beim Übermitteln der Stimme');
}
setIsSubmitted(true);
} catch (err) {
setVoteError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten');
} finally {
setIsVoteLoading(false);
}
};
// Login form
if (!isLoggedIn) {
if (!isLoginEnabled) {
return (
<div className="px-4 py-4">
<div className="mb-4">
<div className="ssvc-main-content">
<div className="text-center p-4">
<div className="mb-4 text-red-600">
Die Mitgliederanmeldung ist derzeit deaktiviert.
</div>
</div>
</div>
</div>
</div>
);
}
return (
<div className="px-4 py-4">
<div className="mb-4">
<div className="ssvc-main-content">
<div className="mb-4">
<EditableText id="current-vote-text" />
</div>
<form onSubmit={handleLoginSubmit} className="space-y-3">
<div>
<label htmlFor="memberNumber" className="block text-sm font-medium text-gray-700 mb-1">
Mitgliedsnummer
</label>
<input
type="text"
id="memberNumber"
value={memberNumber}
onChange={(e) => setMemberNumber(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 focus:outline-none focus:border-[#0057a6]"
required
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
Passwort
</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 focus:outline-none focus:border-[#0057a6]"
required
/>
</div>
{loginError && (
<div className="text-red-500 text-sm">{loginError}</div>
)}
<button
type="submit"
disabled={isLoginLoading}
className="ssvc-button w-full disabled:opacity-50"
>
{isLoginLoading ? 'Anmelden...' : 'Anmelden'}
</button>
</form>
</div>
</div>
</div>
);
}
// Vote form
if (isSubmitted) {
return (
<div className="px-4 py-4">
<div className="ssvc-main-content text-center">
<div className="mb-4">
<div className="w-12 h-12 bg-[#e6f0fa] flex items-center justify-center mx-auto mb-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-[#0057a6]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<h2 className="text-xl font-bold text-[#0057a6] mb-2">Vielen Dank!</h2>
<p className="text-gray-600">
Deine Stimme wurde erfolgreich übermittelt.
</p>
</div>
</div>
</div>
);
}
return (
<div className="px-4 py-4">
<div className="mb-4">
<div className="ssvc-main-content">
<form onSubmit={handleVoteSubmit} className="space-y-4">
<div>
<div className="text-lg font-bold text-[#0057a6] mb-3">
<EditableText id="vote-question" />
</div>
<div className="space-y-2">
{voteOptions.map((option) => (
<label
key={option.id}
className="flex items-center p-2 border border-gray-200 cursor-pointer hover:bg-[#e6f0fa] transition-colors"
>
<input
type="radio"
name="vote"
value={option.id}
checked={selectedOption === option.id}
onChange={() => setSelectedOption(option.id)}
className="h-4 w-4 text-[#0057a6] border-gray-300"
/>
<span className="ml-2 text-gray-800">{option.label}</span>
</label>
))}
</div>
</div>
<div className="p-3 bg-[#f0f7ff] border border-[#cce0ff] rounded-lg">
<label htmlFor="comment" className="flex items-center text-sm font-medium text-[#0057a6] mb-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
</svg>
Kommentare oder Vorschläge (Optional)
</label>
<div>
<textarea
id="comment"
name="comment"
rows={3}
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="Dein Kommentar..."
className="w-full px-3 py-2 border-2 border-gray-300 rounded-md bg-[#e6f0fa] focus:outline-none focus:border-[#0057a6] focus:ring-2 focus:ring-[#0057a6]/20 transition-all duration-200 shadow-sm"
/>
</div>
<p className="mt-1 text-xs text-gray-500">
Bitte kurz fassen und auf konkretes Feedback beschränken.
</p>
</div>
{voteError && (
<div className="text-red-500 text-sm">{voteError}</div>
)}
<button
type="submit"
disabled={isVoteLoading}
className="ssvc-button w-full disabled:opacity-50"
>
{isVoteLoading ? 'Wird übermittelt...' : 'Stimme abgeben'}
</button>
</form>
</div>
</div>
</div>
);
}

View File

@ -80,9 +80,11 @@ body {
} }
.ssvc-footer { .ssvc-footer {
margin-top: 2rem;
padding: 1rem;
text-align: center;
font-size: 0.875rem; font-size: 0.875rem;
color: #666; }
.ssvc-footer a {
font-weight: 500;
text-decoration: none;
transition: opacity 0.2s;
} }

View File

@ -111,7 +111,14 @@ export default function TokenGenerator() {
<div className="mt-6 p-4 bg-[#e6f0fa] w-full"> <div className="mt-6 p-4 bg-[#e6f0fa] w-full">
<h3 className="font-medium text-[#0057a6] mb-2">Generierter Abstimmungslink:</h3> <h3 className="font-medium text-[#0057a6] mb-2">Generierter Abstimmungslink:</h3>
<div className="break-all text-sm text-[#0057a6] mb-2"> <div className="break-all text-sm text-[#0057a6] mb-2">
{generatedLink} <a
href={generatedLink}
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
{generatedLink}
</a>
</div> </div>
<button <button
onClick={copyToClipboard} onClick={copyToClipboard}

View File

@ -1,6 +1,22 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState, useRef, useEffect } from 'react';
// Function to generate a random ID
function generateRandomId(existingIds: string[]): string {
const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
const length = 8;
let result: string;
do {
result = '';
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
} while (existingIds.includes(result));
return result;
}
interface VoteOptionsManagerProps { interface VoteOptionsManagerProps {
voteOptions: { id: string; label: string }[]; voteOptions: { id: string; label: string }[];
@ -13,6 +29,18 @@ export default function VoteOptionsManager({ voteOptions, onVoteOptionsChange }:
const [newOptionLabel, setNewOptionLabel] = useState(''); const [newOptionLabel, setNewOptionLabel] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [editingOptionId, setEditingOptionId] = useState<string | null>(null);
const [editedLabel, setEditedLabel] = useState('');
const editInputRef = useRef<HTMLInputElement>(null);
const labelInputRef = useRef<HTMLInputElement>(null);
// Generate a random ID when the component mounts or when the form is reset
useEffect(() => {
if (!newOptionId && voteOptions) {
const existingIds = voteOptions.map(option => option.id);
setNewOptionId(generateRandomId(existingIds));
}
}, [newOptionId, voteOptions]);
// Add a new vote option // Add a new vote option
const handleAddVoteOption = async () => { const handleAddVoteOption = async () => {
@ -53,9 +81,147 @@ export default function VoteOptionsManager({ voteOptions, onVoteOptionsChange }:
// Update parent component state // Update parent component state
onVoteOptionsChange(updatedOptions); onVoteOptionsChange(updatedOptions);
// Reset form // Reset form and generate a new random ID
setNewOptionId('');
setNewOptionLabel(''); setNewOptionLabel('');
const existingIds = updatedOptions.map(option => option.id);
setNewOptionId(generateRandomId(existingIds));
} catch (err) {
setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten');
} finally {
setIsLoading(false);
}
};
// Start editing a vote option
const handleEditVoteOption = (option: { id: string; label: string }) => {
setEditingOptionId(option.id);
setEditedLabel(option.label);
// Focus the input field after it's rendered
setTimeout(() => {
if (editInputRef.current) {
editInputRef.current.focus();
}
}, 0);
};
// Save the edited vote option
const handleSaveVoteOption = async () => {
if (!editingOptionId || !editedLabel.trim()) {
setError('Das Label darf nicht leer sein');
return;
}
setIsLoading(true);
setError(null);
try {
// Update in local state
const updatedOptions = voteOptions.map(option =>
option.id === editingOptionId
? { ...option, label: editedLabel.trim() }
: option
);
// Save to server
const response = await fetch('/api/editable-text', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: 'vote-options',
content: updatedOptions
}),
});
if (!response.ok) {
throw new Error('Fehler beim Speichern der Abstimmungsoptionen');
}
// Update parent component state
onVoteOptionsChange(updatedOptions);
// Exit edit mode
setEditingOptionId(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten');
} finally {
setIsLoading(false);
}
};
// Cancel editing
const handleCancelEdit = () => {
setEditingOptionId(null);
};
// Move option up in the list
const handleMoveOptionUp = async (index: number) => {
if (index <= 0) return; // Already at the top
setIsLoading(true);
setError(null);
try {
// Create a new array with the option moved up
const updatedOptions = [...voteOptions];
[updatedOptions[index - 1], updatedOptions[index]] = [updatedOptions[index], updatedOptions[index - 1]];
// Save to server
const response = await fetch('/api/editable-text', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: 'vote-options',
content: updatedOptions
}),
});
if (!response.ok) {
throw new Error('Fehler beim Speichern der Abstimmungsoptionen');
}
// Update parent component state
onVoteOptionsChange(updatedOptions);
} catch (err) {
setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten');
} finally {
setIsLoading(false);
}
};
// Move option down in the list
const handleMoveOptionDown = async (index: number) => {
if (index >= voteOptions.length - 1) return; // Already at the bottom
setIsLoading(true);
setError(null);
try {
// Create a new array with the option moved down
const updatedOptions = [...voteOptions];
[updatedOptions[index], updatedOptions[index + 1]] = [updatedOptions[index + 1], updatedOptions[index]];
// Save to server
const response = await fetch('/api/editable-text', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: 'vote-options',
content: updatedOptions
}),
});
if (!response.ok) {
throw new Error('Fehler beim Speichern der Abstimmungsoptionen');
}
// Update parent component state
onVoteOptionsChange(updatedOptions);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten'); setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten');
} finally { } finally {
@ -72,7 +238,8 @@ export default function VoteOptionsManager({ voteOptions, onVoteOptionsChange }:
} }
// Show confirmation dialog // Show confirmation dialog
if (!confirm(`Sind Sie sicher, dass Sie die Option "${optionId}" entfernen möchten? Bestehende Abstimmungen mit dieser Option werden nicht gelöscht, aber in den Statistiken möglicherweise nicht mehr korrekt angezeigt.`)) { const optionToRemove = voteOptions.find(option => option.id === optionId);
if (!confirm(`Sind Sie sicher, dass Sie die Option "${optionToRemove?.label}" entfernen möchten? Bestehende Abstimmungen mit dieser Option werden nicht gelöscht, aber in den Statistiken möglicherweise nicht mehr korrekt angezeigt.`)) {
return; return;
} }
@ -125,18 +292,82 @@ export default function VoteOptionsManager({ voteOptions, onVoteOptionsChange }:
<h3 className="font-medium text-[#0057a6] mb-3">Aktuelle Optionen</h3> <h3 className="font-medium text-[#0057a6] mb-3">Aktuelle Optionen</h3>
<div className="space-y-2 mb-4"> <div className="space-y-2 mb-4">
{voteOptions.map((option) => ( {voteOptions.map((option, index) => (
<div key={option.id} className="flex items-center justify-between p-2 bg-white"> <div key={option.id} className="flex items-center justify-between p-2 bg-white">
<div> <div className="flex items-center space-x-2">
<span className="font-medium">{option.id}:</span> {option.label} <div className="flex flex-col">
<button
onClick={() => handleMoveOptionUp(index)}
className="text-gray-600 hover:text-gray-800 disabled:opacity-30"
disabled={index === 0}
title="Nach oben verschieben"
>
</button>
<button
onClick={() => handleMoveOptionDown(index)}
className="text-gray-600 hover:text-gray-800 disabled:opacity-30"
disabled={index === voteOptions.length - 1}
title="Nach unten verschieben"
>
</button>
</div>
<div>
{editingOptionId === option.id ? (
<div className="flex items-center">
<span className="font-medium mr-2">{option.id}:</span>
<input
ref={editInputRef}
type="text"
value={editedLabel}
onChange={(e) => setEditedLabel(e.target.value)}
className="px-2 py-1 border border-gray-300 focus:outline-none focus:border-[#0057a6]"
onKeyDown={(e) => {
if (e.key === 'Enter') handleSaveVoteOption();
if (e.key === 'Escape') handleCancelEdit();
}}
/>
<button
onClick={handleSaveVoteOption}
className="ml-2 text-green-600 hover:text-green-800"
title="Speichern"
>
</button>
<button
onClick={handleCancelEdit}
className="ml-1 text-gray-600 hover:text-gray-800"
title="Abbrechen"
>
</button>
</div>
) : (
<div>
<span className="font-medium">{option.id}:</span> {option.label}
</div>
)}
</div>
</div>
<div className="flex space-x-2">
{editingOptionId !== option.id && (
<button
onClick={() => handleEditVoteOption(option)}
className="text-blue-600 hover:text-blue-800"
title="Option bearbeiten"
>
</button>
)}
<button
onClick={() => handleRemoveVoteOption(option.id)}
className="text-red-600 hover:text-red-800"
title="Option entfernen"
>
</button>
</div> </div>
<button
onClick={() => handleRemoveVoteOption(option.id)}
className="text-red-600 hover:text-red-800"
title="Option entfernen"
>
</button>
</div> </div>
))} ))}
</div> </div>
@ -167,10 +398,18 @@ export default function VoteOptionsManager({ voteOptions, onVoteOptionsChange }:
<input <input
type="text" type="text"
id="newOptionLabel" id="newOptionLabel"
ref={labelInputRef}
value={newOptionLabel} value={newOptionLabel}
onChange={(e) => setNewOptionLabel(e.target.value)} onChange={(e) => setNewOptionLabel(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 focus:outline-none focus:border-[#0057a6]" className="w-full px-3 py-2 border border-gray-300 focus:outline-none focus:border-[#0057a6]"
placeholder="Anzeigename der Option" placeholder="Anzeigename der Option"
onFocus={() => {
// Generate a random ID if the field is empty
if (!newOptionId) {
const existingIds = voteOptions.map(option => option.id);
setNewOptionId(generateRandomId(existingIds));
}
}}
/> />
<p className="text-xs text-gray-500 mt-1"> <p className="text-xs text-gray-500 mt-1">
Der Anzeigename wird den Abstimmenden angezeigt. Der Anzeigename wird den Abstimmenden angezeigt.