Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
787 changes: 787 additions & 0 deletions docs/prompt-gallery-roadmap.md

Large diffs are not rendered by default.

1,078 changes: 1,056 additions & 22 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"start": "node scripts/sync-webpack-chunks.js && next start",
"lint": "next lint",
"test": "playwright test",
"test:unit": "vitest run",
"test:ui": "playwright test --ui",
"test:headed": "playwright test --headed"
},
Expand All @@ -18,6 +19,7 @@
"@radix-ui/react-dialog": "^1.1.11",
"@radix-ui/react-navigation-menu": "^1.2.10",
"@radix-ui/react-slot": "^1.2.0",
"@sindresorhus/slugify": "^3.0.0",
"@supabase/ssr": "^0.5.0",
"@supabase/supabase-js": "^2.48.0",
"@vercel/analytics": "^1.5.0",
Expand Down Expand Up @@ -54,6 +56,7 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/sanitize-html": "^2.16.0",
"@vitest/ui": "^3.2.4",
"autoprefixer": "^10.4.21",
"dotenv": "^17.2.3",
"eslint": "^9",
Expand All @@ -62,6 +65,7 @@
"tailwindcss": "^4.1.4",
"ts-node": "^10.9.2",
"tsx": "^4.19.2",
"typescript": "^5"
"typescript": "^5",
"vitest": "^3.2.4"
}
}
67 changes: 67 additions & 0 deletions src/app/api/prompts/[slug]/comments/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { NextResponse } from 'next/server'
import { createServerComponentClient, createServiceRoleClient } from '@/lib/supabase/server-client'

export const dynamic = 'force-dynamic'

export async function POST(request: Request, { params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const { content } = (await request.json().catch(() => ({}))) as { content?: string }
const trimmedContent = content?.trim() ?? ''

if (trimmedContent.length < 10) {
return NextResponse.json({ error: 'Comments should be at least 10 characters long.' }, { status: 422 })
}

const authClient = createServerComponentClient()
const {
data: { user },
} = await authClient.auth.getUser()

if (!user) {
return NextResponse.json({ error: 'You need to sign in to comment.' }, { status: 401 })
}

const serviceClient = createServiceRoleClient()

const { data: profile, error: profileError } = await serviceClient
.from('profiles')
.select('id')
.eq('user_id', user.id)
.maybeSingle()

if (profileError) {
return NextResponse.json({ error: profileError.message }, { status: 500 })
}

if (!profile) {
return NextResponse.json({ error: 'Profile not found for user.' }, { status: 404 })
}

const { data: prompt, error: promptError } = await serviceClient
.from('prompts')
.select('id, moderation_status')
.eq('slug', slug)
.maybeSingle()

if (promptError) {
return NextResponse.json({ error: promptError.message }, { status: 500 })
}

if (!prompt || prompt.moderation_status !== 'approved') {
return NextResponse.json({ error: 'Prompt not found or not published yet.' }, { status: 404 })
}

const { error: insertError } = await serviceClient.from('prompt_comments').insert({
prompt_id: prompt.id,
user_id: profile.id,
content: trimmedContent,
markdown_content: trimmedContent,
})

if (insertError) {
return NextResponse.json({ error: insertError.message }, { status: 500 })
}

return NextResponse.json({ message: 'Thanks! Your comment is pending moderation.' }, { status: 201 })
}

57 changes: 57 additions & 0 deletions src/app/api/prompts/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { NextResponse } from 'next/server'
import { createServerComponentClient, createServiceRoleClient } from '@/lib/supabase/server-client'
import { createPrompt } from '@/lib/prompt-gallery/queries'
import { createPromptSchema } from '@/lib/prompt-gallery/validation'

export async function POST(request: Request) {
const payload = await request.json().catch(() => null)

if (!payload) {
return NextResponse.json({ error: 'Invalid request body.' }, { status: 400 })
}

const parsed = createPromptSchema.safeParse(payload)

if (!parsed.success) {
return NextResponse.json(
{
error: 'Validation failed.',
details: parsed.error.flatten(),
},
{ status: 422 },
)
}

const authClient = createServerComponentClient()
const {
data: { user },
} = await authClient.auth.getUser()

if (!user) {
return NextResponse.json({ error: 'You must be signed in to upload prompts.' }, { status: 401 })
}

const serviceClient = createServiceRoleClient()
const { data: profile, error: profileError } = await serviceClient
.from('profiles')
.select('id')
.eq('user_id', user.id)
.maybeSingle()

if (profileError) {
return NextResponse.json({ error: profileError.message }, { status: 500 })
}

if (!profile) {
return NextResponse.json({ error: 'Profile not found for user.' }, { status: 404 })
}

try {
const promptId = await createPrompt(parsed.data, profile.id)
return NextResponse.json({ id: promptId }, { status: 201 })
} catch (error) {
console.error('Unable to create prompt', error)
return NextResponse.json({ error: 'Unable to create prompt.' }, { status: 500 })
}
}

7 changes: 7 additions & 0 deletions src/app/resources/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ const resourceCollections = [
linkLabel: 'Choose a path',
href: '/tutorials',
},
{
title: 'Prompt gallery',
description: 'Explore community prompts with advanced filters, media previews, and copy-ready actions for every model.',
icon: Library,
linkLabel: 'Open gallery',
href: '/resources/prompt-gallery',
},
];

const guides = [
Expand Down
57 changes: 57 additions & 0 deletions src/app/resources/prompt-gallery/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { notFound } from 'next/navigation'
import { Metadata } from 'next'
import { getPromptBySlug, getPromptComments } from '@/lib/prompt-gallery/queries'
import { createServerComponentClient } from '@/lib/supabase/server-client'
import { PromptDetailView } from '@/components/prompt-gallery/PromptDetailView'
import { PromptCommentsSection } from '@/components/prompt-gallery/PromptCommentsSection'
import { PageShell } from '@/components/ui/PageLayout'

interface PromptDetailPageProps {
params: Promise<{ slug: string }>
}

export async function generateMetadata({ params }: PromptDetailPageProps): Promise<Metadata> {
const { slug } = await params
const prompt = await getPromptBySlug(slug)

if (!prompt) {
return {
title: 'Prompt not found | Syntax & Sips',
}
}

return {
title: `${prompt.title} | Prompt Gallery`,
description: prompt.description ?? prompt.preview,
openGraph: {
title: `${prompt.title} | Syntax & Sips`,
description: prompt.description ?? prompt.preview,
images: prompt.thumbnailUrl ? [{ url: prompt.thumbnailUrl }] : undefined,
},
}
}

export default async function PromptDetailPage({ params }: PromptDetailPageProps) {
const { slug } = await params
const prompt = await getPromptBySlug(slug)

if (!prompt) {
notFound()
}

const comments = await getPromptComments(prompt.id)
const supabase = createServerComponentClient()
const {
data: { user },
} = await supabase.auth.getUser()

return (
<PageShell>
<div className="space-y-10">
<PromptDetailView prompt={prompt} />
<PromptCommentsSection promptSlug={prompt.slug} comments={comments} canComment={Boolean(user)} />
</div>
</PageShell>
)
}

53 changes: 53 additions & 0 deletions src/app/resources/prompt-gallery/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Metadata } from 'next'
import { Suspense } from 'react'
import { getPrompts, getPromptFilters } from '@/lib/prompt-gallery/queries'
import { parsePromptFilters } from '@/lib/prompt-gallery/search'
import { PromptGalleryClient } from '@/components/prompt-gallery/PromptGalleryClient'
import { PageShell, PageHero } from '@/components/ui/PageLayout'

export const metadata: Metadata = {
title: 'Prompt Gallery | Syntax & Sips',
description:
'Discover AI prompts curated by the Syntax & Sips community. Filter by media type, model, monetization, and difficulty to find the perfect inspiration.',
}

interface PromptGalleryPageProps {
searchParams: Promise<Record<string, string | string[] | undefined>>
}

const PAGE_SIZE = 12

export default async function PromptGalleryPage({ searchParams }: PromptGalleryPageProps) {
const params = await searchParams
const filters = parsePromptFilters(params)
const page = typeof params.page === 'string' ? Math.max(1, Number.parseInt(params.page, 10) || 1) : 1

const [promptList, metadata] = await Promise.all([
getPrompts(filters, page, PAGE_SIZE),
getPromptFilters(),
])

return (
<PageShell
hero={
<PageHero
eyebrow="Prompt Gallery"
title="Share, remix, and discover world-class prompts"
description="Browse prompts across Midjourney, GPT-4o, Claude, Stable Diffusion, and more. Filter by media type, monetization, and difficulty to find exactly what you need."
/>
}
>
<Suspense fallback={<div className="rounded-3xl border-4 border-black bg-white p-10 text-center">Loading prompt gallery…</div>}>
<PromptGalleryClient
prompts={promptList.prompts}
metadata={metadata}
total={promptList.total}
page={promptList.page}
pageSize={promptList.pageSize}
initialFilters={filters}
/>
</Suspense>
</PageShell>
)
}

28 changes: 28 additions & 0 deletions src/app/resources/prompt-gallery/upload/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Metadata } from 'next'
import { getActiveModels } from '@/lib/prompt-gallery/queries'
import { PageShell, PageHero } from '@/components/ui/PageLayout'
import { PromptUploadWizard } from '@/components/prompt-gallery/PromptUploadWizard'

export const metadata: Metadata = {
title: 'Upload Prompt | Syntax & Sips',
description: 'Share your go-to prompts with the Syntax & Sips community and help others ship faster.',
}

export default async function PromptUploadPage() {
const models = await getActiveModels()

return (
<PageShell
hero={
<PageHero
eyebrow="Upload prompt"
title="Contribute to the gallery"
description="Document your prompt, attach reference media, and tag the models you used so others can remix with confidence."
/>
}
>
<PromptUploadWizard models={models} />
</PageShell>
)
}

Loading
Loading