Shrink control room photo actions
This commit is contained in:
@@ -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,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user