Revamp guest v2 upload camera controls
This commit is contained in:
@@ -32,6 +32,7 @@ export default function AppShell({ children }: AppShellProps) {
|
|||||||
const { isDark } = useGuestThemeVariant();
|
const { isDark } = useGuestThemeVariant();
|
||||||
const actionIconColor = isDark ? '#F8FAFF' : '#0F172A';
|
const actionIconColor = isDark ? '#F8FAFF' : '#0F172A';
|
||||||
const matomoEnabled = typeof window !== 'undefined' && Boolean((window as any).__MATOMO_GUEST__?.enabled);
|
const matomoEnabled = typeof window !== 'undefined' && Boolean((window as any).__MATOMO_GUEST__?.enabled);
|
||||||
|
const isUploadRoute = /\/upload(?:\/|$)/.test(location.pathname);
|
||||||
const showFab = !/\/photo\/\d+/.test(location.pathname);
|
const showFab = !/\/photo\/\d+/.test(location.pathname);
|
||||||
|
|
||||||
const goTo = (path: string) => () => {
|
const goTo = (path: string) => () => {
|
||||||
@@ -145,13 +146,14 @@ export default function AppShell({ children }: AppShellProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
<FloatingActionButton
|
<FloatingActionButton
|
||||||
onPress={goTo('/upload')}
|
onPress={goTo('/upload')}
|
||||||
|
hidden={isUploadRoute}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="$4"
|
size="$4"
|
||||||
circular
|
circular
|
||||||
position="fixed"
|
position="fixed"
|
||||||
bottom={28}
|
bottom={28}
|
||||||
left="calc(50% + 52px)"
|
right={20}
|
||||||
zIndex={1100}
|
zIndex={1100}
|
||||||
backgroundColor={isDark ? 'rgba(12, 16, 32, 0.75)' : 'rgba(255, 255, 255, 0.9)'}
|
backgroundColor={isDark ? 'rgba(12, 16, 32, 0.75)' : 'rgba(255, 255, 255, 0.9)'}
|
||||||
borderWidth={1}
|
borderWidth={1}
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import { useGuestThemeVariant } from '../lib/guestTheme';
|
|||||||
type FloatingActionButtonProps = {
|
type FloatingActionButtonProps = {
|
||||||
onPress: () => void;
|
onPress: () => void;
|
||||||
onLongPress?: () => void;
|
onLongPress?: () => void;
|
||||||
|
hidden?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function FloatingActionButton({ onPress, onLongPress }: FloatingActionButtonProps) {
|
export default function FloatingActionButton({ onPress, onLongPress, hidden = false }: FloatingActionButtonProps) {
|
||||||
const longPressTriggered = React.useRef(false);
|
const longPressTriggered = React.useRef(false);
|
||||||
const { isDark } = useGuestThemeVariant();
|
const { isDark } = useGuestThemeVariant();
|
||||||
|
const translateValue = hidden ? 'translateX(-50%) translateY(36px) scale(0.72)' : 'translateX(-50%)';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@@ -45,8 +47,11 @@ export default function FloatingActionButton({ onPress, onLongPress }: FloatingA
|
|||||||
shadowOpacity={0.5}
|
shadowOpacity={0.5}
|
||||||
shadowRadius={22}
|
shadowRadius={22}
|
||||||
shadowOffset={{ width: 0, height: 10 }}
|
shadowOffset={{ width: 0, height: 10 }}
|
||||||
|
opacity={hidden ? 0 : 1}
|
||||||
|
pointerEvents={hidden ? 'none' : 'auto'}
|
||||||
style={{
|
style={{
|
||||||
transform: 'translateX(-50%)',
|
transform: translateValue,
|
||||||
|
transition: 'transform 320ms cubic-bezier(0.22, 0.61, 0.36, 1), opacity 220ms ease',
|
||||||
boxShadow: isDark
|
boxShadow: isDark
|
||||||
? '0 20px 40px rgba(255, 79, 216, 0.38), 0 0 0 8px rgba(255, 79, 216, 0.16)'
|
? '0 20px 40px rgba(255, 79, 216, 0.38), 0 0 0 8px rgba(255, 79, 216, 0.16)'
|
||||||
: '0 18px 32px rgba(15, 23, 42, 0.2), 0 0 0 8px rgba(255, 255, 255, 0.7)',
|
: '0 18px 32px rgba(15, 23, 42, 0.2), 0 0 0 8px rgba(255, 255, 255, 0.7)',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { YStack, XStack } from '@tamagui/stacks';
|
import { YStack, XStack } from '@tamagui/stacks';
|
||||||
import { SizableText as Text } from '@tamagui/text';
|
import { SizableText as Text } from '@tamagui/text';
|
||||||
import { Button } from '@tamagui/button';
|
import { Button } from '@tamagui/button';
|
||||||
import { Camera, FlipHorizontal, Image, ListVideo, Sparkles, UploadCloud, X } from 'lucide-react';
|
import { Camera, FlipHorizontal, Image, ListVideo, RefreshCcw, Sparkles, UploadCloud, X, Zap, ZapOff } from 'lucide-react';
|
||||||
import AppShell from '../components/AppShell';
|
import AppShell from '../components/AppShell';
|
||||||
import { useEventData } from '../context/EventDataContext';
|
import { useEventData } from '../context/EventDataContext';
|
||||||
import { useOptionalGuestIdentity } from '../context/GuestIdentityContext';
|
import { useOptionalGuestIdentity } from '../context/GuestIdentityContext';
|
||||||
@@ -47,6 +47,7 @@ export default function UploadScreen() {
|
|||||||
const [cameraState, setCameraState] = React.useState<'idle' | 'starting' | 'ready' | 'denied' | 'blocked' | 'unsupported' | 'error' | 'preview'>('idle');
|
const [cameraState, setCameraState] = React.useState<'idle' | 'starting' | 'ready' | 'denied' | 'blocked' | 'unsupported' | 'error' | 'preview'>('idle');
|
||||||
const [facingMode, setFacingMode] = React.useState<'user' | 'environment'>('environment');
|
const [facingMode, setFacingMode] = React.useState<'user' | 'environment'>('environment');
|
||||||
const [mirror, setMirror] = React.useState(true);
|
const [mirror, setMirror] = React.useState(true);
|
||||||
|
const [flashPreferred, setFlashPreferred] = React.useState(false);
|
||||||
const [previewFile, setPreviewFile] = React.useState<File | null>(null);
|
const [previewFile, setPreviewFile] = React.useState<File | null>(null);
|
||||||
const [previewUrl, setPreviewUrl] = React.useState<string | null>(null);
|
const [previewUrl, setPreviewUrl] = React.useState<string | null>(null);
|
||||||
const { isDark } = useGuestThemeVariant();
|
const { isDark } = useGuestThemeVariant();
|
||||||
@@ -55,6 +56,10 @@ export default function UploadScreen() {
|
|||||||
const iconColor = isDark ? '#F8FAFF' : '#0F172A';
|
const iconColor = isDark ? '#F8FAFF' : '#0F172A';
|
||||||
const mutedButton = isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(15, 23, 42, 0.06)';
|
const mutedButton = isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(15, 23, 42, 0.06)';
|
||||||
const mutedButtonBorder = isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(15, 23, 42, 0.12)';
|
const mutedButtonBorder = isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(15, 23, 42, 0.12)';
|
||||||
|
const fabShadow = isDark
|
||||||
|
? '0 20px 40px rgba(255, 79, 216, 0.38), 0 0 0 8px rgba(255, 79, 216, 0.16)'
|
||||||
|
: '0 18px 32px rgba(15, 23, 42, 0.2), 0 0 0 8px rgba(255, 255, 255, 0.7)';
|
||||||
|
const accessoryShadow = isDark ? '0 10px 20px rgba(2, 6, 23, 0.45)' : '0 8px 16px rgba(15, 23, 42, 0.14)';
|
||||||
const autoApprove = event?.guest_upload_visibility === 'immediate';
|
const autoApprove = event?.guest_upload_visibility === 'immediate';
|
||||||
const isExpanded = cameraState === 'ready' || cameraState === 'starting' || cameraState === 'preview';
|
const isExpanded = cameraState === 'ready' || cameraState === 'starting' || cameraState === 'preview';
|
||||||
|
|
||||||
@@ -300,6 +305,16 @@ export default function UploadScreen() {
|
|||||||
[facingMode, mockPreviewEnabled]
|
[facingMode, mockPreviewEnabled]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleSwitchCamera = React.useCallback(async () => {
|
||||||
|
const nextMode = facingMode === 'user' ? 'environment' : 'user';
|
||||||
|
stopCamera();
|
||||||
|
await startCamera(nextMode);
|
||||||
|
}, [facingMode, startCamera, stopCamera]);
|
||||||
|
|
||||||
|
const handleToggleFlash = React.useCallback(() => {
|
||||||
|
setFlashPreferred((prev) => !prev);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleCapture = React.useCallback(async () => {
|
const handleCapture = React.useCallback(async () => {
|
||||||
const video = videoRef.current;
|
const video = videoRef.current;
|
||||||
if (!video) return;
|
if (!video) return;
|
||||||
@@ -485,17 +500,61 @@ export default function UploadScreen() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{cameraState === 'ready' && !previewUrl ? (
|
{cameraState === 'ready' && !previewUrl ? (
|
||||||
<Button
|
<XStack
|
||||||
size="$4"
|
|
||||||
circular
|
|
||||||
position="absolute"
|
position="absolute"
|
||||||
bottom="$4"
|
bottom="$4"
|
||||||
|
left="$4"
|
||||||
|
right="$4"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="$3"
|
||||||
|
circular
|
||||||
|
backgroundColor={flashPreferred ? '$primary' : mutedButton}
|
||||||
|
borderWidth={1}
|
||||||
|
borderColor={flashPreferred ? 'rgba(255,255,255,0.25)' : mutedButtonBorder}
|
||||||
|
onPress={handleToggleFlash}
|
||||||
|
disabled={facingMode !== 'environment'}
|
||||||
|
style={{ boxShadow: accessoryShadow }}
|
||||||
|
aria-label={t('upload.controls.toggleFlash', 'Toggle flash')}
|
||||||
|
>
|
||||||
|
{flashPreferred ? (
|
||||||
|
<Zap size={18} color="#FFFFFF" />
|
||||||
|
) : (
|
||||||
|
<ZapOff size={18} color={iconColor} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="$5"
|
||||||
|
circular
|
||||||
backgroundColor="$primary"
|
backgroundColor="$primary"
|
||||||
onPress={handleCapture}
|
onPress={handleCapture}
|
||||||
|
width={90}
|
||||||
|
height={90}
|
||||||
|
borderRadius={999}
|
||||||
|
shadowColor={isDark ? 'rgba(255, 79, 216, 0.5)' : 'rgba(15, 23, 42, 0.2)'}
|
||||||
|
shadowOpacity={0.5}
|
||||||
|
shadowRadius={22}
|
||||||
|
shadowOffset={{ width: 0, height: 10 }}
|
||||||
|
style={{ boxShadow: fabShadow }}
|
||||||
aria-label={t('upload.captureButton', 'Capture')}
|
aria-label={t('upload.captureButton', 'Capture')}
|
||||||
>
|
>
|
||||||
<Camera size={20} color="#FFFFFF" />
|
<Camera size={34} color="#FFFFFF" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="$3"
|
||||||
|
circular
|
||||||
|
backgroundColor={mutedButton}
|
||||||
|
borderWidth={1}
|
||||||
|
borderColor={mutedButtonBorder}
|
||||||
|
onPress={handleSwitchCamera}
|
||||||
|
style={{ boxShadow: accessoryShadow }}
|
||||||
|
aria-label={t('upload.controls.switchCamera', 'Switch camera')}
|
||||||
|
>
|
||||||
|
<RefreshCcw size={18} color={iconColor} />
|
||||||
|
</Button>
|
||||||
|
</XStack>
|
||||||
) : null}
|
) : null}
|
||||||
{(cameraState === 'ready' || cameraState === 'starting' || cameraState === 'preview') ? (
|
{(cameraState === 'ready' || cameraState === 'starting' || cameraState === 'preview') ? (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
Reference in New Issue
Block a user