feat: Implement Event Deep-Linking (?event=ID) and TEILEN share link buttons

This commit is contained in:
Richard
2026-07-31 12:28:31 +02:00
parent 1a880d3452
commit 5f1607f0d3
4 changed files with 78 additions and 5 deletions
+2 -2
View File
@@ -11,8 +11,8 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<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">
<title>Schichtplaner — Veranstaltungsschichtpläne</title> <title>Schichtplaner — Veranstaltungsschichtpläne</title>
<script type="module" crossorigin src="/assets/index-Mp9FLWF3.js"></script> <script type="module" crossorigin src="/assets/index-BUskNRc4.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B6pC8vGA.css"> <link rel="stylesheet" crossorigin href="/assets/index-DHeBvKbG.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>
+53 -1
View File
@@ -180,16 +180,38 @@ export default function App() {
} catch (e) {} } catch (e) {}
}; };
const updateUrlParam = (id) => {
if (!id) return;
try {
const url = new URL(window.location.href);
url.searchParams.set('event', id);
window.history.replaceState(null, '', url.pathname + url.search);
} catch (e) {}
};
const refreshEvents = async () => { const refreshEvents = async () => {
try { try {
const data = await apiFetch('/events/'); const data = await apiFetch('/events/');
const list = data.results || data; const list = data.results || data;
setEvents(list); setEvents(list);
if (list.length > 0 && !selectedEventId) {
// Check URL query parameters for deep linking (?event=12)
const urlParams = new URLSearchParams(window.location.search);
const urlEventParam = urlParams.get('event') || urlParams.get('event_id');
const targetIdFromUrl = urlEventParam ? parseInt(urlEventParam, 10) : null;
if (targetIdFromUrl && list.some(e => e.id === targetIdFromUrl)) {
setSelectedEventId(targetIdFromUrl);
fetchEventMatrix(targetIdFromUrl);
setActiveTab('schedule');
updateUrlParam(targetIdFromUrl);
} else if (list.length > 0 && !selectedEventId) {
setSelectedEventId(list[0].id); setSelectedEventId(list[0].id);
fetchEventMatrix(list[0].id); fetchEventMatrix(list[0].id);
updateUrlParam(list[0].id);
} else if (selectedEventId) { } else if (selectedEventId) {
fetchEventMatrix(selectedEventId); fetchEventMatrix(selectedEventId);
updateUrlParam(selectedEventId);
} }
} catch (e) {} } catch (e) {}
}; };
@@ -205,6 +227,34 @@ export default function App() {
setSelectedEventId(id); setSelectedEventId(id);
fetchEventMatrix(id); fetchEventMatrix(id);
setActiveTab('schedule'); setActiveTab('schedule');
updateUrlParam(id);
};
const fallbackCopyText = (text) => {
const el = document.createElement('textarea');
el.value = text;
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
showNotification('Direktlink zum Schichtplan in Zwischenablage kopiert! 🔗');
};
const handleShareEvent = (targetEventOrId) => {
const targetId = typeof targetEventOrId === 'object' ? targetEventOrId.id : (targetEventOrId || selectedEventId);
if (!targetId) return;
const shareUrl = `${window.location.origin}${window.location.pathname}?event=${targetId}`;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(shareUrl).then(() => {
showNotification('Direktlink zum Schichtplan in Zwischenablage kopiert! 🔗');
}).catch(() => {
fallbackCopyText(shareUrl);
});
} else {
fallbackCopyText(shareUrl);
}
}; };
const handleLogout = () => { const handleLogout = () => {
@@ -464,6 +514,7 @@ export default function App() {
alert(err.message || 'Aktion fehlgeschlagen.'); alert(err.message || 'Aktion fehlgeschlagen.');
} }
}} }}
onShareEvent={handleShareEvent}
onEditEvent={(evt) => { onEditEvent={(evt) => {
setEventToEdit(evt); setEventToEdit(evt);
setActiveTab('event-editor'); setActiveTab('event-editor');
@@ -572,6 +623,7 @@ export default function App() {
onGenerateClaimLink={handleGenerateClaimLink} onGenerateClaimLink={handleGenerateClaimLink}
onExportPdf={handleExportPdf} onExportPdf={handleExportPdf}
onPrintView={handlePrintView} onPrintView={handlePrintView}
onShareLink={() => handleShareEvent(selectedEventId)}
/> />
</div> </div>
)} )}
+11 -1
View File
@@ -1,7 +1,7 @@
/* Hallmark · pre-emit critique: P5 H5 E5 S5 R5 V5 */ /* Hallmark · pre-emit critique: P5 H5 E5 S5 R5 V5 */
/* Hallmark · component: shift-matrix-table · genre: editorial · theme: Atelier (Dark & Light) · studied-DNA: https://inihaus.de */ /* Hallmark · component: shift-matrix-table · genre: editorial · theme: Atelier (Dark & Light) · studied-DNA: https://inihaus.de */
import React from 'react'; import React from 'react';
import { Clock, UserCheck, Shield, Printer, Download, Award, CheckCircle2, UserX, UserPlus, MapPin, Edit3, Trash2 } from 'lucide-react'; import { Clock, UserCheck, Shield, Printer, Download, Award, CheckCircle2, UserX, UserPlus, MapPin, Edit3, Trash2, Share2 } from 'lucide-react';
export default function ShiftMatrixTable({ export default function ShiftMatrixTable({
event, event,
@@ -12,6 +12,7 @@ export default function ShiftMatrixTable({
onGenerateClaimLink, onGenerateClaimLink,
onExportPdf, onExportPdf,
onPrintView, onPrintView,
onShareLink,
onEditEvent, onEditEvent,
onDeleteEvent onDeleteEvent
}) { }) {
@@ -92,6 +93,15 @@ export default function ShiftMatrixTable({
<Trash2 className="w-3.5 h-3.5" /> LÖSCHEN <Trash2 className="w-3.5 h-3.5" /> LÖSCHEN
</button> </button>
)} )}
{onShareLink && (
<button
onClick={onShareLink}
className="px-3.5 py-1.5 rounded-sm bg-blue-500/10 hover:bg-blue-500/20 text-blue-400 border border-blue-500/30 transition flex items-center gap-1.5 font-bold"
title="Direktlink zu diesem Schichtplan kopieren"
>
<Share2 className="w-3.5 h-3.5" /> TEILEN
</button>
)}
<button <button
onClick={onPrintView} onClick={onPrintView}
className="px-3.5 py-1.5 rounded-sm bg-surface hover:bg-surface-hover text-main border border-grid transition flex items-center gap-1.5 font-bold" className="px-3.5 py-1.5 rounded-sm bg-surface hover:bg-surface-hover text-main border border-grid transition flex items-center gap-1.5 font-bold"
+12 -1
View File
@@ -1,7 +1,7 @@
/* Hallmark · pre-emit critique: P5 H5 E5 S5 R5 V5 */ /* Hallmark · pre-emit critique: P5 H5 E5 S5 R5 V5 */
/* Hallmark · redesign: HomePage · studied-DNA: https://inihaus.de · macrostructure: Newsstand Feed */ /* Hallmark · redesign: HomePage · studied-DNA: https://inihaus.de · macrostructure: Newsstand Feed */
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Search, Calendar, MapPin, Clock, ArrowRight, Plus, EyeOff, CheckCircle2, Edit3, ShieldAlert, Trash2 } from 'lucide-react'; import { Search, Calendar, MapPin, Clock, ArrowRight, Plus, EyeOff, CheckCircle2, Edit3, ShieldAlert, Trash2, Share2 } from 'lucide-react';
export default function HomePage({ export default function HomePage({
events, events,
@@ -9,6 +9,7 @@ export default function HomePage({
onSelectEvent, onSelectEvent,
onOpenCreateEvent, onOpenCreateEvent,
onToggleEventActive, onToggleEventActive,
onShareEvent,
onEditEvent, onEditEvent,
onDeleteEvent, onDeleteEvent,
branding branding
@@ -190,6 +191,16 @@ export default function HomePage({
Schichtplan <ArrowRight className="w-3.5 h-3.5" /> Schichtplan <ArrowRight className="w-3.5 h-3.5" />
</button> </button>
{onShareEvent && (
<button
onClick={() => onShareEvent(evt)}
className="py-1.5 px-2.5 rounded-sm text-[10px] font-mono font-bold bg-blue-500/10 text-blue-400 border border-blue-500/20 hover:bg-blue-500/20 transition flex items-center gap-1 shrink-0"
title="Direktlink zu dieser Veranstaltung kopieren"
>
<Share2 className="w-3.5 h-3.5" /> TEILEN
</button>
)}
{canManage && onEditEvent && ( {canManage && onEditEvent && (
<button <button
onClick={() => onEditEvent(evt)} onClick={() => onEditEvent(evt)}