Shrink control room photo actions
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-01-20 14:02:49 +01:00
parent 5674ed99f1
commit e5e74febbd
2 changed files with 45 additions and 20 deletions

View File

@@ -93,6 +93,8 @@ type PhotoGridAction = {
icon: React.ComponentType<{ size?: number; color?: string }>; icon: React.ComponentType<{ size?: number; color?: string }>;
onPress: () => void; onPress: () => void;
disabled?: boolean; disabled?: boolean;
backgroundColor?: string;
iconColor?: string;
}; };
function PhotoGrid({ function PhotoGrid({
@@ -108,8 +110,8 @@ function PhotoGrid({
}) { }) {
const gridStyle: React.CSSProperties = { const gridStyle: React.CSSProperties = {
display: 'grid', display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))',
gap: 10, gap: 12,
}; };
return ( return (
@@ -139,8 +141,8 @@ function PhotoGridTile({
isBusy: boolean; isBusy: boolean;
}) { }) {
const { border, muted, surfaceMuted } = useAdminTheme(); const { border, muted, surfaceMuted } = useAdminTheme();
const overlayBg = withAlpha('#0f172a', 0.65); const overlayBg = withAlpha('#0f172a', 0.55);
const actionBg = withAlpha('#ffffff', 0.14); const actionBg = withAlpha('#0f172a', 0.55);
return ( return (
<div <div
@@ -184,7 +186,8 @@ function PhotoGridTile({
padding="$1" padding="$1"
borderRadius={12} borderRadius={12}
backgroundColor={overlayBg} backgroundColor={overlayBg}
space="$1.5" space="$2"
justifyContent="space-between"
> >
{actions.map((action) => ( {actions.map((action) => (
<PhotoActionButton <PhotoActionButton
@@ -193,7 +196,8 @@ function PhotoGridTile({
icon={action.icon} icon={action.icon}
onPress={action.onPress} onPress={action.onPress}
disabled={action.disabled} disabled={action.disabled}
backgroundColor={actionBg} backgroundColor={action.backgroundColor ?? actionBg}
iconColor={action.iconColor ?? '#fff'}
/> />
))} ))}
</XStack> </XStack>
@@ -207,32 +211,32 @@ function PhotoActionButton({
onPress, onPress,
disabled = false, disabled = false,
backgroundColor, backgroundColor,
iconColor,
}: { }: {
label: string; label: string;
icon: React.ComponentType<{ size?: number; color?: string }>; icon: React.ComponentType<{ size?: number; color?: string }>;
onPress: () => void; onPress: () => void;
disabled?: boolean; disabled?: boolean;
backgroundColor: string; backgroundColor: string;
iconColor: string;
}) { }) {
return ( return (
<Pressable <Pressable
onPress={disabled ? undefined : onPress} onPress={disabled ? undefined : onPress}
disabled={disabled} disabled={disabled}
aria-label={label}
title={label}
style={{ flex: 1, opacity: disabled ? 0.55 : 1 }} style={{ flex: 1, opacity: disabled ? 0.55 : 1 }}
> >
<YStack <YStack
alignItems="center" alignItems="center"
justifyContent="center" justifyContent="center"
paddingVertical={6} paddingVertical={8}
paddingHorizontal={4} borderRadius={12}
borderRadius={10} minHeight={40}
minHeight={36}
style={{ backgroundColor }} style={{ backgroundColor }}
> >
<Icon size={14} color="#fff" /> <Icon size={18} color={iconColor} />
<Text fontSize={9} fontWeight="700" color="#fff" textAlign="center">
{label}
</Text>
</YStack> </YStack>
</Pressable> </Pressable>
); );
@@ -258,7 +262,7 @@ export default function MobileEventControlRoomPage() {
const isMember = user?.role === 'member'; const isMember = user?.role === 'member';
const slug = slugParam ?? activeEvent?.slug ?? null; const slug = slugParam ?? activeEvent?.slug ?? null;
const online = useOnlineStatus(); const online = useOnlineStatus();
const { textStrong, text, muted, border, accentSoft, accent, danger } = useAdminTheme(); const { textStrong, text, muted, border, accentSoft, accent, danger, primary } = useAdminTheme();
const [activeTab, setActiveTab] = React.useState<'moderation' | 'live'>('moderation'); const [activeTab, setActiveTab] = React.useState<'moderation' | 'live'>('moderation');
const [moderationPhotos, setModerationPhotos] = React.useState<TenantPhoto[]>([]); const [moderationPhotos, setModerationPhotos] = React.useState<TenantPhoto[]>([]);
@@ -903,6 +907,9 @@ export default function MobileEventControlRoomPage() {
const featureLabel = photo.is_featured const featureLabel = photo.is_featured
? t('photos.actions.unfeature', 'Remove highlight') ? t('photos.actions.unfeature', 'Remove highlight')
: t('photos.actions.feature', 'Set highlight'); : t('photos.actions.feature', 'Set highlight');
const approveBg = withAlpha(primary, 0.78);
const hideBg = withAlpha(danger, 0.75);
const highlightBg = photo.is_featured ? withAlpha(accent, 0.85) : withAlpha(accent, 0.55);
return [ return [
{ {
key: 'approve', key: 'approve',
@@ -910,6 +917,7 @@ export default function MobileEventControlRoomPage() {
icon: Check, icon: Check,
onPress: () => handleModerationAction('approve', photo), onPress: () => handleModerationAction('approve', photo),
disabled: !canApprove || isBusy, disabled: !canApprove || isBusy,
backgroundColor: approveBg,
}, },
{ {
key: 'visibility', key: 'visibility',
@@ -917,6 +925,7 @@ export default function MobileEventControlRoomPage() {
icon: canShow ? Eye : EyeOff, icon: canShow ? Eye : EyeOff,
onPress: () => handleModerationAction(visibilityAction, photo), onPress: () => handleModerationAction(visibilityAction, photo),
disabled: isBusy, disabled: isBusy,
backgroundColor: hideBg,
}, },
{ {
key: 'feature', key: 'feature',
@@ -924,6 +933,7 @@ export default function MobileEventControlRoomPage() {
icon: Sparkles, icon: Sparkles,
onPress: () => handleModerationAction(featureAction, photo), onPress: () => handleModerationAction(featureAction, photo),
disabled: isBusy, disabled: isBusy,
backgroundColor: highlightBg,
}, },
]; ];
}} }}
@@ -1023,6 +1033,9 @@ export default function MobileEventControlRoomPage() {
? t('photos.actions.unfeature', 'Remove highlight') ? t('photos.actions.unfeature', 'Remove highlight')
: t('photos.actions.feature', 'Set highlight'); : t('photos.actions.feature', 'Set highlight');
const approveBg = withAlpha(primary, 0.78);
const hideBg = withAlpha(danger, 0.75);
const highlightBg = photo.is_featured ? withAlpha(accent, 0.85) : withAlpha(accent, 0.55);
return [ return [
{ {
key: 'approve', key: 'approve',
@@ -1038,6 +1051,7 @@ export default function MobileEventControlRoomPage() {
} }
}, },
disabled: approveDisabled, disabled: approveDisabled,
backgroundColor: approveBg,
}, },
{ {
key: 'visibility', key: 'visibility',
@@ -1051,6 +1065,7 @@ export default function MobileEventControlRoomPage() {
void handleReject(photo); void handleReject(photo);
}, },
disabled: !online || isLiveBusy || isModerationBusy, disabled: !online || isLiveBusy || isModerationBusy,
backgroundColor: hideBg,
}, },
{ {
key: 'feature', key: 'feature',
@@ -1058,6 +1073,7 @@ export default function MobileEventControlRoomPage() {
icon: Sparkles, icon: Sparkles,
onPress: () => handleModerationAction(featureAction, photo), onPress: () => handleModerationAction(featureAction, photo),
disabled: isModerationBusy, disabled: isModerationBusy,
backgroundColor: highlightBg,
}, },
]; ];
}} }}

View File

@@ -99,8 +99,16 @@ vi.mock('@tamagui/text', () => ({
})); }));
vi.mock('@tamagui/react-native-web-lite', () => ({ vi.mock('@tamagui/react-native-web-lite', () => ({
Pressable: ({ children, onPress }: { children: React.ReactNode; onPress?: () => void }) => ( Pressable: ({
<button type="button" onClick={onPress}> children,
onPress,
...rest
}: {
children: React.ReactNode;
onPress?: () => void;
[key: string]: unknown;
}) => (
<button type="button" onClick={onPress} {...rest}>
{children} {children}
</button> </button>
), ),
@@ -114,6 +122,7 @@ vi.mock('../theme', () => ({
border: '#e5e7eb', border: '#e5e7eb',
accentSoft: '#eef2ff', accentSoft: '#eef2ff',
accent: '#6366f1', accent: '#6366f1',
primary: '#ff5a5f',
danger: '#dc2626', danger: '#dc2626',
surfaceMuted: '#f9fafb', surfaceMuted: '#f9fafb',
}), }),
@@ -172,8 +181,8 @@ describe('MobileEventControlRoomPage', () => {
it('renders compact grid actions for moderation photos', async () => { it('renders compact grid actions for moderation photos', async () => {
render(<MobileEventControlRoomPage />); render(<MobileEventControlRoomPage />);
expect(await screen.findByText('Approve')).toBeInTheDocument(); expect(await screen.findByLabelText('Approve')).toBeInTheDocument();
expect(screen.getByText('Hide')).toBeInTheDocument(); expect(screen.getByLabelText('Hide')).toBeInTheDocument();
expect(screen.getByText('Set highlight')).toBeInTheDocument(); expect(screen.getByLabelText('Set highlight')).toBeInTheDocument();
}); });
}); });