Skip to content

Commit df888a3

Browse files
authored
Merge pull request #127 from peelar/feature/post-export-modal
2 parents 226567c + e0874a2 commit df888a3

17 files changed

Lines changed: 712 additions & 347 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"dopeshot-app": minor
3+
---
4+
5+
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.

apps/app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"clsx": "^2.1.1",
3939
"culori": "^4.0.2",
4040
"flags": "^4.0.2",
41+
"framer-motion": "^11.11.17",
4142
"geist": "^1.5.1",
4243
"html-to-image": "^1.11.13",
4344
"jotai": "^2.15.2",

apps/app/public/me.jpg

853 KB
Loading

apps/app/src/app/(playground)/_components/playground-page.tsx

Lines changed: 98 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import { SidebarTabs } from "@/components/layout/sidebar-tabs";
1212
import { useBrandLogoAutoApply } from "@/hooks/use-brand-logo-auto-apply";
1313
import { usePlaygroundController } from "@/hooks/use-playground-controller";
1414
import { useUserTier } from "@/hooks/use-user-tier";
15-
import { EXPORT_ORIENTATION_DIMENSIONS, ORIENTATION_DIMENSIONS } from "@/domain/layout/screenshot-mode";
15+
import {
16+
EXPORT_ORIENTATION_DIMENSIONS,
17+
ORIENTATION_DIMENSIONS,
18+
} from "@/domain/layout/screenshot-mode";
1619
import {
1720
assetsAtom,
1821
baseConfigAtom,
@@ -42,59 +45,67 @@ import { SidebarErrorBoundary } from "@/components/errors/sidebar-error-boundary
4245
import { MemoryErrorBoundary } from "@/components/errors/memory-error-boundary";
4346
import { InAppUpdateBanner } from "@/components/layout/in-app-update-banner";
4447
import { useOnboardingStatus } from "@/hooks/use-onboarding-status";
45-
import { OnboardingModal, type BrandProfilePayload } from "@/components/onboarding/onboarding-modal";
48+
import {
49+
OnboardingModal,
50+
type BrandProfilePayload,
51+
} from "@/components/onboarding/onboarding-modal";
4652
import { listPersonalBackgrounds } from "@/domain/backgrounds/background-service";
53+
import { ExportSuccessModal } from "@/components/post-export";
54+
import { showExportSheetAtom, exportThumbnailAtom } from "@/hooks/atoms";
55+
import { useRouter } from "next/navigation";
56+
import { track } from "@/lib/analytics";
4757

4858
const FeedbackModal = dynamic(
49-
() => import("@/components/feedback/feedback-modal").then(mod => ({ default: mod.FeedbackModal })),
50-
{ ssr: false }
59+
() =>
60+
import("@/components/feedback/feedback-modal").then((mod) => ({ default: mod.FeedbackModal })),
61+
{ ssr: false },
5162
);
5263

53-
function ExportContainer({
54-
width,
55-
height,
56-
orientation
57-
}: {
58-
width: number;
64+
function ExportContainer({
65+
width,
66+
height,
67+
orientation,
68+
}: {
69+
width: number;
5970
height: number;
6071
orientation: Orientation;
6172
}) {
6273
// Use preview dimensions as base (matching what user sees)
6374
const baseDims = ORIENTATION_DIMENSIONS[orientation];
6475
const scale = width / baseDims.width;
65-
76+
6677
return (
78+
<div
79+
id="export-container"
80+
style={{
81+
position: "fixed",
82+
top: 0,
83+
left: 0,
84+
width: `${width}px`,
85+
height: `${height}px`,
86+
zIndex: -100,
87+
overflow: "hidden",
88+
visibility: "visible",
89+
background: "white",
90+
WebkitFontSmoothing: "antialiased",
91+
MozOsxFontSmoothing: "grayscale",
92+
textRendering: "optimizeLegibility",
93+
display: "flex",
94+
alignItems: "center",
95+
justifyContent: "center",
96+
}}
97+
>
6798
<div
68-
id="export-container"
6999
style={{
70-
position: "fixed",
71-
top: 0,
72-
left: 0,
73-
width: `${width}px`,
74-
height: `${height}px`,
75-
zIndex: -100,
76-
overflow: "hidden",
77-
visibility: "visible",
78-
background: "white",
79-
WebkitFontSmoothing: "antialiased",
80-
MozOsxFontSmoothing: "grayscale",
81-
textRendering: "optimizeLegibility",
82-
display: "flex",
83-
alignItems: "center",
84-
justifyContent: "center",
100+
width: `${baseDims.width}px`,
101+
height: `${baseDims.height}px`,
102+
transformOrigin: "center",
103+
transform: `scale(${scale})`,
85104
}}
86105
>
87-
<div
88-
style={{
89-
width: `${baseDims.width}px`,
90-
height: `${baseDims.height}px`,
91-
transformOrigin: "center",
92-
transform: `scale(${scale})`,
93-
}}
94-
>
95-
<CoverPreview isStatic />
96-
</div>
106+
<CoverPreview isStatic />
97107
</div>
108+
</div>
98109
);
99110
}
100111

@@ -132,12 +143,18 @@ function PlaygroundPageInner({
132143
initialIsAuthenticated,
133144
initialOnboardingOpen,
134145
}: PlaygroundPageProps) {
146+
const router = useRouter();
135147
const orientation = useAtomValue(orientationAtom);
136148
const config = useAtomValue(configAtom);
137149
const personalBackgrounds = useAtomValue(personalBackgroundsAtom);
138150
const [feedbackModalOpen, setFeedbackModalOpen] = useAtom(feedbackModalOpenAtom);
139151
const [feedbackScreenshot, setFeedbackScreenshot] = useState<string | null>(null);
140152

153+
// Export success sheet state
154+
const [showExportSheet, setShowExportSheet] = useAtom(showExportSheetAtom);
155+
const exportThumbnail = useAtomValue(exportThumbnailAtom);
156+
const setExportThumbnail = useSetAtom(exportThumbnailAtom);
157+
141158
const { isBrandUser } = useUserTier();
142159

143160
// Auto-apply brand logo for NEW designs (not loaded from memory)
@@ -150,6 +167,31 @@ function PlaygroundPageInner({
150167
setFeedbackScreenshot(screenshot);
151168
setFeedbackModalOpen(true);
152169
};
170+
171+
// Handle export sheet actions
172+
const handleExportSheetClose = useCallback(() => {
173+
setShowExportSheet(false);
174+
setExportThumbnail(null);
175+
track("export_sheet_dismissed", {
176+
dismiss_method: "button",
177+
});
178+
}, [setShowExportSheet, setExportThumbnail]);
179+
180+
const handleExportSheetSignup = useCallback(() => {
181+
track("export_sheet_signup_clicked");
182+
setShowExportSheet(false);
183+
setExportThumbnail(null);
184+
router.push("/auth");
185+
}, [router, setShowExportSheet, setExportThumbnail]);
186+
187+
const handleExportSheetFeedback = useCallback(() => {
188+
track("export_sheet_feedback_clicked");
189+
setShowExportSheet(false);
190+
setExportThumbnail(null);
191+
setFeedbackScreenshot(null); // Clear stale screenshot to prevent re-sending old canvas
192+
setFeedbackModalOpen(true);
193+
}, [setShowExportSheet, setExportThumbnail, setFeedbackModalOpen]);
194+
153195
// Memory hook for loading items
154196
const {
155197
loadMemoryItem,
@@ -201,7 +243,12 @@ function PlaygroundPageInner({
201243
if (!shouldOpenOnboarding) return;
202244
if (hasLoadedOnboardingProfile || isOnboardingProfileLoading) return;
203245
void loadOnboardingProfile();
204-
}, [hasLoadedOnboardingProfile, isOnboardingProfileLoading, loadOnboardingProfile, shouldOpenOnboarding]);
246+
}, [
247+
hasLoadedOnboardingProfile,
248+
isOnboardingProfileLoading,
249+
loadOnboardingProfile,
250+
shouldOpenOnboarding,
251+
]);
205252

206253
useEffect(() => {
207254
if (!shouldOpenOnboarding || !hasLoadedOnboardingProfile) return;
@@ -271,7 +318,9 @@ function PlaygroundPageInner({
271318
if (typeof window !== "undefined") {
272319
const hasIdleCallback = "requestIdleCallback" in window;
273320
if (hasIdleCallback) {
274-
prefetchBackgroundsHandleRef.current = window.requestIdleCallback(prefetch, { timeout: 3000 });
321+
prefetchBackgroundsHandleRef.current = window.requestIdleCallback(prefetch, {
322+
timeout: 3000,
323+
});
275324
} else {
276325
// In browser context, setTimeout returns a number (not Node's Timeout object)
277326
prefetchBackgroundsHandleRef.current = Number(window.setTimeout(prefetch, 1200));
@@ -330,10 +379,7 @@ function PlaygroundPageInner({
330379
} = usePlaygroundController({ demoEnabled: !isLoggedIn });
331380

332381
// Only show loading when actually loading a specific memory item from URL
333-
const showLoadingState =
334-
isLoggedIn &&
335-
Boolean(initialMemoryItemId) &&
336-
!loadedItemId;
382+
const showLoadingState = isLoggedIn && Boolean(initialMemoryItemId) && !loadedItemId;
337383

338384
const showEmptyState = useMemo(() => {
339385
const hasText = Boolean(config.text.title?.trim() || config.text.subtitle?.trim());
@@ -453,10 +499,7 @@ function PlaygroundPageInner({
453499
{/* Right: Sidebar - spans full height from below nav */}
454500
<SidebarErrorBoundary>
455501
<div className="hidden h-full min-h-0 w-80 overflow-hidden border-l border-border bg-background sm:flex sm:flex-col">
456-
<SidebarTabs
457-
onUploadAsset={handleFileProcess}
458-
onFeedbackClick={handleFeedbackClick}
459-
/>
502+
<SidebarTabs onUploadAsset={handleFileProcess} onFeedbackClick={handleFeedbackClick} />
460503
</div>
461504
</SidebarErrorBoundary>
462505
</div>
@@ -502,6 +545,14 @@ function PlaygroundPageInner({
502545
onOpenChange={setFeedbackModalOpen}
503546
screenshotDataUrl={feedbackScreenshot}
504547
/>
548+
549+
<ExportSuccessModal
550+
isOpen={showExportSheet}
551+
onClose={handleExportSheetClose}
552+
onSignup={handleExportSheetSignup}
553+
onFeedback={handleExportSheetFeedback}
554+
thumbnailUrl={exportThumbnail ?? undefined}
555+
/>
505556
</main>
506557
);
507558
}

0 commit comments

Comments
 (0)