fixed minor issues,
This commit is contained in:
		
							parent
							
								
									b53c186342
								
							
						
					
					
						commit
						0886757f89
					
				
							
								
								
									
										4
									
								
								data/editable_text.json
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										4
									
								
								data/editable_text.json
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							@ -27,8 +27,8 @@
 | 
			
		||||
        "label": "Ich enthalte mich"
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "id": "nichts",
 | 
			
		||||
        "label": "Wild"
 | 
			
		||||
        "id": "123",
 | 
			
		||||
        "label": "123"
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										0
									
								
								data/jwt_keys.json
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								data/jwt_keys.json
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										1366
									
								
								data/member_credentials.json
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										1366
									
								
								data/member_credentials.json
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										0
									
								
								data/responses.json
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								data/responses.json
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										0
									
								
								data/settings.json
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								data/settings.json
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										0
									
								
								data/used_tokens.json
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								data/used_tokens.json
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							@ -24,7 +24,7 @@ export default function PublicResultsPage() {
 | 
			
		||||
        if (response.ok) {
 | 
			
		||||
          const data = await response.json();
 | 
			
		||||
          if (data.texts && Array.isArray(data.texts)) {
 | 
			
		||||
            const voteOptionsEntry = data.texts.find((text: any) => text.id === 'vote-options');
 | 
			
		||||
            const voteOptionsEntry = data.texts.find((text: { id: string }) => text.id === 'vote-options');
 | 
			
		||||
            if (voteOptionsEntry && Array.isArray(voteOptionsEntry.content)) {
 | 
			
		||||
              setVoteOptions(voteOptionsEntry.content);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -77,7 +77,7 @@ export default function AdminPage() {
 | 
			
		||||
            setMemberAuthEnabled(data.settings.memberAuthEnabled);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
      } catch {
 | 
			
		||||
        // Silently fail, user will need to enter password
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
@ -190,7 +190,7 @@ export default function AdminPage() {
 | 
			
		||||
            setEditableTexts(data.texts);
 | 
			
		||||
 | 
			
		||||
            // Find and set vote options
 | 
			
		||||
            const voteOptionsEntry = data.texts.find((text: any) => text.id === 'vote-options');
 | 
			
		||||
            const voteOptionsEntry = data.texts.find((text: { id: string }) => text.id === 'vote-options');
 | 
			
		||||
            if (voteOptionsEntry && Array.isArray(voteOptionsEntry.content)) {
 | 
			
		||||
              setVoteOptions(voteOptionsEntry.content);
 | 
			
		||||
            }
 | 
			
		||||
@ -336,45 +336,6 @@ export default function AdminPage() {
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Update a vote option
 | 
			
		||||
  const handleUpdateVoteOption = async (optionId: string, newLabel: string) => {
 | 
			
		||||
    if (!newLabel.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 === optionId ? { ...option, label: newLabel } : option
 | 
			
		||||
      );
 | 
			
		||||
      setVoteOptions(updatedOptions);
 | 
			
		||||
 | 
			
		||||
      // 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');
 | 
			
		||||
      }
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten');
 | 
			
		||||
    } finally {
 | 
			
		||||
      setIsLoading(false);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const copyToClipboard = () => {
 | 
			
		||||
    if (generatedLink) {
 | 
			
		||||
      navigator.clipboard.writeText(generatedLink);
 | 
			
		||||
@ -469,7 +430,7 @@ export default function AdminPage() {
 | 
			
		||||
        try {
 | 
			
		||||
          const errorData = await response.json();
 | 
			
		||||
          throw new Error(errorData.error || 'Fehler beim Generieren der Tokens');
 | 
			
		||||
        } catch (jsonError) {
 | 
			
		||||
        } catch {
 | 
			
		||||
          // If not JSON, use status text
 | 
			
		||||
          throw new Error(`Fehler beim Generieren der Tokens: ${response.statusText}`);
 | 
			
		||||
        }
 | 
			
		||||
@ -668,7 +629,7 @@ export default function AdminPage() {
 | 
			
		||||
                  <div className="space-y-3">
 | 
			
		||||
                    <div>
 | 
			
		||||
                      <label htmlFor="newOptionId" className="block text-sm font-medium text-gray-700 mb-1">
 | 
			
		||||
                        Option ID (z.B. "yes", "no")
 | 
			
		||||
                        Option ID (z.B. "yes", "no")
 | 
			
		||||
                      </label>
 | 
			
		||||
                      <input
 | 
			
		||||
                        type="text"
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ import path from 'path';
 | 
			
		||||
 | 
			
		||||
// Path to the editable text file
 | 
			
		||||
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');
 | 
			
		||||
@ -61,7 +61,8 @@ function getEditableTexts() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Save editable texts
 | 
			
		||||
function saveEditableTexts(texts: any[]) {
 | 
			
		||||
 | 
			
		||||
function saveEditableTexts(texts: EditableText[]) {
 | 
			
		||||
  ensureDataDirectory();
 | 
			
		||||
  fs.writeFileSync(TEXT_FILE, JSON.stringify(texts, null, 2));
 | 
			
		||||
}
 | 
			
		||||
@ -107,7 +108,9 @@ export async function POST(request: NextRequest) {
 | 
			
		||||
    const texts = getEditableTexts();
 | 
			
		||||
 | 
			
		||||
    // Find and update the text with the given ID
 | 
			
		||||
    const textIndex = texts.findIndex((text: any) => text.id === id);
 | 
			
		||||
    const textIndex = texts.findIndex((text: EditableText
 | 
			
		||||
 | 
			
		||||
    ) => text.id === id);
 | 
			
		||||
 | 
			
		||||
    if (textIndex === -1) {
 | 
			
		||||
      // Text not found, add new
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,6 @@
 | 
			
		||||
import { checkAdminAuth, generateRandomToken } from '@/lib/auth';
 | 
			
		||||
import { NextRequest, NextResponse } from 'next/server';
 | 
			
		||||
 | 
			
		||||
// Get admin password from environment variable or use default for development
 | 
			
		||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'schafwaschener-segelverein-admin';
 | 
			
		||||
 | 
			
		||||
export async function POST(request: NextRequest) {
 | 
			
		||||
  try {
 | 
			
		||||
    const body = await request.json();
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ import { checkAdminAuth } from '@/lib/auth';
 | 
			
		||||
import { addMember, deleteMember, getMemberCredentials, updateMember } from '@/lib/server-auth';
 | 
			
		||||
import { NextRequest, NextResponse } from 'next/server';
 | 
			
		||||
 | 
			
		||||
export async function GET(request: NextRequest) {
 | 
			
		||||
export async function GET() {
 | 
			
		||||
  try {
 | 
			
		||||
    // Check for admin auth
 | 
			
		||||
    const isAuthenticated = await checkAdminAuth();
 | 
			
		||||
@ -18,7 +18,7 @@ export async function GET(request: NextRequest) {
 | 
			
		||||
    const members = getMemberCredentials();
 | 
			
		||||
 | 
			
		||||
    // Remove password from response
 | 
			
		||||
    const sanitizedMembers = members.map(({ password, ...rest }) => rest);
 | 
			
		||||
    const sanitizedMembers = members.map((v) => ({ ...v, password: undefined }));
 | 
			
		||||
 | 
			
		||||
    return NextResponse.json({ members: sanitizedMembers });
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,11 @@
 | 
			
		||||
import { NextRequest, NextResponse } from 'next/server';
 | 
			
		||||
import { getSettings } from '@/lib/server-auth';
 | 
			
		||||
import { NextResponse } from 'next/server';
 | 
			
		||||
 | 
			
		||||
export async function GET(request: NextRequest) {
 | 
			
		||||
export async function GET() {
 | 
			
		||||
  try {
 | 
			
		||||
    // Get settings
 | 
			
		||||
    const settings = getSettings();
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    return NextResponse.json({ settings });
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Error getting settings:', error);
 | 
			
		||||
 | 
			
		||||
@ -4,10 +4,8 @@ import { NextRequest, NextResponse } from 'next/server';
 | 
			
		||||
 | 
			
		||||
export async function POST(request: NextRequest) {
 | 
			
		||||
  try {
 | 
			
		||||
    const body = await request.json();
 | 
			
		||||
    const { password } = body;
 | 
			
		||||
    // Check for admin auth
 | 
			
		||||
    const isAuthenticated = await checkAdminAuth(password);
 | 
			
		||||
    const isAuthenticated = await checkAdminAuth();
 | 
			
		||||
    if (!isAuthenticated) {
 | 
			
		||||
      return NextResponse.json(
 | 
			
		||||
        { error: 'Unauthorized' },
 | 
			
		||||
 | 
			
		||||
@ -7,13 +7,13 @@ import { EditableText } from '@/components/EditableText';
 | 
			
		||||
 | 
			
		||||
export default function LoginPage() {
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  const [memberNumber, setMemberNumber] = useState('');
 | 
			
		||||
  const [password, setPassword] = useState('');
 | 
			
		||||
  const [isLoading, setIsLoading] = useState(false);
 | 
			
		||||
  const [error, setError] = useState<string | null>(null);
 | 
			
		||||
  const [isEnabled, setIsEnabled] = useState(true);
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // Check if member authentication is enabled
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const checkSettings = async () => {
 | 
			
		||||
@ -25,29 +25,29 @@ export default function LoginPage() {
 | 
			
		||||
          },
 | 
			
		||||
          body: JSON.stringify({ memberNumber: '', password: '' }),
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        if (response.status === 403) {
 | 
			
		||||
          setIsEnabled(false);
 | 
			
		||||
        }
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
      } catch {
 | 
			
		||||
        // Silently fail, assume enabled
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    checkSettings();
 | 
			
		||||
  }, []);
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  const handleSubmit = async (e: React.FormEvent) => {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    if (!memberNumber || !password) {
 | 
			
		||||
      setError('Bitte geben Sie Ihre Mitgliedsnummer und Ihr Passwort ein');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    setIsLoading(true);
 | 
			
		||||
    setError(null);
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await fetch('/api/member-login', {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
@ -56,13 +56,13 @@ export default function LoginPage() {
 | 
			
		||||
        },
 | 
			
		||||
        body: JSON.stringify({ memberNumber, password }),
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
 | 
			
		||||
      const data = await response.json();
 | 
			
		||||
      
 | 
			
		||||
 | 
			
		||||
      if (!response.ok) {
 | 
			
		||||
        throw new Error(data.error || 'Anmeldung fehlgeschlagen');
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
 | 
			
		||||
      // Redirect to voting page with token
 | 
			
		||||
      router.push(data.voteUrl);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
@ -71,19 +71,19 @@ export default function LoginPage() {
 | 
			
		||||
      setIsLoading(false);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  if (!isEnabled) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="container mx-auto px-4 py-8">
 | 
			
		||||
        <div className="mb-8">
 | 
			
		||||
          <h1 className="text-2xl font-bold text-[#0057a6] mb-4">MITGLIEDERANMELDUNG</h1>
 | 
			
		||||
          
 | 
			
		||||
 | 
			
		||||
          <div className="ssvc-main-content">
 | 
			
		||||
            <div className="text-center p-6">
 | 
			
		||||
              <div className="mb-4 text-red-600">
 | 
			
		||||
                Die Mitgliederanmeldung ist derzeit deaktiviert.
 | 
			
		||||
              </div>
 | 
			
		||||
              
 | 
			
		||||
 | 
			
		||||
              <Link href="/" className="ssvc-button inline-block">
 | 
			
		||||
                Zurück zur Startseite
 | 
			
		||||
              </Link>
 | 
			
		||||
@ -93,17 +93,17 @@ export default function LoginPage() {
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="container mx-auto px-4 py-8">
 | 
			
		||||
      <div className="mb-8">
 | 
			
		||||
        <h1 className="text-2xl font-bold text-[#0057a6] mb-4">MITGLIEDERANMELDUNG</h1>
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        <div className="ssvc-main-content">
 | 
			
		||||
          <div className="mb-6">
 | 
			
		||||
            <EditableText id="current-vote-text" />
 | 
			
		||||
          </div>
 | 
			
		||||
          
 | 
			
		||||
 | 
			
		||||
          <form onSubmit={handleSubmit} className="space-y-4">
 | 
			
		||||
            <div>
 | 
			
		||||
              <label htmlFor="memberNumber" className="block text-sm font-medium text-gray-700 mb-1">
 | 
			
		||||
@ -118,7 +118,7 @@ export default function LoginPage() {
 | 
			
		||||
                required
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            <div>
 | 
			
		||||
              <label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
 | 
			
		||||
                Passwort
 | 
			
		||||
@ -132,11 +132,11 @@ export default function LoginPage() {
 | 
			
		||||
                required
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            {error && (
 | 
			
		||||
              <div className="text-red-500 text-sm">{error}</div>
 | 
			
		||||
            )}
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            <button
 | 
			
		||||
              type="submit"
 | 
			
		||||
              disabled={isLoading}
 | 
			
		||||
@ -145,7 +145,7 @@ export default function LoginPage() {
 | 
			
		||||
              {isLoading ? 'Anmelden...' : 'Anmelden'}
 | 
			
		||||
            </button>
 | 
			
		||||
          </form>
 | 
			
		||||
          
 | 
			
		||||
 | 
			
		||||
          <div className="mt-6 text-center">
 | 
			
		||||
            <Link href="/" className="text-[#0057a6] hover:underline">
 | 
			
		||||
              Zurück zur Startseite
 | 
			
		||||
 | 
			
		||||
@ -1,23 +1,35 @@
 | 
			
		||||
'use client';
 | 
			
		||||
 | 
			
		||||
import { useState, useEffect } from 'react';
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default function VotePage() {
 | 
			
		||||
  return (
 | 
			
		||||
    <Suspense fallback={<LoadingComponent />}>
 | 
			
		||||
      <VotePageContent />
 | 
			
		||||
    </Suspense>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function LoadingComponent() {
 | 
			
		||||
  return <div>Lädt...</div>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function VotePageContent() {
 | 
			
		||||
  const searchParams = useSearchParams();
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
  const token = searchParams.get('token');
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  const [voteOptions, setVoteOptions] = useState<VoteOptionConfig[]>([]);
 | 
			
		||||
  const [selectedOption, setSelectedOption] = useState<VoteOption | null>(null);
 | 
			
		||||
  const [comment, setComment] = useState('');
 | 
			
		||||
  const [isLoading, setIsLoading] = useState(false);
 | 
			
		||||
  const [error, setError] = useState<string | null>(null);
 | 
			
		||||
  const [isSubmitted, setIsSubmitted] = useState(false);
 | 
			
		||||
  
 | 
			
		||||
  // Fetch vote options
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const fetchVoteOptions = async () => {
 | 
			
		||||
@ -26,7 +38,7 @@ export default function VotePage() {
 | 
			
		||||
        if (response.ok) {
 | 
			
		||||
          const data = await response.json();
 | 
			
		||||
          if (data.texts && Array.isArray(data.texts)) {
 | 
			
		||||
            const voteOptionsEntry = data.texts.find((text: any) => text.id === 'vote-options');
 | 
			
		||||
            const voteOptionsEntry = data.texts.find((text: { id: string }) => text.id === 'vote-options');
 | 
			
		||||
            if (voteOptionsEntry && Array.isArray(voteOptionsEntry.content)) {
 | 
			
		||||
              setVoteOptions(voteOptionsEntry.content);
 | 
			
		||||
            }
 | 
			
		||||
@ -42,28 +54,28 @@ export default function VotePage() {
 | 
			
		||||
        ]);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    fetchVoteOptions();
 | 
			
		||||
  }, []);
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // Redirect if no token is provided
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!token) {
 | 
			
		||||
      router.push('/');
 | 
			
		||||
    }
 | 
			
		||||
  }, [token, router]);
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  const handleSubmit = async (e: React.FormEvent) => {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    if (!selectedOption) {
 | 
			
		||||
      setError('Bitte wählen Sie eine Option');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    setIsLoading(true);
 | 
			
		||||
    setError(null);
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await fetch('/api/submit-vote', {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
@ -76,13 +88,13 @@ export default function VotePage() {
 | 
			
		||||
          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) {
 | 
			
		||||
      setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten');
 | 
			
		||||
@ -90,7 +102,7 @@ export default function VotePage() {
 | 
			
		||||
      setIsLoading(false);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  if (isSubmitted) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="container mx-auto px-4 py-8">
 | 
			
		||||
@ -106,8 +118,8 @@ export default function VotePage() {
 | 
			
		||||
              Ihre Stimme wurde erfolgreich übermittelt.
 | 
			
		||||
            </p>
 | 
			
		||||
          </div>
 | 
			
		||||
          
 | 
			
		||||
          <Link 
 | 
			
		||||
 | 
			
		||||
          <Link
 | 
			
		||||
            href="/"
 | 
			
		||||
            className="ssvc-button inline-block"
 | 
			
		||||
          >
 | 
			
		||||
@ -117,22 +129,22 @@ export default function VotePage() {
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="container mx-auto px-4 py-8">
 | 
			
		||||
      <div className="mb-8">
 | 
			
		||||
        <h1 className="text-2xl font-bold text-[#0057a6] mb-4">SATZUNGSÄNDERUNG - ABSTIMMUNG</h1>
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        <div className="ssvc-main-content">
 | 
			
		||||
          <form onSubmit={handleSubmit} className="space-y-6">
 | 
			
		||||
            <div>
 | 
			
		||||
              <div className="text-xl font-bold text-[#0057a6] mb-4">
 | 
			
		||||
                <EditableText id="vote-question" />
 | 
			
		||||
              </div>
 | 
			
		||||
              
 | 
			
		||||
 | 
			
		||||
              <div className="space-y-3">
 | 
			
		||||
                {voteOptions.map((option) => (
 | 
			
		||||
                  <label 
 | 
			
		||||
                  <label
 | 
			
		||||
                    key={option.id}
 | 
			
		||||
                    className="flex items-center p-3 border border-gray-200 cursor-pointer hover:bg-[#e6f0fa] transition-colors"
 | 
			
		||||
                  >
 | 
			
		||||
@ -149,7 +161,7 @@ export default function VotePage() {
 | 
			
		||||
                ))}
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            <div>
 | 
			
		||||
              <label htmlFor="comment" className="block text-sm font-medium text-gray-700 mb-1">
 | 
			
		||||
                Kommentare oder Vorschläge (Optional)
 | 
			
		||||
@ -169,11 +181,11 @@ export default function VotePage() {
 | 
			
		||||
                Bitte geben Sie keine persönlichen Daten in Ihren Kommentaren an.
 | 
			
		||||
              </p>
 | 
			
		||||
            </div>
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            {error && (
 | 
			
		||||
              <div className="text-red-500 text-sm">{error}</div>
 | 
			
		||||
            )}
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            <button
 | 
			
		||||
              type="submit"
 | 
			
		||||
              disabled={isLoading}
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,9 @@
 | 
			
		||||
'use client';
 | 
			
		||||
 | 
			
		||||
import { useState, useEffect, forwardRef } from 'react';
 | 
			
		||||
import dynamic from 'next/dynamic';
 | 
			
		||||
import { useEffect, useState } from 'react';
 | 
			
		||||
// Import ReactQuill dynamically to avoid SSR issues
 | 
			
		||||
const ReactQuill = dynamic(() => import('react-quill-new'), { 
 | 
			
		||||
const ReactQuill = dynamic(() => import('react-quill-new'), {
 | 
			
		||||
  ssr: false,
 | 
			
		||||
  loading: () => <div className="border border-gray-300 h-64 flex items-center justify-center">Loading editor...</div>
 | 
			
		||||
});
 | 
			
		||||
@ -22,7 +22,7 @@ interface QuillEditorProps {
 | 
			
		||||
const QuillEditor = ({ value, onChange, placeholder }: QuillEditorProps) => {
 | 
			
		||||
  // Use state to track if component is mounted (client-side)
 | 
			
		||||
  const [mounted, setMounted] = useState(false);
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // Only render the editor on the client
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setMounted(true);
 | 
			
		||||
@ -33,7 +33,7 @@ const QuillEditor = ({ value, onChange, placeholder }: QuillEditorProps) => {
 | 
			
		||||
    toolbar: [
 | 
			
		||||
      [{ 'header': [1, 2, 3, false] }],
 | 
			
		||||
      ['bold', 'italic', 'underline', 'strike'],
 | 
			
		||||
      [{ 'list': 'ordered'}, { 'list': 'bullet' }],
 | 
			
		||||
      [{ 'list': 'ordered' }, { 'list': 'bullet' }],
 | 
			
		||||
      ['link'],
 | 
			
		||||
      ['clean']
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
@ -18,8 +18,8 @@ export interface MemberCredential {
 | 
			
		||||
export async function generateRandomToken(memberNumber?: string): Promise<string> {
 | 
			
		||||
  // This function is used in API routes
 | 
			
		||||
  const tokenId = uuidv4();
 | 
			
		||||
 | 
			
		||||
  const payload: any = { tokenId };
 | 
			
		||||
  type Payload = { tokenId: string; memberNumber?: string };
 | 
			
		||||
  const payload: Payload = { tokenId };
 | 
			
		||||
 | 
			
		||||
  // If memberNumber is provided, include it in the token
 | 
			
		||||
  if (memberNumber) {
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,7 @@
 | 
			
		||||
import fs from 'fs';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
import { SignJWT, jwtVerify } from 'jose';
 | 
			
		||||
import { v4 as uuidv4 } from 'uuid';
 | 
			
		||||
import crypto from 'crypto';
 | 
			
		||||
import fs from 'fs';
 | 
			
		||||
import { jwtVerify } from 'jose';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
import { MemberCredential, comparePassword, hashPassword } from './auth';
 | 
			
		||||
 | 
			
		||||
// Keys for JWT signing and verification
 | 
			
		||||
@ -32,11 +31,11 @@ function ensureDataDirectory() {
 | 
			
		||||
// Get all used tokens from the blacklist file
 | 
			
		||||
export function getUsedTokens(): string[] {
 | 
			
		||||
  ensureDataDirectory();
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  if (!fs.existsSync(BLACKLIST_FILE)) {
 | 
			
		||||
    return [];
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  const data = fs.readFileSync(BLACKLIST_FILE, 'utf-8');
 | 
			
		||||
  return JSON.parse(data);
 | 
			
		||||
}
 | 
			
		||||
@ -44,10 +43,10 @@ export function getUsedTokens(): string[] {
 | 
			
		||||
// Add a token to the blacklist
 | 
			
		||||
export function addToBlacklist(tokenId: string): void {
 | 
			
		||||
  const usedTokens = getUsedTokens();
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  if (!usedTokens.includes(tokenId)) {
 | 
			
		||||
    usedTokens.push(tokenId);
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    ensureDataDirectory();
 | 
			
		||||
    fs.writeFileSync(BLACKLIST_FILE, JSON.stringify(usedTokens, null, 2));
 | 
			
		||||
  }
 | 
			
		||||
@ -56,13 +55,13 @@ export function addToBlacklist(tokenId: string): void {
 | 
			
		||||
// Get settings
 | 
			
		||||
export function getSettings(): { memberAuthEnabled: boolean } {
 | 
			
		||||
  ensureDataDirectory();
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  if (!fs.existsSync(SETTINGS_FILE)) {
 | 
			
		||||
    const defaultSettings = { memberAuthEnabled: false };
 | 
			
		||||
    fs.writeFileSync(SETTINGS_FILE, JSON.stringify(defaultSettings, null, 2));
 | 
			
		||||
    return defaultSettings;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  const data = fs.readFileSync(SETTINGS_FILE, 'utf-8');
 | 
			
		||||
  return JSON.parse(data);
 | 
			
		||||
}
 | 
			
		||||
@ -76,12 +75,12 @@ export function updateSettings(settings: { memberAuthEnabled: boolean }): void {
 | 
			
		||||
// Get all member credentials
 | 
			
		||||
export function getMemberCredentials(): MemberCredential[] {
 | 
			
		||||
  ensureDataDirectory();
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  if (!fs.existsSync(MEMBER_CREDENTIALS_FILE)) {
 | 
			
		||||
    fs.writeFileSync(MEMBER_CREDENTIALS_FILE, JSON.stringify([], null, 2));
 | 
			
		||||
    return [];
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  const data = fs.readFileSync(MEMBER_CREDENTIALS_FILE, 'utf-8');
 | 
			
		||||
  return JSON.parse(data);
 | 
			
		||||
}
 | 
			
		||||
@ -95,22 +94,22 @@ export function saveMemberCredentials(credentials: MemberCredential[]): void {
 | 
			
		||||
// Add a new member
 | 
			
		||||
export function addMember(memberNumber: string, password: string): boolean {
 | 
			
		||||
  const members = getMemberCredentials();
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // Check if member already exists
 | 
			
		||||
  if (members.some(m => m.memberNumber === memberNumber)) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // Hash the password
 | 
			
		||||
  const hashedPassword = hashPassword(password);
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // Add the new member
 | 
			
		||||
  members.push({
 | 
			
		||||
    memberNumber,
 | 
			
		||||
    password: hashedPassword,
 | 
			
		||||
    hasVoted: false
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  saveMemberCredentials(members);
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
@ -119,19 +118,19 @@ export function addMember(memberNumber: string, password: string): boolean {
 | 
			
		||||
export function updateMember(memberNumber: string, data: { password?: string, hasVoted?: boolean }): boolean {
 | 
			
		||||
  const members = getMemberCredentials();
 | 
			
		||||
  const memberIndex = members.findIndex(m => m.memberNumber === memberNumber);
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  if (memberIndex === -1) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  if (data.password) {
 | 
			
		||||
    members[memberIndex].password = hashPassword(data.password);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  if (data.hasVoted !== undefined) {
 | 
			
		||||
    members[memberIndex].hasVoted = data.hasVoted;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  saveMemberCredentials(members);
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
@ -140,13 +139,13 @@ export function updateMember(memberNumber: string, data: { password?: string, ha
 | 
			
		||||
export function deleteMember(memberNumber: string): boolean {
 | 
			
		||||
  const members = getMemberCredentials();
 | 
			
		||||
  const initialLength = members.length;
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  const filteredMembers = members.filter(m => m.memberNumber !== memberNumber);
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  if (filteredMembers.length === initialLength) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  saveMemberCredentials(filteredMembers);
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
@ -155,50 +154,50 @@ export function deleteMember(memberNumber: string): boolean {
 | 
			
		||||
export function importMembersFromCSV(csvContent: string): { added: number, skipped: number } {
 | 
			
		||||
  const members = getMemberCredentials();
 | 
			
		||||
  const existingMemberNumbers = new Set(members.map(m => m.memberNumber));
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  let added = 0;
 | 
			
		||||
  let skipped = 0;
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // Parse CSV content
 | 
			
		||||
  const lines = csvContent.split('\n');
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  for (let i = 0; i < lines.length; i++) {
 | 
			
		||||
    const line = lines[i].trim();
 | 
			
		||||
    if (!line) continue;
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    // Skip header row if present
 | 
			
		||||
    if (i === 0 && (line.toLowerCase().includes('member') || line.toLowerCase().includes('password'))) {
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    // Split by comma, semicolon, or tab
 | 
			
		||||
    const parts = line.split(/[,;\t]/);
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    if (parts.length >= 2) {
 | 
			
		||||
      const memberNumber = parts[0].trim();
 | 
			
		||||
      const password = parts[1].trim();
 | 
			
		||||
      
 | 
			
		||||
 | 
			
		||||
      // Skip if member number already exists
 | 
			
		||||
      if (existingMemberNumbers.has(memberNumber)) {
 | 
			
		||||
        skipped++;
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
 | 
			
		||||
      // Hash the password
 | 
			
		||||
      const hashedPassword = hashPassword(password);
 | 
			
		||||
      
 | 
			
		||||
 | 
			
		||||
      // Add the new member
 | 
			
		||||
      members.push({
 | 
			
		||||
        memberNumber,
 | 
			
		||||
        password: hashedPassword,
 | 
			
		||||
        hasVoted: false
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
 | 
			
		||||
      existingMemberNumbers.add(memberNumber);
 | 
			
		||||
      added++;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  saveMemberCredentials(members);
 | 
			
		||||
  return { added, skipped };
 | 
			
		||||
}
 | 
			
		||||
@ -207,19 +206,19 @@ export function importMembersFromCSV(csvContent: string): { added: number, skipp
 | 
			
		||||
export function verifyMemberCredentials(memberNumber: string, password: string): { valid: boolean, hasVoted: boolean } {
 | 
			
		||||
  const members = getMemberCredentials();
 | 
			
		||||
  const member = members.find(m => m.memberNumber === memberNumber);
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  if (!member) {
 | 
			
		||||
    return { valid: false, hasVoted: false };
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  const passwordValid = comparePassword(password, member.password);
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  if (passwordValid) {
 | 
			
		||||
    // Update last login time
 | 
			
		||||
    member.lastLogin = new Date().toISOString();
 | 
			
		||||
    saveMemberCredentials(members);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  return { valid: passwordValid, hasVoted: member.hasVoted };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -231,11 +230,11 @@ export function markMemberAsVoted(memberNumber: string): boolean {
 | 
			
		||||
// Reset all member voting status
 | 
			
		||||
export function resetMemberVotingStatus(): void {
 | 
			
		||||
  const members = getMemberCredentials();
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  for (const member of members) {
 | 
			
		||||
    member.hasVoted = false;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  saveMemberCredentials(members);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -265,7 +264,7 @@ export async function ensureKeys() {
 | 
			
		||||
          privateKey = encoder.encode(secretKey);
 | 
			
		||||
          publicKey = encoder.encode(secretKey);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        // Save keys to file
 | 
			
		||||
        ensureDataDirectory();
 | 
			
		||||
        fs.writeFileSync(KEYS_FILE, JSON.stringify({
 | 
			
		||||
@ -291,25 +290,25 @@ export async function verifyToken(token: string): Promise<{ valid: boolean, memb
 | 
			
		||||
    const secretKey = process.env.JWT_SECRET_KEY || 'schafwaschener-segelverein-secret-key-for-jwt-signing';
 | 
			
		||||
    const encoder = new TextEncoder();
 | 
			
		||||
    const key = encoder.encode(secretKey);
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    const { payload } = await jwtVerify(token, key);
 | 
			
		||||
    const tokenId = payload.tokenId as string;
 | 
			
		||||
    const memberNumber = payload.memberNumber as string | undefined;
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    // Check if token has been used before
 | 
			
		||||
    const usedTokens = getUsedTokens();
 | 
			
		||||
    if (usedTokens.includes(tokenId)) {
 | 
			
		||||
      return { valid: false };
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    // Mark token as used by adding to blacklist
 | 
			
		||||
    addToBlacklist(tokenId);
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    // If token contains a member number, mark that member as voted
 | 
			
		||||
    if (memberNumber) {
 | 
			
		||||
      markMemberAsVoted(memberNumber);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    return { valid: true, memberNumber };
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Token verification failed:', error);
 | 
			
		||||
 | 
			
		||||
@ -33,7 +33,7 @@ function ensureDataDirectory() {
 | 
			
		||||
// Get vote options from editable_text.json
 | 
			
		||||
export function getVoteOptions(): VoteOptionConfig[] {
 | 
			
		||||
  ensureDataDirectory();
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  if (!fs.existsSync(TEXT_FILE)) {
 | 
			
		||||
    // Default options if file doesn't exist
 | 
			
		||||
    return [
 | 
			
		||||
@ -42,17 +42,17 @@ export function getVoteOptions(): VoteOptionConfig[] {
 | 
			
		||||
      { id: 'abstain', label: 'Ich enthalte mich' }
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const data = fs.readFileSync(TEXT_FILE, 'utf-8');
 | 
			
		||||
    const texts = JSON.parse(data);
 | 
			
		||||
    
 | 
			
		||||
    const voteOptionsEntry = texts.find((text: any) => text.id === 'vote-options');
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    const voteOptionsEntry = texts.find((text: { id: string }) => text.id === 'vote-options');
 | 
			
		||||
 | 
			
		||||
    if (voteOptionsEntry && Array.isArray(voteOptionsEntry.content)) {
 | 
			
		||||
      return voteOptionsEntry.content;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    // Return default options if not found or invalid
 | 
			
		||||
    return [
 | 
			
		||||
      { id: 'yes', label: 'Ja, ich stimme zu' },
 | 
			
		||||
@ -73,11 +73,11 @@ export function getVoteOptions(): VoteOptionConfig[] {
 | 
			
		||||
// Get all survey responses
 | 
			
		||||
export function getAllResponses(): SurveyResponse[] {
 | 
			
		||||
  ensureDataDirectory();
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  if (!fs.existsSync(DATA_FILE)) {
 | 
			
		||||
    return [];
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  const data = fs.readFileSync(DATA_FILE, 'utf-8');
 | 
			
		||||
  return JSON.parse(data);
 | 
			
		||||
}
 | 
			
		||||
@ -85,7 +85,7 @@ export function getAllResponses(): SurveyResponse[] {
 | 
			
		||||
// Save a new survey response
 | 
			
		||||
export function saveResponse(vote: VoteOption, comment?: string): SurveyResponse {
 | 
			
		||||
  const responses = getAllResponses();
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // Create a new response with a random ID (not associated with the voter)
 | 
			
		||||
  const newResponse: SurveyResponse = {
 | 
			
		||||
    id: uuidv4(), // Random ID only for internal tracking
 | 
			
		||||
@ -93,12 +93,12 @@ export function saveResponse(vote: VoteOption, comment?: string): SurveyResponse
 | 
			
		||||
    comment,
 | 
			
		||||
    timestamp: new Date().toISOString(),
 | 
			
		||||
  };
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  responses.push(newResponse);
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  ensureDataDirectory();
 | 
			
		||||
  fs.writeFileSync(DATA_FILE, JSON.stringify(responses, null, 2));
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  return newResponse;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -106,23 +106,23 @@ export function saveResponse(vote: VoteOption, comment?: string): SurveyResponse
 | 
			
		||||
export function getSurveyStats() {
 | 
			
		||||
  const responses = getAllResponses();
 | 
			
		||||
  const voteOptions = getVoteOptions();
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // Initialize stats object with total count
 | 
			
		||||
  const stats: Record<string, number> = {
 | 
			
		||||
    total: responses.length
 | 
			
		||||
  };
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // Initialize count for each vote option
 | 
			
		||||
  voteOptions.forEach(option => {
 | 
			
		||||
    stats[option.id] = 0;
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // Count votes for each option
 | 
			
		||||
  responses.forEach(response => {
 | 
			
		||||
    if (stats[response.vote] !== undefined) {
 | 
			
		||||
      stats[response.vote]++;
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  return stats;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,8 +0,0 @@
 | 
			
		||||
import { NextResponse } from 'next/server';
 | 
			
		||||
import type { NextRequest } from 'next/server';
 | 
			
		||||
 | 
			
		||||
export function middleware(request: NextRequest) {
 | 
			
		||||
  // This middleware doesn't do anything special yet
 | 
			
		||||
  // It's just a placeholder for future authentication middleware
 | 
			
		||||
  return NextResponse.next();
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user