feat: Persist guest session captcha verification and display guest's own name in navbar header and shift matrix table
This commit is contained in:
+56
-56
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -12,8 +12,8 @@
|
|||||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=JetBrains+Mono:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=JetBrains+Mono:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
<script src="https://cdn.jsdelivr.net/gh/arooshbd2024/aCAPTCHA@main/api/captcha-v1.1.js" defer></script>
|
<script src="https://cdn.jsdelivr.net/gh/arooshbd2024/aCAPTCHA@main/api/captcha-v1.1.js" defer></script>
|
||||||
<title>Schichtplaner — Veranstaltungsschichtpläne</title>
|
<title>Schichtplaner — Veranstaltungsschichtpläne</title>
|
||||||
<script type="module" crossorigin src="/assets/index-DXooeo1R.js"></script>
|
<script type="module" crossorigin src="/assets/index-BocP3KKS.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-jfrwfPo3.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-CR8RYpZS.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-paper text-main font-sans antialiased min-h-screen">
|
<body class="bg-paper text-main font-sans antialiased min-h-screen">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -3,8 +3,15 @@ import { X, ShieldCheck, UserCheck, AlertCircle, CheckCircle2 } from 'lucide-rea
|
|||||||
|
|
||||||
export default function GuestSignupModal({ shift, onClose, onSubmit }) {
|
export default function GuestSignupModal({ shift, onClose, onSubmit }) {
|
||||||
const [guestName, setGuestName] = useState('');
|
const [guestName, setGuestName] = useState('');
|
||||||
const [captchaToken, setCaptchaToken] = useState('');
|
const [sessionVerified, setSessionVerified] = useState(() => {
|
||||||
const [isCaptchaSolved, setIsCaptchaSolved] = useState(false);
|
return sessionStorage.getItem('guest_captcha_verified') === 'true';
|
||||||
|
});
|
||||||
|
const [captchaToken, setCaptchaToken] = useState(() => {
|
||||||
|
return sessionStorage.getItem('guest_captcha_verified') === 'true' ? 'acaptcha-session-verified' : '';
|
||||||
|
});
|
||||||
|
const [isCaptchaSolved, setIsCaptchaSolved] = useState(() => {
|
||||||
|
return sessionStorage.getItem('guest_captcha_verified') === 'true';
|
||||||
|
});
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const captchaRef = useRef(null);
|
const captchaRef = useRef(null);
|
||||||
@@ -18,12 +25,16 @@ export default function GuestSignupModal({ shift, onClose, onSubmit }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (sessionVerified) return;
|
||||||
|
|
||||||
const el = captchaRef.current;
|
const el = captchaRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
const handleVerified = (e) => {
|
const handleVerified = (e) => {
|
||||||
const isVerified = e?.detail?.verified || (el.isValid && el.isValid());
|
const isVerified = e?.detail?.verified || (el.isValid && el.isValid());
|
||||||
if (isVerified) {
|
if (isVerified) {
|
||||||
|
sessionStorage.setItem('guest_captcha_verified', 'true');
|
||||||
|
setSessionVerified(true);
|
||||||
setIsCaptchaSolved(true);
|
setIsCaptchaSolved(true);
|
||||||
const token = 'acaptcha-verified-' + Math.random().toString(36).substring(2, 10);
|
const token = 'acaptcha-verified-' + Math.random().toString(36).substring(2, 10);
|
||||||
setCaptchaToken(token);
|
setCaptchaToken(token);
|
||||||
@@ -49,7 +60,7 @@ export default function GuestSignupModal({ shift, onClose, onSubmit }) {
|
|||||||
el.removeEventListener('statuschange', handleVerified);
|
el.removeEventListener('statuschange', handleVerified);
|
||||||
el.removeEventListener('reset', handleReset);
|
el.removeEventListener('reset', handleReset);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [sessionVerified]);
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -60,18 +71,18 @@ export default function GuestSignupModal({ shift, onClose, onSubmit }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const el = captchaRef.current;
|
const isVerified = sessionVerified || isCaptchaSolved;
|
||||||
const isVerified = isCaptchaSolved || (el && el.isValid && el.isValid());
|
|
||||||
|
|
||||||
if (!isVerified) {
|
if (!isVerified) {
|
||||||
setError('Bitte löse zuerst das Captcha.');
|
setError('Bitte löse zuerst das Captcha.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = captchaToken || ('acaptcha-verified-' + Math.random().toString(36).substring(2, 10));
|
const token = captchaToken || 'acaptcha-session-verified';
|
||||||
|
|
||||||
// Save in localStorage for guest convenience
|
// Save in localStorage for guest convenience
|
||||||
localStorage.setItem('guest_display_name', guestName.trim());
|
localStorage.setItem('guest_display_name', guestName.trim());
|
||||||
|
window.dispatchEvent(new Event('guest_name_updated'));
|
||||||
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
@@ -125,11 +136,19 @@ export default function GuestSignupModal({ shift, onClose, onSubmit }) {
|
|||||||
className="w-full px-3.5 py-2.5 rounded-xl bg-slate-900 border border-slate-800 text-white text-sm focus:outline-none focus:border-blue-500 transition"
|
className="w-full px-3.5 py-2.5 rounded-xl bg-slate-900 border border-slate-800 text-white text-sm focus:outline-none focus:border-blue-500 transition"
|
||||||
/>
|
/>
|
||||||
<p className="text-[11px] text-slate-500 mt-1">
|
<p className="text-[11px] text-slate-500 mt-1">
|
||||||
Dein Name wird in deiner Sitzung gespeichert. Falls du später ein Konto mit diesem Namen erstellst, werden deine Gast-Schichten automatisch übertragen!
|
Dein Name bleibt in deiner aktuellen Sitzung gespeichert. Falls du später ein Konto mit diesem Namen erstellst, werden deine Gast-Schichten automatisch übernommen!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Official aCAPTCHA Web Component */}
|
{/* Security Check Section */}
|
||||||
|
{sessionVerified ? (
|
||||||
|
<div className="p-3.5 rounded-xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-xs flex items-center justify-between font-mono">
|
||||||
|
<span className="flex items-center gap-2 font-semibold">
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0" /> Security Check verifiziert
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-emerald-400/80">[ In dieser Sitzung aktiv ]</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="p-4 rounded-xl bg-slate-900/90 border border-slate-800 space-y-3">
|
<div className="p-4 rounded-xl bg-slate-900/90 border border-slate-800 space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs font-semibold text-slate-300 flex items-center gap-1.5 font-mono">
|
<span className="text-xs font-semibold text-slate-300 flex items-center gap-1.5 font-mono">
|
||||||
@@ -162,6 +181,7 @@ export default function GuestSignupModal({ shift, onClose, onSubmit }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="pt-2 flex justify-end gap-2">
|
<div className="pt-2 flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -20,6 +20,19 @@ export default function Navbar({
|
|||||||
onInstallPwa
|
onInstallPwa
|
||||||
}) {
|
}) {
|
||||||
const primaryColor = branding?.primary_color || 'var(--brand-primary)';
|
const primaryColor = branding?.primary_color || 'var(--brand-primary)';
|
||||||
|
const [savedGuestName, setSavedGuestName] = React.useState(() => localStorage.getItem('guest_display_name') || '');
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const updateGuestName = () => {
|
||||||
|
setSavedGuestName(localStorage.getItem('guest_display_name') || '');
|
||||||
|
};
|
||||||
|
window.addEventListener('guest_name_updated', updateGuestName);
|
||||||
|
window.addEventListener('storage', updateGuestName);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('guest_name_updated', updateGuestName);
|
||||||
|
window.removeEventListener('storage', updateGuestName);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const cycleTheme = () => {
|
const cycleTheme = () => {
|
||||||
if (themeMode === 'dark') onChangeThemeMode('light');
|
if (themeMode === 'dark') onChangeThemeMode('light');
|
||||||
@@ -50,6 +63,10 @@ export default function Navbar({
|
|||||||
<div className="text-[10px] font-mono text-muted mt-1 uppercase tracking-wider flex items-center gap-1.5">
|
<div className="text-[10px] font-mono text-muted mt-1 uppercase tracking-wider flex items-center gap-1.5">
|
||||||
{user ? (
|
{user ? (
|
||||||
<span className="text-emerald-500 font-semibold">[ {user.display_name || user.username} ]</span>
|
<span className="text-emerald-500 font-semibold">[ {user.display_name || user.username} ]</span>
|
||||||
|
) : savedGuestName ? (
|
||||||
|
<span className="text-amber-400 font-semibold flex items-center gap-1">
|
||||||
|
[ 👤 GAST: {savedGuestName} ]
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-amber-500 font-semibold">[ GAST-MODUS ]</span>
|
<span className="text-amber-500 font-semibold">[ GAST-MODUS ]</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -15,6 +15,17 @@ export default function ShiftMatrixTable({
|
|||||||
onEditEvent,
|
onEditEvent,
|
||||||
onDeleteEvent
|
onDeleteEvent
|
||||||
}) {
|
}) {
|
||||||
|
const [guestDisplayName, setGuestDisplayName] = React.useState(() => localStorage.getItem('guest_display_name') || '');
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handleUpdate = () => setGuestDisplayName(localStorage.getItem('guest_display_name') || '');
|
||||||
|
window.addEventListener('guest_name_updated', handleUpdate);
|
||||||
|
window.addEventListener('storage', handleUpdate);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('guest_name_updated', handleUpdate);
|
||||||
|
window.removeEventListener('storage', handleUpdate);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
if (!event || !event.task_areas || event.task_areas.length === 0) {
|
if (!event || !event.task_areas || event.task_areas.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="hallmark-panel rounded-sm p-12 text-center border border-grid font-mono">
|
<div className="hallmark-panel rounded-sm p-12 text-center border border-grid font-mono">
|
||||||
@@ -199,12 +210,15 @@ export default function ShiftMatrixTable({
|
|||||||
|
|
||||||
{/* Attendees List */}
|
{/* Attendees List */}
|
||||||
<div className="mt-2 space-y-1 font-sans">
|
<div className="mt-2 space-y-1 font-sans">
|
||||||
{shift.signups.map((su) => (
|
{shift.signups.map((su) => {
|
||||||
<div key={su.id} className="text-[11px] text-muted flex items-center justify-between gap-2 p-1.5 rounded-sm bg-subtle border border-grid">
|
const isOwnGuest = isGuest && su.is_guest && guestDisplayName && su.display_name?.trim().toLowerCase() === guestDisplayName.trim().toLowerCase();
|
||||||
|
return (
|
||||||
|
<div key={su.id} className={`text-[11px] text-muted flex items-center justify-between gap-2 p-1.5 rounded-sm border ${isOwnGuest ? 'bg-amber-500/10 border-amber-500/30' : 'bg-subtle border-grid'}`}>
|
||||||
<div className="flex items-center gap-1.5 truncate">
|
<div className="flex items-center gap-1.5 truncate">
|
||||||
<UserCheck className="w-3 h-3 text-muted shrink-0" />
|
<UserCheck className={`w-3 h-3 shrink-0 ${isOwnGuest ? 'text-amber-400' : 'text-muted'}`} />
|
||||||
<span className="truncate">{su.display_name}</span>
|
<span className={`truncate ${isOwnGuest ? 'font-bold text-amber-300' : ''}`}>{su.display_name}</span>
|
||||||
{su.is_guest && <span className="text-[9px] font-mono text-amber-500 px-1 rounded-sm bg-amber-500/10 border border-amber-500/20 font-bold">GAST</span>}
|
{su.is_guest && <span className="text-[9px] font-mono text-amber-500 px-1 rounded-sm bg-amber-500/10 border border-amber-500/20 font-bold">GAST</span>}
|
||||||
|
{isOwnGuest && <span className="text-[9px] font-mono text-emerald-400 px-1 rounded-sm bg-emerald-500/10 border border-emerald-500/20 font-bold">DU</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-1 shrink-0">
|
<div className="flex items-center gap-1 shrink-0">
|
||||||
@@ -229,7 +243,8 @@ export default function ShiftMatrixTable({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user