feat: implement AI styling foundation and billing scope rework
This commit is contained in:
@@ -6,6 +6,7 @@ import { Camera, ChevronLeft, ChevronRight, Download, Heart, Loader2, Share2, Sp
|
||||
import AppShell from '../components/AppShell';
|
||||
import PhotoFrameTile from '../components/PhotoFrameTile';
|
||||
import ShareSheet from '../components/ShareSheet';
|
||||
import AiMagicEditSheet from '../components/AiMagicEditSheet';
|
||||
import { useEventData } from '../context/EventDataContext';
|
||||
import { createPhotoShareLink, deletePhoto, fetchGallery, fetchPhoto, likePhoto, unlikePhoto } from '../services/photosApi';
|
||||
import { usePollGalleryDelta } from '../hooks/usePollGalleryDelta';
|
||||
@@ -17,6 +18,7 @@ import { buildEventPath } from '../lib/routes';
|
||||
import { getBentoSurfaceTokens } from '../lib/bento';
|
||||
import { usePollStats } from '../hooks/usePollStats';
|
||||
import { pushGuestToast } from '../lib/toast';
|
||||
import { GUEST_AI_MAGIC_EDITS_ENABLED } from '../lib/featureFlags';
|
||||
|
||||
type GalleryFilter = 'latest' | 'popular' | 'mine' | 'photobooth';
|
||||
|
||||
@@ -111,6 +113,7 @@ export default function GalleryScreen() {
|
||||
url: null,
|
||||
loading: false,
|
||||
});
|
||||
const [aiMagicEditOpen, setAiMagicEditOpen] = React.useState(false);
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = React.useState(false);
|
||||
const [deleteBusy, setDeleteBusy] = React.useState(false);
|
||||
const [deleteConfirmMounted, setDeleteConfirmMounted] = React.useState(false);
|
||||
@@ -294,6 +297,7 @@ export default function GalleryScreen() {
|
||||
const lightboxSelected = lightboxIndex >= 0 ? displayPhotos[lightboxIndex] : null;
|
||||
const lightboxOpen = Boolean(selectedPhotoId);
|
||||
const canDelete = Boolean(lightboxPhoto && (lightboxPhoto.isMine || myPhotoIds.has(lightboxPhoto.id)));
|
||||
const hasAiStylingAccess = GUEST_AI_MAGIC_EDITS_ENABLED && Boolean(event?.capabilities?.ai_styling);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (filter === 'photobooth' && !photos.some((photo) => photo.ingestSource === 'photobooth')) {
|
||||
@@ -340,6 +344,7 @@ export default function GalleryScreen() {
|
||||
[searchParams, setSearchParams, token]
|
||||
);
|
||||
const closeLightbox = React.useCallback(() => {
|
||||
setAiMagicEditOpen(false);
|
||||
const next = new URLSearchParams(searchParams);
|
||||
next.delete('photo');
|
||||
setSearchParams(next, { replace: true });
|
||||
@@ -769,6 +774,14 @@ export default function GalleryScreen() {
|
||||
document.body.removeChild(link);
|
||||
}, []);
|
||||
|
||||
const openAiMagicEdit = React.useCallback(() => {
|
||||
if (!lightboxPhoto || !hasAiStylingAccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAiMagicEditOpen(true);
|
||||
}, [hasAiStylingAccess, lightboxPhoto]);
|
||||
|
||||
const handleTouchStart = (event: React.TouchEvent) => {
|
||||
touchStartX.current = event.touches[0]?.clientX ?? null;
|
||||
};
|
||||
@@ -1313,6 +1326,17 @@ export default function GalleryScreen() {
|
||||
<Download size={14} color={isDark ? '#F8FAFF' : '#0F172A'} />
|
||||
</Button>
|
||||
) : null}
|
||||
{lightboxPhoto && hasAiStylingAccess ? (
|
||||
<Button
|
||||
unstyled
|
||||
paddingHorizontal="$2"
|
||||
paddingVertical="$1.5"
|
||||
onPress={openAiMagicEdit}
|
||||
aria-label={t('galleryPage.lightbox.aiMagicEditAria', 'AI Magic Edit')}
|
||||
>
|
||||
<Sparkles size={14} color={isDark ? '#F8FAFF' : '#0F172A'} />
|
||||
</Button>
|
||||
) : null}
|
||||
{lightboxPhoto && canDelete ? (
|
||||
<Button
|
||||
unstyled
|
||||
@@ -1454,6 +1478,15 @@ export default function GalleryScreen() {
|
||||
onCopyLink={() => copyLink(shareSheet.url)}
|
||||
variant="inline"
|
||||
/>
|
||||
{hasAiStylingAccess ? (
|
||||
<AiMagicEditSheet
|
||||
open={aiMagicEditOpen}
|
||||
onOpenChange={setAiMagicEditOpen}
|
||||
eventToken={token ?? null}
|
||||
photoId={lightboxPhoto?.id ?? null}
|
||||
originalImageUrl={lightboxPhoto?.imageUrl ?? null}
|
||||
/>
|
||||
) : null}
|
||||
</YStack>
|
||||
</YStack>
|
||||
</YStack>
|
||||
|
||||
@@ -3,12 +3,13 @@ import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { YStack, XStack } from '@tamagui/stacks';
|
||||
import { SizableText as Text } from '@tamagui/text';
|
||||
import { Button } from '@tamagui/button';
|
||||
import { ArrowLeft, ChevronLeft, ChevronRight, Download, Heart, Share2 } from 'lucide-react';
|
||||
import { ArrowLeft, ChevronLeft, ChevronRight, Download, Heart, Share2, Sparkles } from 'lucide-react';
|
||||
import { useGesture } from '@use-gesture/react';
|
||||
import { animated, to, useSpring } from '@react-spring/web';
|
||||
import AppShell from '../components/AppShell';
|
||||
import SurfaceCard from '../components/SurfaceCard';
|
||||
import ShareSheet from '../components/ShareSheet';
|
||||
import AiMagicEditSheet from '../components/AiMagicEditSheet';
|
||||
import { useEventData } from '../context/EventDataContext';
|
||||
import { fetchGallery, fetchPhoto, likePhoto, createPhotoShareLink } from '../services/photosApi';
|
||||
import { useTranslation } from '@/shared/guest/i18n/useTranslation';
|
||||
@@ -16,6 +17,7 @@ import { useLocale } from '@/shared/guest/i18n/LocaleContext';
|
||||
import { useGuestThemeVariant } from '../lib/guestTheme';
|
||||
import { buildEventPath } from '../lib/routes';
|
||||
import { pushGuestToast } from '../lib/toast';
|
||||
import { GUEST_AI_MAGIC_EDITS_ENABLED } from '../lib/featureFlags';
|
||||
|
||||
type LightboxPhoto = {
|
||||
id: number;
|
||||
@@ -85,6 +87,7 @@ export default function PhotoLightboxScreen() {
|
||||
url: null,
|
||||
loading: false,
|
||||
});
|
||||
const [aiMagicEditOpen, setAiMagicEditOpen] = React.useState(false);
|
||||
const zoomContainerRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const zoomImageRef = React.useRef<HTMLImageElement | null>(null);
|
||||
const baseSizeRef = React.useRef({ width: 0, height: 0 });
|
||||
@@ -100,6 +103,7 @@ export default function PhotoLightboxScreen() {
|
||||
}));
|
||||
|
||||
const selected = selectedIndex !== null ? photos[selectedIndex] : null;
|
||||
const hasAiStylingAccess = GUEST_AI_MAGIC_EDITS_ENABLED && Boolean(event?.capabilities?.ai_styling);
|
||||
|
||||
const loadPage = React.useCallback(
|
||||
async (nextCursor?: string | null, replace = false) => {
|
||||
@@ -381,6 +385,14 @@ export default function PhotoLightboxScreen() {
|
||||
document.body.removeChild(link);
|
||||
}, []);
|
||||
|
||||
const openAiMagicEdit = React.useCallback(() => {
|
||||
if (!selected || !hasAiStylingAccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAiMagicEditOpen(true);
|
||||
}, [hasAiStylingAccess, selected]);
|
||||
|
||||
const bind = useGesture(
|
||||
{
|
||||
onDrag: ({ down, movement: [mx, my], offset: [ox, oy], last, event }) => {
|
||||
@@ -642,6 +654,22 @@ export default function PhotoLightboxScreen() {
|
||||
</Text>
|
||||
</XStack>
|
||||
</Button>
|
||||
{hasAiStylingAccess ? (
|
||||
<Button
|
||||
unstyled
|
||||
onPress={openAiMagicEdit}
|
||||
paddingHorizontal="$3"
|
||||
paddingVertical="$2"
|
||||
aria-label={t('galleryPage.lightbox.aiMagicEditAria', 'AI Magic Edit')}
|
||||
>
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Sparkles size={16} color={isDark ? '#F8FAFF' : '#0F172A'} />
|
||||
<Text fontSize="$2" fontWeight="$6">
|
||||
{t('galleryPage.lightbox.aiMagicEdit', 'AI Magic Edit')}
|
||||
</Text>
|
||||
</XStack>
|
||||
</Button>
|
||||
) : null}
|
||||
</XStack>
|
||||
</XStack>
|
||||
</YStack>
|
||||
@@ -667,6 +695,15 @@ export default function PhotoLightboxScreen() {
|
||||
onCopyLink={() => copyLink(shareSheet.url)}
|
||||
variant="inline"
|
||||
/>
|
||||
{hasAiStylingAccess ? (
|
||||
<AiMagicEditSheet
|
||||
open={aiMagicEditOpen}
|
||||
onOpenChange={setAiMagicEditOpen}
|
||||
eventToken={token ?? null}
|
||||
photoId={selected?.id ?? null}
|
||||
originalImageUrl={selected?.imageUrl ?? null}
|
||||
/>
|
||||
) : null}
|
||||
</SurfaceCard>
|
||||
</YStack>
|
||||
</AppShell>
|
||||
|
||||
Reference in New Issue
Block a user