diff --git a/.changeset/1769627060-post-export-sheet.md b/.changeset/1769627060-post-export-sheet.md new file mode 100644 index 00000000..52fa3130 --- /dev/null +++ b/.changeset/1769627060-post-export-sheet.md @@ -0,0 +1,5 @@ +--- +"dopeshot-app": minor +--- + +Add post-export success modal for anonymous users to encourage signup. Features thumbnail preview, signup CTA, and direct contact options. Logged-in users see no interruption. diff --git a/apps/app/package.json b/apps/app/package.json index f9ff5f76..5af2d3e5 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -38,6 +38,7 @@ "clsx": "^2.1.1", "culori": "^4.0.2", "flags": "^4.0.2", + "framer-motion": "^11.11.17", "geist": "^1.5.1", "html-to-image": "^1.11.13", "jotai": "^2.15.2", diff --git a/apps/app/public/me.jpg b/apps/app/public/me.jpg new file mode 100644 index 00000000..73045eab Binary files /dev/null and b/apps/app/public/me.jpg differ diff --git a/apps/app/src/app/(playground)/_components/playground-page.tsx b/apps/app/src/app/(playground)/_components/playground-page.tsx index 892e7dbf..d75f5f24 100644 --- a/apps/app/src/app/(playground)/_components/playground-page.tsx +++ b/apps/app/src/app/(playground)/_components/playground-page.tsx @@ -12,7 +12,10 @@ import { SidebarTabs } from "@/components/layout/sidebar-tabs"; import { useBrandLogoAutoApply } from "@/hooks/use-brand-logo-auto-apply"; import { usePlaygroundController } from "@/hooks/use-playground-controller"; import { useUserTier } from "@/hooks/use-user-tier"; -import { EXPORT_ORIENTATION_DIMENSIONS, ORIENTATION_DIMENSIONS } from "@/domain/layout/screenshot-mode"; +import { + EXPORT_ORIENTATION_DIMENSIONS, + ORIENTATION_DIMENSIONS, +} from "@/domain/layout/screenshot-mode"; import { assetsAtom, baseConfigAtom, @@ -42,59 +45,67 @@ import { SidebarErrorBoundary } from "@/components/errors/sidebar-error-boundary import { MemoryErrorBoundary } from "@/components/errors/memory-error-boundary"; import { InAppUpdateBanner } from "@/components/layout/in-app-update-banner"; import { useOnboardingStatus } from "@/hooks/use-onboarding-status"; -import { OnboardingModal, type BrandProfilePayload } from "@/components/onboarding/onboarding-modal"; +import { + OnboardingModal, + type BrandProfilePayload, +} from "@/components/onboarding/onboarding-modal"; import { listPersonalBackgrounds } from "@/domain/backgrounds/background-service"; +import { ExportSuccessModal } from "@/components/post-export"; +import { showExportSheetAtom, exportThumbnailAtom } from "@/hooks/atoms"; +import { useRouter } from "next/navigation"; +import { track } from "@/lib/analytics"; const FeedbackModal = dynamic( - () => import("@/components/feedback/feedback-modal").then(mod => ({ default: mod.FeedbackModal })), - { ssr: false } + () => + import("@/components/feedback/feedback-modal").then((mod) => ({ default: mod.FeedbackModal })), + { ssr: false }, ); -function ExportContainer({ - width, - height, - orientation -}: { - width: number; +function ExportContainer({ + width, + height, + orientation, +}: { + width: number; height: number; orientation: Orientation; }) { // Use preview dimensions as base (matching what user sees) const baseDims = ORIENTATION_DIMENSIONS[orientation]; const scale = width / baseDims.width; - + return ( +
-
- -
+
+
); } @@ -132,12 +143,18 @@ function PlaygroundPageInner({ initialIsAuthenticated, initialOnboardingOpen, }: PlaygroundPageProps) { + const router = useRouter(); const orientation = useAtomValue(orientationAtom); const config = useAtomValue(configAtom); const personalBackgrounds = useAtomValue(personalBackgroundsAtom); const [feedbackModalOpen, setFeedbackModalOpen] = useAtom(feedbackModalOpenAtom); const [feedbackScreenshot, setFeedbackScreenshot] = useState(null); + // Export success sheet state + const [showExportSheet, setShowExportSheet] = useAtom(showExportSheetAtom); + const exportThumbnail = useAtomValue(exportThumbnailAtom); + const setExportThumbnail = useSetAtom(exportThumbnailAtom); + const { isBrandUser } = useUserTier(); // Auto-apply brand logo for NEW designs (not loaded from memory) @@ -150,6 +167,31 @@ function PlaygroundPageInner({ setFeedbackScreenshot(screenshot); setFeedbackModalOpen(true); }; + + // Handle export sheet actions + const handleExportSheetClose = useCallback(() => { + setShowExportSheet(false); + setExportThumbnail(null); + track("export_sheet_dismissed", { + dismiss_method: "button", + }); + }, [setShowExportSheet, setExportThumbnail]); + + const handleExportSheetSignup = useCallback(() => { + track("export_sheet_signup_clicked"); + setShowExportSheet(false); + setExportThumbnail(null); + router.push("/auth"); + }, [router, setShowExportSheet, setExportThumbnail]); + + const handleExportSheetFeedback = useCallback(() => { + track("export_sheet_feedback_clicked"); + setShowExportSheet(false); + setExportThumbnail(null); + setFeedbackScreenshot(null); // Clear stale screenshot to prevent re-sending old canvas + setFeedbackModalOpen(true); + }, [setShowExportSheet, setExportThumbnail, setFeedbackModalOpen]); + // Memory hook for loading items const { loadMemoryItem, @@ -201,7 +243,12 @@ function PlaygroundPageInner({ if (!shouldOpenOnboarding) return; if (hasLoadedOnboardingProfile || isOnboardingProfileLoading) return; void loadOnboardingProfile(); - }, [hasLoadedOnboardingProfile, isOnboardingProfileLoading, loadOnboardingProfile, shouldOpenOnboarding]); + }, [ + hasLoadedOnboardingProfile, + isOnboardingProfileLoading, + loadOnboardingProfile, + shouldOpenOnboarding, + ]); useEffect(() => { if (!shouldOpenOnboarding || !hasLoadedOnboardingProfile) return; @@ -271,7 +318,9 @@ function PlaygroundPageInner({ if (typeof window !== "undefined") { const hasIdleCallback = "requestIdleCallback" in window; if (hasIdleCallback) { - prefetchBackgroundsHandleRef.current = window.requestIdleCallback(prefetch, { timeout: 3000 }); + prefetchBackgroundsHandleRef.current = window.requestIdleCallback(prefetch, { + timeout: 3000, + }); } else { // In browser context, setTimeout returns a number (not Node's Timeout object) prefetchBackgroundsHandleRef.current = Number(window.setTimeout(prefetch, 1200)); @@ -330,10 +379,7 @@ function PlaygroundPageInner({ } = usePlaygroundController({ demoEnabled: !isLoggedIn }); // Only show loading when actually loading a specific memory item from URL - const showLoadingState = - isLoggedIn && - Boolean(initialMemoryItemId) && - !loadedItemId; + const showLoadingState = isLoggedIn && Boolean(initialMemoryItemId) && !loadedItemId; const showEmptyState = useMemo(() => { const hasText = Boolean(config.text.title?.trim() || config.text.subtitle?.trim()); @@ -453,10 +499,7 @@ function PlaygroundPageInner({ {/* Right: Sidebar - spans full height from below nav */}
- +
@@ -502,6 +545,14 @@ function PlaygroundPageInner({ onOpenChange={setFeedbackModalOpen} screenshotDataUrl={feedbackScreenshot} /> + + ); } diff --git a/apps/app/src/components/feedback/feedback-modal.tsx b/apps/app/src/components/feedback/feedback-modal.tsx index 0faadc5c..31d5e976 100644 --- a/apps/app/src/components/feedback/feedback-modal.tsx +++ b/apps/app/src/components/feedback/feedback-modal.tsx @@ -1,21 +1,14 @@ "use client"; import { useState, useEffect } from "react"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, -} from "@/components/ui/dialog"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { track } from "@/lib/analytics"; -import { MessageSquare, X, Check } from "lucide-react"; -import { cn } from "@/lib/utils/cn"; -import { Switch } from "@/components/ui/switch"; +import { Calendar } from "lucide-react"; +import { AdrianAvatar } from "@/components/ui/adrian-avatar"; interface FeedbackModalProps { open: boolean; @@ -23,18 +16,11 @@ interface FeedbackModalProps { screenshotDataUrl?: string | null; } -export function FeedbackModal({ - open, - onOpenChange, - screenshotDataUrl, -}: FeedbackModalProps) { +export function FeedbackModal({ open, onOpenChange, screenshotDataUrl }: FeedbackModalProps) { const [feedback, setFeedback] = useState(""); const [email, setEmail] = useState(""); - const [includeScreenshot, setIncludeScreenshot] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); - const [submitStatus, setSubmitStatus] = useState< - "idle" | "success" | "error" - >("idle"); + const [submitStatus, setSubmitStatus] = useState<"idle" | "success" | "error">("idle"); const [errorMessage, setErrorMessage] = useState(null); // Reset state when modal opens @@ -42,12 +28,11 @@ export function FeedbackModal({ if (open) { setFeedback(""); setEmail(""); - setIncludeScreenshot(!!screenshotDataUrl); setSubmitStatus("idle"); setErrorMessage(null); track("feedback_modal_opened"); } - }, [open, screenshotDataUrl]); + }, [open]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -71,7 +56,7 @@ export function FeedbackModal({ body: JSON.stringify({ message: feedback, email: email.trim() || undefined, - screenshot: includeScreenshot ? screenshotDataUrl : undefined, + screenshot: screenshotDataUrl || undefined, }), }); @@ -83,7 +68,7 @@ export function FeedbackModal({ track("feedback_submitted", { hasEmail: !!email.trim(), - hasScreenshot: includeScreenshot && !!screenshotDataUrl, + hasScreenshot: !!screenshotDataUrl, messageLength: feedback.length, }); @@ -97,9 +82,7 @@ export function FeedbackModal({ console.error("Feedback submission failed:", error); setSubmitStatus("error"); setErrorMessage( - error instanceof Error - ? error.message - : "Failed to submit feedback. Please try again." + error instanceof Error ? error.message : "Failed to submit feedback. Please try again.", ); } finally { setIsSubmitting(false); @@ -111,127 +94,104 @@ export function FeedbackModal({ return ( - + {submitStatus === "success" ? (
-
- -
-

- Thank you for your feedback! -

-

- We'll review it shortly +

Thank you!

+

+ I'll review your feedback shortly

) : ( <> -
-
- -
-
- - Share Your Feedback - - - Help us improve dopeshot for you - + Share your feedback
-
- {/* Feedback textarea */} -
- -