From ee61bbbc5397adfad903e654f9ad2814e765c212 Mon Sep 17 00:00:00 2001 From: Dev1822 Date: Wed, 5 Aug 2026 10:39:51 +0530 Subject: [PATCH 1/2] feat: add custom background images for user profiles --- app/[username]/page.tsx | 15 ++++++- app/api/user/background/route.ts | 30 ++++++++++++++ app/dashboard/AppearanceSection.tsx | 61 +++++++++++++++++++++++++++++ app/dashboard/DashboardClient.tsx | 5 +++ app/dashboard/page.tsx | 1 + lib/userLookup.ts | 1 + prisma/schema.prisma | 3 ++ 7 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 app/api/user/background/route.ts diff --git a/app/[username]/page.tsx b/app/[username]/page.tsx index 9ec7f69..c99a7cb 100644 --- a/app/[username]/page.tsx +++ b/app/[username]/page.tsx @@ -126,8 +126,21 @@ export default async function PublicProfile({ return (
+ {user.backgroundImage && ( + <> +
+
+ + )} -
+
void; onUpdateLayout: (layout: LayoutStyle) => void; + onUpdateBackgroundImage: (url: string | null) => void; }) { const [selectedTheme, setSelectedTheme] = useState(initialTheme || "default"); const [selectedLayout, setSelectedLayout] = useState(initialLayout || "LIST"); + const [backgroundImage, setBackgroundImage] = useState(initialBackgroundImage || ""); const [savingTheme, setSavingTheme] = useState(false); const [savingLayout, setSavingLayout] = useState(false); + const [savingBg, setSavingBg] = useState(false); async function handleSaveTheme(themeId: string) { setSelectedTheme(themeId); @@ -90,6 +96,34 @@ export function AppearanceSection({ } } + async function handleSaveBackgroundImage() { + setSavingBg(true); + try { + const csrfToken = await getCsrfToken(); + const res = await fetch("/api/user/background", { + method: "PATCH", + headers: { + "Content-Type": "application/json", + "x-csrf-token": csrfToken, + }, + body: JSON.stringify({ backgroundImage: backgroundImage || null }), + }); + + if (!res.ok) { + throw new Error("Failed to save background image"); + } + + const data = await res.json(); + onUpdateBackgroundImage(data.backgroundImage); + toast.success("Background image updated!"); + } catch (error) { + toast.error("Failed to update background image"); + console.error(error); + } finally { + setSavingBg(false); + } + } + return (
{/* Layout Section */} @@ -169,6 +203,33 @@ export function AppearanceSection({ ))}
+ + {/* Background Image Section */} +
+
+

Custom Background

+

+ Set a custom background image URL for your public profile. +

+
+
+ setBackgroundImage(e.target.value)} + disabled={savingBg} + /> + +
+
); } diff --git a/app/dashboard/DashboardClient.tsx b/app/dashboard/DashboardClient.tsx index fdb1e20..cc2090b 100644 --- a/app/dashboard/DashboardClient.tsx +++ b/app/dashboard/DashboardClient.tsx @@ -19,6 +19,7 @@ export default function DashboardClient({ initialSeoTitle, initialSeoDescription, initialLayout, + initialBackgroundImage, qrCode, enableEmailCapture, subscribers = [], @@ -29,6 +30,7 @@ export default function DashboardClient({ initialSeoTitle?: string; initialSeoDescription?: string; initialLayout?: LayoutStyle; + initialBackgroundImage?: string | null; qrCode?: React.ReactNode; enableEmailCapture?: boolean; subscribers?: { id: string; email: string; createdAt: Date }[]; @@ -36,6 +38,7 @@ export default function DashboardClient({ const [links, setLinks] = useState(initialLinks); const [theme, setTheme] = useState(initialTheme || "default"); const [layoutStyle, setLayoutStyle] = useState(initialLayout || "LIST"); + const [backgroundImage, setBackgroundImage] = useState(initialBackgroundImage || ""); const [seoTitle, setSeoTitle] = useState(initialSeoTitle || ""); const [seoDescription, setSeoDescription] = useState(initialSeoDescription || ""); const [activeTab, setActiveTab] = useState<"links" | "appearance" | "seo">("links"); @@ -399,8 +402,10 @@ export default function DashboardClient({ ) : ( } diff --git a/lib/userLookup.ts b/lib/userLookup.ts index 1e370c8..e79c7f0 100644 --- a/lib/userLookup.ts +++ b/lib/userLookup.ts @@ -13,6 +13,7 @@ const publicProfileSelect = { username: true, bio: true, image: true, + backgroundImage: true, theme: true, themeType: true, themeColor: true, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f3ff6b8..1484dd2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -17,6 +17,7 @@ model User { email String @unique emailVerified DateTime? image String? + backgroundImage String? password String? @@ -214,6 +215,7 @@ model ProfileDraft { username String? bio String? image String? + backgroundImage String? themeType String? themeColor String? themeCustom String? @@ -232,6 +234,7 @@ model ProfileVersion { username String? bio String? image String? + backgroundImage String? themeType String? themeColor String? themeCustom String? From a4a271d95e33cb34f709a87fa0e2a106add4f123 Mon Sep 17 00:00:00 2001 From: Dev1822 Date: Wed, 5 Aug 2026 10:54:32 +0530 Subject: [PATCH 2/2] fix: address PR review feedback --- app/api/user/background/route.ts | 3 +++ app/dashboard/AppearanceSection.tsx | 3 ++- lib/profileWorkflow.ts | 20 ++++++++++++++++++- .../migration.sql | 8 ++++++++ 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 prisma/migrations/20260805_add_background_image/migration.sql diff --git a/app/api/user/background/route.ts b/app/api/user/background/route.ts index 5c02313..d4851f8 100644 --- a/app/api/user/background/route.ts +++ b/app/api/user/background/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import prisma from "@/lib/prisma"; import { getServerSession } from "next-auth"; import { authOptions } from "@/lib/auth"; +import { revalidateTag } from "next/cache"; export async function PATCH(req: NextRequest) { try { @@ -21,6 +22,8 @@ export async function PATCH(req: NextRequest) { where: { email: session.user.email }, data: { backgroundImage }, }); + + revalidateTag("public-profile", "default"); return NextResponse.json({ success: true, backgroundImage: updatedUser.backgroundImage }, { status: 200 }); } catch (error) { diff --git a/app/dashboard/AppearanceSection.tsx b/app/dashboard/AppearanceSection.tsx index 49dd935..eaa8a85 100644 --- a/app/dashboard/AppearanceSection.tsx +++ b/app/dashboard/AppearanceSection.tsx @@ -215,9 +215,10 @@ export function AppearanceSection({
setBackgroundImage(e.target.value)} disabled={savingBg} /> diff --git a/lib/profileWorkflow.ts b/lib/profileWorkflow.ts index 30e1549..19dbff7 100644 --- a/lib/profileWorkflow.ts +++ b/lib/profileWorkflow.ts @@ -19,6 +19,7 @@ export interface ProfileSnapshot { themeType: string | null; themeColor: string | null; themeCustom: string | null; + backgroundImage: string | null; } /** @@ -59,6 +60,7 @@ export async function upsertProfileDraft( themeType: draft.themeType, themeColor: draft.themeColor, themeCustom: draft.themeCustom, + backgroundImage: draft.backgroundImage, }; } @@ -77,7 +79,7 @@ export async function getEditableProfileState( // a subset of fields are being edited in the draft). const user = await prisma.user.findUnique({ where: { id: userId }, - select: { name: true, username: true, bio: true, image: true, themeType: true, themeColor: true, themeCustom: true }, + select: { name: true, username: true, bio: true, image: true, themeType: true, themeColor: true, themeCustom: true, backgroundImage: true }, }); // If a draft exists but the live user record is missing, the system @@ -96,6 +98,7 @@ export async function getEditableProfileState( themeType: draft.themeType ?? user.themeType ?? null, themeColor: draft.themeColor ?? user.themeColor ?? null, themeCustom: draft.themeCustom ?? user.themeCustom ?? null, + backgroundImage: draft.backgroundImage ?? user.backgroundImage ?? null, }; } @@ -109,6 +112,7 @@ export async function getEditableProfileState( themeType: true, themeColor: true, themeCustom: true, + backgroundImage: true, }, }); @@ -124,6 +128,7 @@ export async function getEditableProfileState( themeType: user.themeType, themeColor: user.themeColor, themeCustom: user.themeCustom, + backgroundImage: user.backgroundImage, }; } @@ -182,6 +187,9 @@ function diffProfileSnapshots( if (before.themeCustom !== after.themeCustom) { diff.themeCustom = { before: before.themeCustom, after: after.themeCustom }; } + if (before.backgroundImage !== after.backgroundImage) { + diff.backgroundImage = { before: before.backgroundImage, after: after.backgroundImage }; + } return diff; } @@ -243,6 +251,7 @@ export async function publishProfileDraft( themeType: true, themeColor: true, themeCustom: true, + backgroundImage: true, }, }); @@ -259,6 +268,7 @@ export async function publishProfileDraft( themeType: draft.themeType ?? user.themeType, themeColor: draft.themeColor ?? user.themeColor, themeCustom: draft.themeCustom ?? user.themeCustom, + backgroundImage: draft.backgroundImage ?? user.backgroundImage, }; // Calculate diff @@ -287,6 +297,7 @@ export async function publishProfileDraft( themeType: afterSnapshot.themeType ?? "solid", themeColor: afterSnapshot.themeColor ?? "slate", themeCustom: afterSnapshot.themeCustom, + backgroundImage: afterSnapshot.backgroundImage, }, }); @@ -301,6 +312,7 @@ export async function publishProfileDraft( themeType: afterSnapshot.themeType ?? "solid", themeColor: afterSnapshot.themeColor ?? "slate", themeCustom: afterSnapshot.themeCustom, + backgroundImage: afterSnapshot.backgroundImage, changeType: "publish", diffJson: JSON.stringify(diff), }, @@ -399,6 +411,7 @@ export async function resolvePreviewToken( themeType: draft.themeType, themeColor: draft.themeColor, themeCustom: draft.themeCustom, + backgroundImage: draft.backgroundImage, }; return { @@ -427,6 +440,7 @@ export async function getProfileVersions(userId: string, limit: number = 20) { themeType: v.themeType, themeColor: v.themeColor, themeCustom: v.themeCustom, + backgroundImage: v.backgroundImage, }, changeType: v.changeType, diff: v.diffJson ? JSON.parse(v.diffJson) : {}, @@ -462,6 +476,7 @@ export async function rollbackProfileVersion( themeType: true, themeColor: true, themeCustom: true, + backgroundImage: true, }, }); @@ -478,6 +493,7 @@ export async function rollbackProfileVersion( themeType: version.themeType, themeColor: version.themeColor, themeCustom: version.themeCustom, + backgroundImage: version.backgroundImage, }; // Calculate diff @@ -510,6 +526,7 @@ export async function rollbackProfileVersion( themeType: afterSnapshot.themeType ?? "solid", themeColor: afterSnapshot.themeColor ?? "slate", themeCustom: afterSnapshot.themeCustom, + backgroundImage: afterSnapshot.backgroundImage, }, }); @@ -524,6 +541,7 @@ export async function rollbackProfileVersion( themeType: afterSnapshot.themeType ?? "solid", themeColor: afterSnapshot.themeColor ?? "slate", themeCustom: afterSnapshot.themeCustom, + backgroundImage: afterSnapshot.backgroundImage, changeType: "rollback", diffJson: JSON.stringify(diff), }, diff --git a/prisma/migrations/20260805_add_background_image/migration.sql b/prisma/migrations/20260805_add_background_image/migration.sql new file mode 100644 index 0000000..dc21300 --- /dev/null +++ b/prisma/migrations/20260805_add_background_image/migration.sql @@ -0,0 +1,8 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "backgroundImage" TEXT; + +-- AlterTable +ALTER TABLE "ProfileDraft" ADD COLUMN "backgroundImage" TEXT; + +-- AlterTable +ALTER TABLE "ProfileVersion" ADD COLUMN "backgroundImage" TEXT;