removed the old event admin components and pages

This commit is contained in:
Codex Agent
2025-12-12 13:38:06 +01:00
parent bbf8d4a0f4
commit 1719d96fed
85 changed files with 994 additions and 19981 deletions

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Image as ImageIcon, RefreshCcw, Search, Filter } from 'lucide-react';
import { YStack, XStack } from '@tamagui/stacks';
@@ -7,13 +7,28 @@ import { SizableText as Text } from '@tamagui/text';
import { Pressable } from '@tamagui/react-native-web-lite';
import { MobileShell } from './components/MobileShell';
import { MobileCard, PillBadge, CTAButton } from './components/Primitives';
import { getEventPhotos, updatePhotoVisibility, featurePhoto, unfeaturePhoto, TenantPhoto } from '../api';
import {
getEventPhotos,
updatePhotoVisibility,
featurePhoto,
unfeaturePhoto,
TenantPhoto,
EventAddonCatalogItem,
createEventAddonCheckout,
getAddonCatalog,
EventAddonSummary,
EventLimitSummary,
getEvent,
} from '../api';
import toast from 'react-hot-toast';
import { isAuthError } from '../auth/tokens';
import { getApiErrorMessage } from '../lib/apiError';
import { MobileSheet } from './components/Sheet';
import { useEventContext } from '../context/EventContext';
import { useTheme } from '@tamagui/core';
import { buildLimitWarnings } from '../lib/limitWarnings';
import { adminPath } from '../constants';
import { scopeDefaults, selectAddonKeyForScope } from './addons';
type FilterKey = 'all' | 'featured' | 'hidden';
@@ -22,6 +37,7 @@ export default function MobileEventPhotosPage() {
const { activeEvent, selectEvent } = useEventContext();
const slug = slugParam ?? activeEvent?.slug ?? null;
const navigate = useNavigate();
const location = useLocation();
const { t } = useTranslation('management');
const [photos, setPhotos] = React.useState<TenantPhoto[]>([]);
@@ -38,6 +54,10 @@ export default function MobileEventPhotosPage() {
const [onlyFeatured, setOnlyFeatured] = React.useState(false);
const [onlyHidden, setOnlyHidden] = React.useState(false);
const [lightbox, setLightbox] = React.useState<TenantPhoto | null>(null);
const [limits, setLimits] = React.useState<EventLimitSummary | null>(null);
const [catalogAddons, setCatalogAddons] = React.useState<EventAddonCatalogItem[]>([]);
const [eventAddons, setEventAddons] = React.useState<EventAddonSummary[]>([]);
const [busyScope, setBusyScope] = React.useState<string | null>(null);
const theme = useTheme();
const text = String(theme.color?.val ?? '#111827');
const muted = String(theme.gray?.val ?? '#4b5563');
@@ -82,8 +102,15 @@ export default function MobileEventPhotosPage() {
});
setPhotos((prev) => (page === 1 ? result.photos : [...prev, ...result.photos]));
setTotalCount(result.meta?.total ?? result.photos.length);
setLimits(result.limits ?? null);
const lastPage = result.meta?.last_page ?? 1;
setHasMore(page < lastPage);
const [addons, event] = await Promise.all([
getAddonCatalog().catch(() => [] as EventAddonCatalogItem[]),
getEvent(slug).catch(() => null),
]);
setCatalogAddons(addons ?? []);
setEventAddons(event?.addons ?? []);
} catch (err) {
if (!isAuthError(err)) {
setError(getApiErrorMessage(err, t('mobilePhotos.loadFailed', 'Fotos konnten nicht geladen werden.')));
@@ -91,12 +118,24 @@ export default function MobileEventPhotosPage() {
} finally {
setLoading(false);
}
}, [slug, filter, t, page]);
}, [slug, filter, t, page, onlyFeatured, onlyHidden, search]);
React.useEffect(() => {
void load();
}, [load]);
React.useEffect(() => {
if (!location.search || !slug) return;
const params = new URLSearchParams(location.search);
if (params.get('addon_success')) {
toast.success(t('mobileBilling.addonApplied', 'Add-on applied. Limits update shortly.'));
setPage(1);
void load();
params.delete('addon_success');
navigate({ pathname: location.pathname, search: params.toString() ? `?${params.toString()}` : '' }, { replace: true });
}
}, [location.search, slug, load, navigate, t, location.pathname]);
React.useEffect(() => {
setPage(1);
}, [filter, slug]);
@@ -215,6 +254,15 @@ export default function MobileEventPhotosPage() {
</MobileCard>
) : (
<YStack space="$3">
<LimitWarnings
limits={limits}
addons={catalogAddons}
onCheckout={(scopeOrKey) => { void handleCheckout(scopeOrKey, slug, catalogAddons, setBusyScope, t); }}
busyScope={busyScope}
translate={translateLimits(t)}
textColor={text}
borderColor={border}
/>
<Text fontSize="$sm" color={muted}>
{t('mobilePhotos.count', '{{count}} photos', { count: totalCount })}
</Text>
@@ -358,6 +406,15 @@ export default function MobileEventPhotosPage() {
/>
</YStack>
</MobileSheet>
{eventAddons.length ? (
<YStack marginTop="$3" space="$2">
<Text fontSize="$sm" color={text} fontWeight="700">
{t('events.sections.addons.title', 'Add-ons & Upgrades')}
</Text>
<EventAddonList addons={eventAddons} textColor={text} mutedColor={muted} />
</YStack>
) : null}
</MobileShell>
);
}
@@ -372,3 +429,197 @@ function Field({ label, color, children }: { label: string; color: string; child
</YStack>
);
}
type LimitTranslator = (key: string, options?: Record<string, unknown>) => string;
function translateLimits(t: (key: string, defaultValue?: string, options?: Record<string, unknown>) => string): LimitTranslator {
const defaults: Record<string, string> = {
photosBlocked: 'Upload limit reached. Buy more photos to continue.',
photosWarning: '{{remaining}} of {{limit}} photos remaining.',
guestsBlocked: 'Guest limit reached.',
guestsWarning: '{{remaining}} of {{limit}} guests remaining.',
galleryExpired: 'Gallery expired. Extend to keep it online.',
galleryWarningDay: 'Gallery expires in {{days}} day.',
galleryWarningDays: 'Gallery expires in {{days}} days.',
buyMorePhotos: 'Buy more photos',
extendGallery: 'Extend gallery',
};
return (key, options) => t(`limits.${key}`, defaults[key] ?? key, options);
}
function LimitWarnings({
limits,
addons,
onCheckout,
busyScope,
translate,
textColor,
borderColor,
}: {
limits: EventLimitSummary | null;
addons: EventAddonCatalogItem[];
onCheckout: (scopeOrKey: 'photos' | 'gallery' | string) => void;
busyScope: string | null;
translate: LimitTranslator;
textColor: string;
borderColor: string;
}) {
const warnings = React.useMemo(() => buildLimitWarnings(limits, translate), [limits, translate]);
if (!warnings.length) {
return null;
}
return (
<YStack space="$2">
{warnings.map((warning) => (
<MobileCard key={warning.id} borderColor={borderColor} space="$2">
<Text fontSize="$sm" color={textColor} fontWeight="700">
{warning.message}
</Text>
{(warning.scope === 'photos' || warning.scope === 'gallery') && addons.length ? (
<MobileAddonsPicker
scope={warning.scope}
addons={addons}
busy={busyScope === warning.scope}
onCheckout={onCheckout}
translate={translate}
/>
) : null}
<CTAButton
label={
warning.scope === 'photos'
? translate('buyMorePhotos')
: warning.scope === 'gallery'
? translate('extendGallery')
: translate('buyMorePhotos')
}
onPress={() => onCheckout(warning.scope)}
loading={busyScope === warning.scope}
/>
</MobileCard>
))}
</YStack>
);
}
function MobileAddonsPicker({
scope,
addons,
busy,
onCheckout,
translate,
}: {
scope: 'photos' | 'gallery';
addons: EventAddonCatalogItem[];
busy: boolean;
onCheckout: (addonKey: string) => void;
translate: LimitTranslator;
}) {
const options = React.useMemo(() => {
const whitelist = scopeDefaults[scope];
const filtered = addons.filter((addon) => addon.price_id && whitelist.includes(addon.key));
return filtered.length ? filtered : addons.filter((addon) => addon.price_id);
}, [addons, scope]);
const [selected, setSelected] = React.useState<string>(() => options[0]?.key ?? selectAddonKeyForScope(addons, scope));
React.useEffect(() => {
if (options[0]?.key) {
setSelected(options[0].key);
}
}, [options]);
if (!options.length) {
return null;
}
return (
<XStack space="$2" alignItems="center">
<select
value={selected}
onChange={(event) => setSelected(event.target.value)}
style={{
flex: 1,
height: 40,
borderRadius: 10,
border: '1px solid #e5e7eb',
padding: '0 12px',
fontSize: 13,
background: '#fff',
}}
>
{options.map((addon) => (
<option key={addon.key} value={addon.key}>
{addon.label ?? addon.key}
</option>
))}
</select>
<CTAButton
label={scope === 'gallery' ? translate('extendGallery') : translate('buyMorePhotos')}
disabled={!selected || busy}
onPress={() => selected && onCheckout(selected)}
loading={busy}
/>
</XStack>
);
}
function EventAddonList({ addons, textColor, mutedColor }: { addons: EventAddonSummary[]; textColor: string; mutedColor: string }) {
return (
<YStack space="$2">
{addons.map((addon) => (
<MobileCard key={addon.id} borderColor="#e5e7eb" space="$1.5">
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$sm" fontWeight="700" color={textColor}>
{addon.label ?? addon.key}
</Text>
<PillBadge tone={addon.status === 'completed' ? 'success' : addon.status === 'pending' ? 'warning' : 'muted'}>
{addon.status}
</PillBadge>
</XStack>
<Text fontSize="$xs" color={mutedColor}>
{addon.purchased_at ? new Date(addon.purchased_at).toLocaleString() : '—'}
</Text>
<XStack space="$2" marginTop="$1" flexWrap="wrap">
{addon.extra_photos ? <PillBadge tone="muted">+{addon.extra_photos} photos</PillBadge> : null}
{addon.extra_guests ? <PillBadge tone="muted">+{addon.extra_guests} guests</PillBadge> : null}
{addon.extra_gallery_days ? <PillBadge tone="muted">+{addon.extra_gallery_days} days</PillBadge> : null}
</XStack>
</MobileCard>
))}
</YStack>
);
}
async function handleCheckout(
scopeOrKey: 'photos' | 'gallery' | string,
slug: string | null,
addons: EventAddonCatalogItem[],
setBusyScope: (scope: string | null) => void,
t: (key: string, defaultValue?: string) => string,
): Promise<void> {
if (!slug) return;
const scope = scopeOrKey === 'photos' || scopeOrKey === 'gallery' ? scopeOrKey : scopeOrKey.includes('gallery') ? 'gallery' : 'photos';
const addonKey = scopeOrKey === 'photos' || scopeOrKey === 'gallery' ? selectAddonKeyForScope(addons, scope) : scopeOrKey;
const currentUrl = typeof window !== 'undefined' ? `${window.location.origin}${adminPath(`/mobile/events/${slug}/photos`)}` : '';
const successUrl = `${currentUrl}?addon_success=1`;
setBusyScope(scope);
try {
const checkout = await createEventAddonCheckout(slug, {
addon_key: addonKey,
quantity: 1,
success_url: successUrl,
cancel_url: currentUrl,
});
if (checkout.checkout_url) {
window.location.href = checkout.checkout_url;
} else {
toast.error(t('mobileBilling.checkoutUnavailable', 'Checkout unavailable right now.'));
}
} catch (err) {
toast.error(getApiErrorMessage(err, t('mobileBilling.checkoutFailed', 'Checkout failed.')));
} finally {
setBusyScope(null);
}
}