feat: update package copy and admin control room
This commit is contained in:
131
resources/js/admin/mobile/components/LimitWarnings.tsx
Normal file
131
resources/js/admin/mobile/components/LimitWarnings.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user