Skip to content
Open
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
26 changes: 22 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-easy-crop": "^5.5.6",
"sharp": "^0.34.5",
"zustand": "^5.0.11"
},
"devDependencies": {
Expand Down
44 changes: 43 additions & 1 deletion src/app/admin/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from "lucide-react";
import { Type } from "lucide-react";
import AdminPageHeader from "@/components/admin/AdminPageHeader";
import CardBackgroundSettingsPanel from "@/components/admin/CardBackgroundSettingsPanel";
import useAdminTheme from "@/hooks/useAdminTheme";
import { ADMIN_THEMES } from "@/lib/admin-themes";
import { ADMIN_FONTS } from "@/lib/admin-fonts";
Expand All @@ -38,6 +39,13 @@ import {
type HeroEffectIntensity,
type HomeSectionsSettings,
} from "@/lib/home-sections-config";
import {
CARD_BACKGROUND_SETTINGS_STORAGE_KEY,
CARD_BACKGROUND_SETTINGS_UPDATED_EVENT,
DEFAULT_CARD_BACKGROUND_SETTINGS,
sanitizeCardBackgroundSettings,
type CardBackgroundSettings,
} from "@/lib/card-background-settings";

interface StoreSettings {
storeName: string;
Expand Down Expand Up @@ -127,6 +135,8 @@ export default function SettingsPage() {
const [homeSections, setHomeSections] = useState<HomeSectionsSettings>(
DEFAULT_HOME_SECTIONS_SETTINGS
);
const [cardBackgroundSettings, setCardBackgroundSettings] =
useState<CardBackgroundSettings>(DEFAULT_CARD_BACKGROUND_SETTINGS);

// Admin info (read-only)
const [adminEmail] = useState("admin@ivaniabeauty.com");
Expand Down Expand Up @@ -168,6 +178,13 @@ export default function SettingsPage() {
DEFAULT_HOME_SECTIONS_SETTINGS
);
setHomeSections(sanitizeHomeSectionsSettings(storedHomeSections));
const storedCardBackgroundSettings = getStoredValue<CardBackgroundSettings>(
CARD_BACKGROUND_SETTINGS_STORAGE_KEY,
DEFAULT_CARD_BACKGROUND_SETTINGS
);
setCardBackgroundSettings(
sanitizeCardBackgroundSettings(storedCardBackgroundSettings)
);
setLoading(false);
}, []);

Expand Down Expand Up @@ -264,14 +281,32 @@ export default function SettingsPage() {
);
window.dispatchEvent(new Event(HOME_SECTIONS_UPDATED_EVENT));
break;
case "card-backgrounds":
localStorage.setItem(
CARD_BACKGROUND_SETTINGS_STORAGE_KEY,
JSON.stringify(
sanitizeCardBackgroundSettings(cardBackgroundSettings)
)
);
window.dispatchEvent(
new Event(CARD_BACKGROUND_SETTINGS_UPDATED_EVENT)
);
break;
}
} catch (error) {
console.error("Error saving settings:", error);
} finally {
setSavingSection(null);
}
},
[store, shipping, payments, shopSections.enabledSectionIds, homeSections]
[
store,
shipping,
payments,
shopSections.enabledSectionIds,
homeSections,
cardBackgroundSettings,
]
);

if (loading) {
Expand Down Expand Up @@ -907,6 +942,13 @@ export default function SettingsPage() {
</div>
</div>

<CardBackgroundSettingsPanel
value={cardBackgroundSettings}
onChange={setCardBackgroundSettings}
onSave={() => saveSection("card-backgrounds")}
saving={savingSection === "card-backgrounds"}
/>

{/* Admin Info */}
<div className="bg-white dark:bg-gray-900 rounded-2xl border border-gray-100 dark:border-gray-800 p-6 shadow-sm transition-colors duration-300">
<div className="flex items-center gap-3 mb-5">
Expand Down
167 changes: 167 additions & 0 deletions src/app/api/admin/card-backgrounds/generate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { randomUUID } from "node:crypto";
import { NextResponse } from "next/server";
import { getAdminSession } from "@/lib/firebase/auth-helpers";
import { adminStorage, isFirebaseConfigured } from "@/lib/firebase/admin";
import {
generateCardBackgroundImage,
isCardBackgroundGenerationConfigured,
} from "@/lib/card-background-ai/generator";

export const maxDuration = 120;

interface GenerateBackgroundBody {
prompt?: string;
label?: string;
aspectRatio?: string;
}

function sanitizePrompt(value: unknown, max = 1200): string | null {
if (typeof value !== "string") return null;
const trimmed = value.trim();
if (!trimmed) return null;
return trimmed.slice(0, max);
}

function sanitizeLabel(value: unknown): string {
if (typeof value !== "string") return "Fondo IA";
const trimmed = value.trim();
if (!trimmed) return "Fondo IA";
return trimmed.slice(0, 40);
}

function sanitizeAspectRatio(value: unknown): string {
if (typeof value !== "string") return "3:4";
const trimmed = value.trim();
return trimmed || "3:4";
}

function extensionFromMimeType(mimeType: string): string {
if (mimeType.includes("jpeg")) return "jpg";
if (mimeType.includes("webp")) return "webp";
return "png";
}

function getGeminiEnvPresence() {
const presence = {
GEMINI_API_KEY: Boolean(process.env.GEMINI_API_KEY?.trim()),
GOOGLE_API_KEY: Boolean(process.env.GOOGLE_API_KEY?.trim()),
GOOGLE_GENERATIVE_AI_API_KEY: Boolean(
process.env.GOOGLE_GENERATIVE_AI_API_KEY?.trim()
),
GEMINI_CARD_BACKGROUND_MODEL: Boolean(
process.env.GEMINI_CARD_BACKGROUND_MODEL?.trim()
),
GEMINI_IMAGE_MODEL: Boolean(process.env.GEMINI_IMAGE_MODEL?.trim()),
};

const missingKeyCandidates = Object.entries(presence)
.filter(([name, isSet]) => !isSet && !name.includes("MODEL"))
.map(([name]) => name);

return {
presence,
missingKeyCandidates,
};
}

export async function POST(request: Request) {
const admin = await getAdminSession();
if (!admin) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

if (!isFirebaseConfigured()) {
return NextResponse.json(
{ error: "Firebase Storage not configured" },
{ status: 503 }
);
}

if (!isCardBackgroundGenerationConfigured()) {
const envInfo = getGeminiEnvPresence();
return NextResponse.json(
{
error:
"Gemini card background generation is not configured. Set GEMINI_API_KEY (or GOOGLE_API_KEY / GOOGLE_GENERATIVE_AI_API_KEY) and restart dev server.",
...(process.env.NODE_ENV !== "production"
? {
debug: {
envPresence: envInfo.presence,
missingKeyCandidates: envInfo.missingKeyCandidates,
},
}
: {}),
},
{ status: 503 }
);
}

try {
const body = (await request.json().catch(() => ({}))) as GenerateBackgroundBody;
const prompt = sanitizePrompt(body.prompt);
const label = sanitizeLabel(body.label);
const aspectRatio = sanitizeAspectRatio(body.aspectRatio);

const generation = await generateCardBackgroundImage({
customPrompt: prompt || undefined,
aspectRatio,
});

const ext = extensionFromMimeType(generation.mimeType);
const fileName = `products/card-backgrounds/${Date.now()}-${randomUUID()}.${ext}`;
const bucket = adminStorage.bucket();
const file = bucket.file(fileName);

await file.save(generation.imageBuffer, {
metadata: {
contentType: generation.mimeType,
},
});
await file.makePublic();
const imageUrl = `https://storage.googleapis.com/${bucket.name}/${fileName}`;

return NextResponse.json({
success: true,
imageUrl,
preset: {
id: `ai_${Date.now()}`,
label,
type: "image",
imageUrl,
builtIn: false,
createdAt: Date.now(),
},
mimeType: generation.mimeType,
prompt: generation.prompt,
revisedPrompt: generation.revisedPrompt,
modelUsed: generation.modelUsed,
aspectRatio: generation.aspectRatio,
});
} catch (error) {
const typed = error as Error & { status?: number; code?: string; attempts?: unknown };
const status =
typeof typed.status === "number" &&
typed.status >= 400 &&
typed.status <= 599
? typed.status
: 500;

return NextResponse.json(
{
error:
typed.message || "Failed to generate card background",
...(process.env.NODE_ENV !== "production"
? {
debug: {
status: typed.status || null,
code: typed.code || null,
attempts: typed.attempts || null,
},
}
: {}),
},
{ status }
);
}
}

Loading