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 (