feat: update package copy and admin control room

This commit is contained in:
Codex Agent
2026-01-15 19:54:04 +01:00
parent ad829ae509
commit 7e32d8f706
42 changed files with 1310 additions and 2017 deletions

View File

@@ -0,0 +1,131 @@
import React from 'react';
import { XStack, YStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { buildLimitWarnings } from '../../lib/limitWarnings';
import type { EventAddonCatalogItem, EventLimitSummary } from '../../api';
import { scopeDefaults, selectAddonKeyForScope } from '../addons';
import { CTAButton, MobileCard } from './Primitives';
import { MobileSelect } from './FormControls';
type LimitTranslator = (key: string, options?: Record<string, unknown>) => string;
export 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' || warning.scope === 'guests')
&& resolveAddonOptions(addons, warning.scope).length ? (
<MobileAddonsPicker
scope={warning.scope}
addons={addons}
busy={busyScope === warning.scope}
onCheckout={onCheckout}
translate={translate}
/>
) : (
<CTAButton
label={
warning.scope === 'photos'
? translate('buyMorePhotos')
: warning.scope === 'gallery'
? translate('extendGallery')
: translate('buyMoreGuests')
}
onPress={() => onCheckout(warning.scope)}
loading={busyScope === warning.scope}
/>
)}
</MobileCard>
))}
</YStack>
);
}
function resolveAddonOptions(addons: EventAddonCatalogItem[], scope: 'photos' | 'gallery' | 'guests'): EventAddonCatalogItem[] {
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);
}
function MobileAddonsPicker({
scope,
addons,
busy,
onCheckout,
translate,
}: {
scope: 'photos' | 'gallery' | 'guests';
addons: EventAddonCatalogItem[];
busy: boolean;
onCheckout: (addonKey: string) => void;
translate: LimitTranslator;
}) {
const options = React.useMemo(() => resolveAddonOptions(addons, scope), [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">
<MobileSelect
value={selected}
onChange={(event) => setSelected(event.target.value)}
containerStyle={{ flex: 1, minWidth: 0 }}
compact
>
{options.map((addon) => (
<option key={addon.key} value={addon.key}>
{addon.label ?? addon.key}
</option>
))}
</MobileSelect>
<CTAButton
label={
scope === 'gallery'
? translate('extendGallery')
: scope === 'guests'
? translate('buyMoreGuests')
: translate('buyMorePhotos')
}
disabled={!selected || busy}
onPress={() => selected && onCheckout(selected)}
loading={busy}
fullWidth={false}
/>
</XStack>
);
}

View File

@@ -281,10 +281,10 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
</Text>
{effectiveActive?.slug ? (
<CTAButton
label={t('status.queueAction', 'Open Photos')}
label={t('status.queueAction', 'Open moderation')}
tone="ghost"
fullWidth={false}
onPress={() => navigate(adminPath(`/mobile/events/${effectiveActive.slug}/photos`))}
onPress={() => navigate(adminPath(`/mobile/events/${effectiveActive.slug}/control-room`))}
/>
) : null}
</MobileCard>

View File

@@ -193,6 +193,15 @@ export function ActionTile({
delayMs?: number;
}) {
const { textStrong } = useAdminTheme();
const backgroundColor = `${color}18`;
const borderColor = `${color}40`;
const shadowColor = `${color}2b`;
const iconShadow = `${color}55`;
const tileStyle = {
...(delayMs ? { animationDelay: `${delayMs}ms` } : {}),
backgroundImage: `linear-gradient(135deg, ${backgroundColor}, ${color}0f)`,
boxShadow: `0 10px 24px ${shadowColor}`,
};
return (
<Pressable
onPress={disabled ? undefined : onPress}
@@ -201,18 +210,26 @@ export function ActionTile({
>
<YStack
className="admin-fade-up"
style={delayMs ? { animationDelay: `${delayMs}ms` } : undefined}
style={tileStyle}
borderRadius={16}
padding="$3"
space="$2.5"
backgroundColor={`${color}22`}
backgroundColor={backgroundColor}
borderWidth={1}
borderColor={`${color}55`}
borderColor={borderColor}
minHeight={110}
alignItems="center"
justifyContent="center"
>
<XStack width={34} height={34} borderRadius={12} backgroundColor={color} alignItems="center" justifyContent="center">
<XStack
width={36}
height={36}
borderRadius={12}
backgroundColor={color}
alignItems="center"
justifyContent="center"
style={{ boxShadow: `0 6px 14px ${iconShadow}` }}
>
<IconCmp size={16} color="white" />
</XStack>
<Text fontSize="$sm" fontWeight="700" color={textStrong} textAlign="center">