Skip to content

Commit 96f4d6e

Browse files
bidahclaude
andcommitted
📱 feat(ui-prompts): mobile-optimized prompt detail layout
Move title and description above video/gallery on mobile web. Show video as first carousel item on mobile only, preserving the side-by-side video + gallery layout on desktop. Also add screen_references/ to gitignore. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d8379b5 commit 96f4d6e

4 files changed

Lines changed: 86 additions & 24 deletions

File tree

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,6 @@ remotion/
6464

6565
# Git worktrees
6666
.worktrees/
67+
68+
# Screen references
69+
screen_references/

‎apps/web/app/(app)/ui-prompts/[slug]/page.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default async function UiPromptDetailPage({ params }: Props) {
1212
return (
1313
<main className="min-h-dvh bg-background">
1414
<NavHeader session={session} />
15-
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
15+
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-2 lg:py-12">
1616
<PromptDetail slug={slug} isAuthenticated={!!session} />
1717
</div>
1818
</main>

‎apps/web/components/ui-prompts/prompt-detail.tsx‎

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,26 @@ export function PromptDetail({ slug, isAuthenticated }: PromptDetailProps) {
6868

6969
return (
7070
<div>
71-
71+
{/* Mobile: Title + Description on top */}
72+
<div className="lg:hidden mb-6 space-y-3">
73+
<div className="flex items-start gap-1">
74+
<h1 className="text-2xl font-bold text-foreground">{prompt.title}</h1>
75+
{prompt.featured && (
76+
<Badge className="bg-foreground text-background border-foreground/80 hover:bg-foreground flex-shrink-0">
77+
Featured
78+
</Badge>
79+
)}
80+
</div>
81+
<p className="text-muted-foreground">{prompt.description}</p>
82+
</div>
7283

7384
{/* Side-by-side Layout */}
7485
<div className="flex flex-col lg:flex-row gap-8 lg:gap-12">
7586
{/* Left: Video + Gallery */}
7687
<div className="lg:w-[60%] flex flex-col sm:flex-row items-start gap-3">
88+
{/* Desktop: video shown side-by-side with gallery */}
7789
{prompt.videoPreviewUrl && (
78-
<div className="flex-1 min-w-0 flex justify-center">
90+
<div className="hidden sm:flex flex-1 min-w-0 justify-center">
7991
<video
8092
src={prompt.videoPreviewUrl}
8193
autoPlay
@@ -92,14 +104,15 @@ export function PromptDetail({ slug, isAuthenticated }: PromptDetailProps) {
92104
<ScreenshotGallery
93105
screenshots={screenshots}
94106
title={prompt.title}
107+
videoUrl={prompt.videoPreviewUrl}
95108
/>
96109
</div>
97110
</div>
98111

99112
{/* Right: Details */}
100113
<div className="lg:w-[40%] space-y-3">
101-
{/* Title + Featured Badge */}
102-
<div className="flex items-start gap-1">
114+
{/* Title + Featured Badge (desktop only) */}
115+
<div className="hidden lg:flex items-start gap-1">
103116
<h1 className="text-2xl font-bold text-foreground">{prompt.title}</h1>
104117
{prompt.featured && (
105118
<Badge className="bg-foreground text-background border-foreground/80 hover:bg-foreground flex-shrink-0">
@@ -108,8 +121,8 @@ export function PromptDetail({ slug, isAuthenticated }: PromptDetailProps) {
108121
)}
109122
</div>
110123

111-
{/* Description */}
112-
<p className="text-muted-foreground">{prompt.description}</p>
124+
{/* Description (desktop only) */}
125+
<p className="hidden lg:block text-muted-foreground">{prompt.description}</p>
113126

114127
{/* Tags */}
115128
{prompt.tags && prompt.tags.length > 0 && (

‎apps/web/components/ui-prompts/screenshot-gallery.tsx‎

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,61 +4,107 @@ import { useState } from "react"
44
import { ChevronLeft, ChevronRight } from "lucide-react"
55
import { PhoneFrame } from "@/components/ui-prompts/phone-frame"
66
import { Button } from "@/components/ui/button"
7+
import { useIsMobile } from "@/hooks/use-mobile"
78

89
interface ScreenshotGalleryProps {
910
screenshots: string[]
1011
title: string
12+
/** On mobile, the video is shown as the first carousel item */
13+
videoUrl?: string | null
1114
}
1215

1316
export function ScreenshotGallery({
1417
screenshots,
1518
title,
19+
videoUrl,
1620
}: ScreenshotGalleryProps) {
1721
const [activeIndex, setActiveIndex] = useState(0)
22+
const isMobile = useIsMobile()
23+
24+
// Only include video in carousel on mobile
25+
const includeVideo = isMobile && !!videoUrl
26+
const totalItems = includeVideo ? screenshots.length + 1 : screenshots.length
27+
const isVideoSlot = includeVideo && activeIndex === 0
28+
const screenshotIndex = includeVideo ? activeIndex - 1 : activeIndex
1829

1930
return (
2031
<div className="flex flex-col items-center gap-4">
21-
{/* Main Phone Frame */}
22-
<PhoneFrame
23-
src={screenshots[activeIndex]}
24-
alt={`${title} - Screenshot ${activeIndex + 1}`}
25-
className="max-w-[280px] w-full"
26-
/>
32+
{/* Main display */}
33+
{isVideoSlot ? (
34+
<div className="max-w-[280px] w-full flex justify-center">
35+
<video
36+
src={videoUrl!}
37+
autoPlay
38+
loop
39+
muted
40+
playsInline
41+
controls
42+
className="max-w-[280px] w-full rounded-[2rem] border-[3px] border-neutral-700 bg-black object-cover"
43+
style={{ aspectRatio: "9 / 19.5" }}
44+
/>
45+
</div>
46+
) : (
47+
<PhoneFrame
48+
src={screenshots[includeVideo ? screenshotIndex : activeIndex]}
49+
alt={`${title} - Screenshot ${(includeVideo ? screenshotIndex : activeIndex) + 1}`}
50+
className="max-w-[280px] w-full"
51+
/>
52+
)}
2753

2854
{/* Thumbnail Strip */}
29-
{screenshots.length > 1 && (
55+
{totalItems > 1 && (
3056
<div className="flex items-center gap-2">
3157
<Button
3258
variant="ghost"
3359
size="icon"
3460
className="h-8 w-8 text-muted-foreground hover:text-foreground"
3561
onClick={() =>
3662
setActiveIndex((prev) =>
37-
prev === 0 ? screenshots.length - 1 : prev - 1
63+
prev === 0 ? totalItems - 1 : prev - 1
3864
)
3965
}
4066
>
4167
<ChevronLeft className="h-4 w-4" />
4268
</Button>
4369

4470
<div className="flex items-center gap-2 overflow-x-auto">
45-
{screenshots.map((src, index) => (
71+
{/* Mobile-only video thumbnail */}
72+
{includeVideo && (
4673
<button
47-
key={index}
48-
onClick={() => setActiveIndex(index)}
74+
onClick={() => setActiveIndex(0)}
4975
className={`flex-shrink-0 w-12 h-24 rounded-lg overflow-hidden border-2 transition-colors ${
50-
index === activeIndex
76+
activeIndex === 0
5177
? "border-foreground"
5278
: "border-border hover:border-primary/50"
5379
}`}
5480
>
55-
<img
56-
src={src}
57-
alt={`${title} - Thumbnail ${index + 1}`}
81+
<video
82+
src={videoUrl!}
83+
muted
5884
className="w-full h-full object-cover"
5985
/>
6086
</button>
61-
))}
87+
)}
88+
{screenshots.map((src, index) => {
89+
const itemIndex = includeVideo ? index + 1 : index
90+
return (
91+
<button
92+
key={index}
93+
onClick={() => setActiveIndex(itemIndex)}
94+
className={`flex-shrink-0 w-12 h-24 rounded-lg overflow-hidden border-2 transition-colors ${
95+
itemIndex === activeIndex
96+
? "border-foreground"
97+
: "border-border hover:border-primary/50"
98+
}`}
99+
>
100+
<img
101+
src={src}
102+
alt={`${title} - Thumbnail ${index + 1}`}
103+
className="w-full h-full object-cover"
104+
/>
105+
</button>
106+
)
107+
})}
62108
</div>
63109

64110
<Button
@@ -67,7 +113,7 @@ export function ScreenshotGallery({
67113
className="h-8 w-8 text-muted-foreground hover:text-foreground"
68114
onClick={() =>
69115
setActiveIndex((prev) =>
70-
prev === screenshots.length - 1 ? 0 : prev + 1
116+
prev === totalItems - 1 ? 0 : prev + 1
71117
)
72118
}
73119
>

0 commit comments

Comments
 (0)