Added opaque join-token support across backend and frontend: new migration/model/service/endpoints, guest controllers now resolve tokens, and the demo seeder seeds a token. Tenant event details list/manage tokens with copy/revoke actions, and the guest PWA uses tokens end-to-end (routing, storage, uploads, achievements, etc.). Docs TODO updated to reflect completed steps.
This commit is contained in:
@@ -29,12 +29,12 @@ function TabLink({
|
||||
}
|
||||
|
||||
export default function BottomNav() {
|
||||
const { slug } = useParams();
|
||||
const { token } = useParams();
|
||||
const location = useLocation();
|
||||
const { event } = useEventData();
|
||||
|
||||
if (!slug) return null; // Only show bottom nav within event context
|
||||
const base = `/e/${encodeURIComponent(slug)}`;
|
||||
if (!token) return null; // Only show bottom nav within event context
|
||||
const base = `/e/${encodeURIComponent(token)}`;
|
||||
const currentPath = location.pathname;
|
||||
const locale = event?.default_locale || 'de';
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function BottomNav() {
|
||||
const t = translations[locale as keyof typeof translations] || translations.de;
|
||||
|
||||
// Improved active state logic
|
||||
const isHomeActive = currentPath === base || currentPath === `/${slug}`;
|
||||
const isHomeActive = currentPath === base || currentPath === `/${token}`;
|
||||
const isTasksActive = currentPath.startsWith(`${base}/tasks`) || currentPath === `${base}/upload`;
|
||||
const isAchievementsActive = currentPath.startsWith(`${base}/achievements`);
|
||||
const isGalleryActive = currentPath.startsWith(`${base}/gallery`) || currentPath.startsWith(`${base}/photos`);
|
||||
|
||||
@@ -16,7 +16,8 @@ interface EmotionPickerProps {
|
||||
}
|
||||
|
||||
export default function EmotionPicker({ onSelect }: EmotionPickerProps) {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const { token: slug } = useParams<{ token: string }>();
|
||||
const eventKey = slug ?? '';
|
||||
const navigate = useNavigate();
|
||||
const [emotions, setEmotions] = useState<Emotion[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -33,7 +34,7 @@ export default function EmotionPicker({ onSelect }: EmotionPickerProps) {
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (!slug) return;
|
||||
if (!eventKey) return;
|
||||
|
||||
async function fetchEmotions() {
|
||||
try {
|
||||
@@ -41,7 +42,7 @@ export default function EmotionPicker({ onSelect }: EmotionPickerProps) {
|
||||
setError(null);
|
||||
|
||||
// Try API first
|
||||
const response = await fetch(`/api/v1/events/${slug}/emotions`);
|
||||
const response = await fetch(`/api/v1/events/${encodeURIComponent(eventKey)}/emotions`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setEmotions(Array.isArray(data) ? data : fallbackEmotions);
|
||||
@@ -60,14 +61,15 @@ export default function EmotionPicker({ onSelect }: EmotionPickerProps) {
|
||||
}
|
||||
|
||||
fetchEmotions();
|
||||
}, [slug]);
|
||||
}, [eventKey]);
|
||||
|
||||
const handleEmotionSelect = (emotion: Emotion) => {
|
||||
if (onSelect) {
|
||||
onSelect(emotion);
|
||||
} else {
|
||||
// Default: Navigate to tasks with emotion filter
|
||||
navigate(`/e/${slug}/tasks?emotion=${emotion.slug}`);
|
||||
if (!eventKey) return;
|
||||
navigate(`/e/${encodeURIComponent(eventKey)}/tasks?emotion=${emotion.slug}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -139,11 +141,14 @@ export default function EmotionPicker({ onSelect }: EmotionPickerProps) {
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full text-sm text-gray-600 dark:text-gray-300 hover:text-pink-600 hover:bg-pink-50 dark:hover:bg-gray-800 border-t border-gray-200 dark:border-gray-700 pt-3 mt-3"
|
||||
onClick={() => navigate(`/e/${slug}/tasks`)}
|
||||
onClick={() => {
|
||||
if (!eventKey) return;
|
||||
navigate(`/e/${encodeURIComponent(eventKey)}/tasks`);
|
||||
}}
|
||||
>
|
||||
Überspringen und Aufgabe wählen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ export default function GalleryPreview({ slug }: Props) {
|
||||
My Photos
|
||||
</button>
|
||||
</div>
|
||||
<Link to={`/e/${slug}/gallery?mode=${mode}`} className="text-sm text-pink-600 hover:text-pink-700 font-medium">
|
||||
<Link to={`/e/${encodeURIComponent(slug)}/gallery?mode=${mode}`} className="text-sm text-pink-600 hover:text-pink-700 font-medium">
|
||||
Alle ansehen →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -97,7 +97,7 @@ export default function GalleryPreview({ slug }: Props) {
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{items.map((p: any) => (
|
||||
<Link key={p.id} to={`/e/${slug}/gallery?photoId=${p.id}`} className="block">
|
||||
<Link key={p.id} to={`/e/${encodeURIComponent(slug)}/gallery?photoId=${p.id}`} className="block">
|
||||
<div className="relative">
|
||||
<img
|
||||
src={p.thumbnail_path || p.file_path}
|
||||
@@ -123,4 +123,3 @@ export default function GalleryPreview({ slug }: Props) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ export default function Header({ slug, title = '' }: { slug?: string; title?: st
|
||||
}
|
||||
|
||||
const { event, loading: eventLoading, error: eventError } = useEventData();
|
||||
const stats = statsContext && statsContext.slug === slug ? statsContext : undefined;
|
||||
const guestName = identity && identity.slug === slug && identity.hydrated && identity.name ? identity.name : null;
|
||||
const stats = statsContext && statsContext.eventKey === slug ? statsContext : undefined;
|
||||
const guestName = identity && identity.eventKey === slug && identity.hydrated && identity.name ? identity.name : null;
|
||||
|
||||
if (eventLoading) {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user