From 2561446907b36c23d81a26c31e2fa73b3c9cd53b Mon Sep 17 00:00:00 2001 From: Jean Jacques Avril Date: Sun, 2 Mar 2025 21:43:19 +0000 Subject: [PATCH] optimized iframe view --- public/iframe-example.html | 97 ++++++ src/app/{ => (full)}/abstimmung/page.tsx | 0 src/app/{ => (full)}/admin/page.tsx | 0 .../{ => (full)}/api/editable-text/route.ts | 0 .../api/generate-bulk-tokens/route.ts | 0 .../{ => (full)}/api/generate-token/route.ts | 0 .../{ => (full)}/api/member-login/route.ts | 0 src/app/{ => (full)}/api/members/route.ts | 0 .../{ => (full)}/api/public-stats/route.ts | 0 src/app/{ => (full)}/api/reset-votes/route.ts | 0 src/app/{ => (full)}/api/settings/route.ts | 0 src/app/{ => (full)}/api/stats/route.ts | 0 src/app/{ => (full)}/api/submit-vote/route.ts | 0 .../api/toggle-member-auth/route.ts | 0 .../{ => (full)}/api/upload-members/route.ts | 0 src/app/{ => (full)}/layout.tsx | 2 +- src/app/{ => (full)}/login/page.tsx | 0 src/app/{ => (full)}/page.tsx | 0 src/app/{ => (full)}/vote/page.tsx | 0 src/app/(iframe)/iframe/layout.tsx | 23 ++ src/app/(iframe)/iframe/page.tsx | 328 ++++++++++++++++++ 21 files changed, 449 insertions(+), 1 deletion(-) create mode 100644 public/iframe-example.html rename src/app/{ => (full)}/abstimmung/page.tsx (100%) rename src/app/{ => (full)}/admin/page.tsx (100%) rename src/app/{ => (full)}/api/editable-text/route.ts (100%) rename src/app/{ => (full)}/api/generate-bulk-tokens/route.ts (100%) rename src/app/{ => (full)}/api/generate-token/route.ts (100%) rename src/app/{ => (full)}/api/member-login/route.ts (100%) rename src/app/{ => (full)}/api/members/route.ts (100%) rename src/app/{ => (full)}/api/public-stats/route.ts (100%) rename src/app/{ => (full)}/api/reset-votes/route.ts (100%) rename src/app/{ => (full)}/api/settings/route.ts (100%) rename src/app/{ => (full)}/api/stats/route.ts (100%) rename src/app/{ => (full)}/api/submit-vote/route.ts (100%) rename src/app/{ => (full)}/api/toggle-member-auth/route.ts (100%) rename src/app/{ => (full)}/api/upload-members/route.ts (100%) rename src/app/{ => (full)}/layout.tsx (99%) rename src/app/{ => (full)}/login/page.tsx (100%) rename src/app/{ => (full)}/page.tsx (100%) rename src/app/{ => (full)}/vote/page.tsx (100%) create mode 100644 src/app/(iframe)/iframe/layout.tsx create mode 100644 src/app/(iframe)/iframe/page.tsx diff --git a/public/iframe-example.html b/public/iframe-example.html new file mode 100644 index 0000000..d72fe91 --- /dev/null +++ b/public/iframe-example.html @@ -0,0 +1,97 @@ + + + + + + + SSVC Rimsting Abstimmung - Iframe Beispiel + + + + +
+

SSVC Rimsting Abstimmung - Iframe Beispiel

+

Diese Seite zeigt, wie die Abstimmungskomponente als Iframe in eine externe Webseite eingebunden werden kann. +

+ +

Live Beispiel

+
+ +
+ +

HTML Code zum Einbinden

+
+ <iframe + src="https://ihre-domain.de/iframe" + width="100%" + height="600" + frameborder="0" + style="border:none;" + title="SSVC Rimsting Abstimmung"> + </iframe> +
+ +
+ Hinweis: Ersetzen Sie "https://ihre-domain.de/iframe" mit der tatsächlichen URL Ihrer + Abstimmungsseite. +

+ Sie können auch einen Token direkt in der URL übergeben, um den Login-Schritt zu überspringen: +
+ https://ihre-domain.de/iframe?token=IHR_TOKEN +
+
+ + + \ No newline at end of file diff --git a/src/app/abstimmung/page.tsx b/src/app/(full)/abstimmung/page.tsx similarity index 100% rename from src/app/abstimmung/page.tsx rename to src/app/(full)/abstimmung/page.tsx diff --git a/src/app/admin/page.tsx b/src/app/(full)/admin/page.tsx similarity index 100% rename from src/app/admin/page.tsx rename to src/app/(full)/admin/page.tsx diff --git a/src/app/api/editable-text/route.ts b/src/app/(full)/api/editable-text/route.ts similarity index 100% rename from src/app/api/editable-text/route.ts rename to src/app/(full)/api/editable-text/route.ts diff --git a/src/app/api/generate-bulk-tokens/route.ts b/src/app/(full)/api/generate-bulk-tokens/route.ts similarity index 100% rename from src/app/api/generate-bulk-tokens/route.ts rename to src/app/(full)/api/generate-bulk-tokens/route.ts diff --git a/src/app/api/generate-token/route.ts b/src/app/(full)/api/generate-token/route.ts similarity index 100% rename from src/app/api/generate-token/route.ts rename to src/app/(full)/api/generate-token/route.ts diff --git a/src/app/api/member-login/route.ts b/src/app/(full)/api/member-login/route.ts similarity index 100% rename from src/app/api/member-login/route.ts rename to src/app/(full)/api/member-login/route.ts diff --git a/src/app/api/members/route.ts b/src/app/(full)/api/members/route.ts similarity index 100% rename from src/app/api/members/route.ts rename to src/app/(full)/api/members/route.ts diff --git a/src/app/api/public-stats/route.ts b/src/app/(full)/api/public-stats/route.ts similarity index 100% rename from src/app/api/public-stats/route.ts rename to src/app/(full)/api/public-stats/route.ts diff --git a/src/app/api/reset-votes/route.ts b/src/app/(full)/api/reset-votes/route.ts similarity index 100% rename from src/app/api/reset-votes/route.ts rename to src/app/(full)/api/reset-votes/route.ts diff --git a/src/app/api/settings/route.ts b/src/app/(full)/api/settings/route.ts similarity index 100% rename from src/app/api/settings/route.ts rename to src/app/(full)/api/settings/route.ts diff --git a/src/app/api/stats/route.ts b/src/app/(full)/api/stats/route.ts similarity index 100% rename from src/app/api/stats/route.ts rename to src/app/(full)/api/stats/route.ts diff --git a/src/app/api/submit-vote/route.ts b/src/app/(full)/api/submit-vote/route.ts similarity index 100% rename from src/app/api/submit-vote/route.ts rename to src/app/(full)/api/submit-vote/route.ts diff --git a/src/app/api/toggle-member-auth/route.ts b/src/app/(full)/api/toggle-member-auth/route.ts similarity index 100% rename from src/app/api/toggle-member-auth/route.ts rename to src/app/(full)/api/toggle-member-auth/route.ts diff --git a/src/app/api/upload-members/route.ts b/src/app/(full)/api/upload-members/route.ts similarity index 100% rename from src/app/api/upload-members/route.ts rename to src/app/(full)/api/upload-members/route.ts diff --git a/src/app/layout.tsx b/src/app/(full)/layout.tsx similarity index 99% rename from src/app/layout.tsx rename to src/app/(full)/layout.tsx index 368c5f3..62611eb 100644 --- a/src/app/layout.tsx +++ b/src/app/(full)/layout.tsx @@ -1,7 +1,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import Link from "next/link"; -import "./globals.css"; +import "../globals.css"; const geistSans = Geist({ variable: "--font-geist-sans", diff --git a/src/app/login/page.tsx b/src/app/(full)/login/page.tsx similarity index 100% rename from src/app/login/page.tsx rename to src/app/(full)/login/page.tsx diff --git a/src/app/page.tsx b/src/app/(full)/page.tsx similarity index 100% rename from src/app/page.tsx rename to src/app/(full)/page.tsx diff --git a/src/app/vote/page.tsx b/src/app/(full)/vote/page.tsx similarity index 100% rename from src/app/vote/page.tsx rename to src/app/(full)/vote/page.tsx diff --git a/src/app/(iframe)/iframe/layout.tsx b/src/app/(iframe)/iframe/layout.tsx new file mode 100644 index 0000000..210b096 --- /dev/null +++ b/src/app/(iframe)/iframe/layout.tsx @@ -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 ( + + +
+ {children} +
+ + + ); +} diff --git a/src/app/(iframe)/iframe/page.tsx b/src/app/(iframe)/iframe/page.tsx new file mode 100644 index 0000000..b1f3f45 --- /dev/null +++ b/src/app/(iframe)/iframe/page.tsx @@ -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(null); + const [isLoginEnabled, setIsLoginEnabled] = useState(true); + const [isLoggedIn, setIsLoggedIn] = useState(false); + + // Vote state + const [voteOptions, setVoteOptions] = useState([]); + const [selectedOption, setSelectedOption] = useState(null); + const [comment, setComment] = useState(''); + const [isVoteLoading, setIsVoteLoading] = useState(false); + const [voteError, setVoteError] = useState(null); + const [isSubmitted, setIsSubmitted] = useState(false); + const [voteToken, setVoteToken] = useState(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 ( +
+
+
+
+
+ Die Mitgliederanmeldung ist derzeit deaktiviert. +
+
+
+
+
+ ); + } + + return ( +
+
+
+
+ +
+ +
+
+ + setMemberNumber(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 focus:outline-none focus:border-[#0057a6]" + required + /> +
+ +
+ + setPassword(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 focus:outline-none focus:border-[#0057a6]" + required + /> +
+ + {loginError && ( +
{loginError}
+ )} + + +
+
+
+
+ ); + } + + // Vote form + if (isSubmitted) { + return ( +
+
+
+
+ + + +
+

Vielen Dank!

+

+ Deine Stimme wurde erfolgreich übermittelt. +

+
+
+
+ ); + } + + return ( +
+
+
+
+
+
+ +
+ +
+ {voteOptions.map((option) => ( + + ))} +
+
+ +
+ +
+