Skip to content

Commit d645486

Browse files
committed
feat: Radiant Atelier v3.0 Upgrade - AI Assistant, QuickShip, and Warm White Theme
1 parent 7f94de8 commit d645486

File tree

18 files changed

+2394
-545
lines changed

18 files changed

+2394
-545
lines changed

app/admin/orders/OrderList.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Box, MapPin, Truck, ExternalLink, ChevronDown, ShoppingBag, CheckSquare
66
import { updateOrderStatus } from '@/lib/actions/admin'
77
import { generateShippingLabel } from '@/app/admin/orders/actions'
88
import { FulfillmentRitual } from '@/components/admin/FulfillmentRitual'
9+
import QuickShip from '@/components/admin/QuickShip'
910
import { toast } from 'sonner'
1011
import { useRouter } from 'next/navigation'
1112

@@ -189,6 +190,17 @@ export default function OrderList({ initialOrders }: OrderListProps) {
189190
Begin Fulfillment
190191
</button>
191192
)}
193+
194+
{/* QUICK SHIP DISPATCH */}
195+
{!order.shipping_label_url && order.status === 'paid' && order.shipping_address?.country === 'US' && (
196+
<QuickShip
197+
orderId={order.id}
198+
customerName={order.customer_email || 'Guest'}
199+
shippingAddress={order.shipping_address}
200+
orderItems={order.order_items || []}
201+
onSuccess={() => router.refresh()}
202+
/>
203+
)}
192204
</div>
193205
</div>
194206
))}
@@ -322,6 +334,17 @@ export default function OrderList({ initialOrders }: OrderListProps) {
322334
Fulfill
323335
</button>
324336
)}
337+
338+
{/* QUICK SHIP DISPATCH */}
339+
{!order.shipping_label_url && order.status === 'paid' && order.shipping_address?.country === 'US' && (
340+
<QuickShip
341+
orderId={order.id}
342+
customerName={order.customer_email || 'Guest'}
343+
shippingAddress={order.shipping_address}
344+
orderItems={order.order_items || []}
345+
onSuccess={() => router.refresh()}
346+
/>
347+
)}
325348
</div>
326349
</td>
327350
</tr>

app/admin/settings/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export default async function AdminSettings() {
7676
defaultChecked={isEnabled}
7777
className="sr-only peer"
7878
/>
79-
<div className="w-14 h-7 bg-[#121214] peer-focus:outline-none rounded-full border border-white/10 peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[4px] after:bg-white after:border-charcoal/30 after:border after:rounded-full after:h-6 after:w-6 after:transition-all peer-checked:bg-emerald-500 shadow-inner group transition-all"></div>
79+
<div className="w-14 h-7 bg-[#121214] peer-focus:outline-none rounded-full border border-white/10 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[4px] after:bg-white after:border-charcoal/30 after:border after:rounded-full after:h-6 after:w-6 after:transition-all peer-checked:bg-emerald-500 shadow-inner group transition-all"></div>
8080
</label>
8181
</div>
8282
</SettingsForm>
@@ -98,7 +98,7 @@ export default async function AdminSettings() {
9898
</div>
9999
<label className="relative inline-flex items-center cursor-pointer">
100100
<input type="checkbox" name="show_bestsellers_hero" defaultChecked={homeConfig.show_bestsellers_hero === true} className="sr-only peer" />
101-
<div className="w-11 h-6 bg-[#121214] peer-focus:outline-none rounded-full border border-white/10 peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-charcoal/30 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-gold shadow-inner group transition-all"></div>
101+
<div className="w-11 h-6 bg-[#121214] peer-focus:outline-none rounded-full border border-white/10 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-charcoal/30 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-gold shadow-inner group transition-all"></div>
102102
</label>
103103
</div>
104104

@@ -109,7 +109,7 @@ export default async function AdminSettings() {
109109
</div>
110110
<label className="relative inline-flex items-center cursor-pointer">
111111
<input type="checkbox" name="show_bestsellers" defaultChecked={homeConfig.show_bestsellers !== false} className="sr-only peer" />
112-
<div className="w-11 h-6 bg-[#121214] peer-focus:outline-none rounded-full border border-white/10 peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-charcoal/30 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-gold shadow-inner group transition-all"></div>
112+
<div className="w-11 h-6 bg-[#121214] peer-focus:outline-none rounded-full border border-white/10 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-charcoal/30 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-gold shadow-inner group transition-all"></div>
113113
</label>
114114
</div>
115115

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import ShippingRateManager from "@/components/admin/ShippingRateManager";
2+
3+
export default function ShippingSettingsPage() {
4+
return (
5+
<div className="p-8">
6+
<ShippingRateManager />
7+
</div>
8+
);
9+
}

app/api/admin/ai-product/route.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"use server";
2+
import { NextResponse } from "next/server";
3+
import { createClient } from "@/lib/supabase/server";
4+
5+
const CATEGORY_VARIANTS: Record<string, { names: string[]; colors: string[] }> = {
6+
lipstick: {
7+
names: ["Ruby Red", "Rose Nude", "Coral Bliss", "Berry Kiss", "Mauve Dream", "Classic Red", "Pink Petal", "Dusty Rose"],
8+
colors: ["#C0392B", "#E8B4B8", "#FF7F50", "#8E44AD", "#D4A5A5", "#E74C3C", "#FFB6C1", "#C9A0A0"],
9+
},
10+
foundation: {
11+
names: ["Ivory 01", "Beige 02", "Sand 03", "Honey 04", "Caramel 05", "Mocha 06", "Espresso 07", "Porcelain 00"],
12+
colors: ["#FFF8F0", "#F5E6D3", "#E8C9A0", "#D4A574", "#C4854A", "#A0522D", "#6B3A2A", "#FDF3E7"],
13+
},
14+
eyeshadow: {
15+
names: ["Champagne", "Rose Gold", "Smoky Quartz", "Midnight", "Bronze", "Lilac", "Copper", "Pearl"],
16+
colors: ["#F7E7CE", "#B76E79", "#7B7B7B", "#1A1A2E", "#CD7F32", "#C8A2C8", "#B87333", "#F0EAD6"],
17+
},
18+
blush: {
19+
names: ["Peachy Keen", "Rose Flush", "Berry Bliss", "Golden Hour", "Coral Glow", "Nude Flush"],
20+
colors: ["#FFCBA4", "#FFB7C5", "#CC5F8F", "#E8B86D", "#FF6B6B", "#E8B4A0"],
21+
},
22+
default: {
23+
names: ["Shade 01", "Shade 02", "Shade 03", "Shade 04", "Shade 05", "Shade 06"],
24+
colors: ["#E8D5C4", "#C4A882", "#A07850", "#785030", "#503010", "#8B6914"],
25+
},
26+
};
27+
28+
function detectCategory(title: string): string {
29+
const t = title.toLowerCase();
30+
if (t.includes("lip") || t.includes("lipstick") || t.includes("gloss") || t.includes("balm")) return "lipstick";
31+
if (t.includes("foundation") || t.includes("concealer") || t.includes("bb") || t.includes("cc")) return "foundation";
32+
if (t.includes("eye") || t.includes("shadow") || t.includes("liner")) return "eyeshadow";
33+
if (t.includes("blush") || t.includes("bronzer") || t.includes("highlighter")) return "blush";
34+
return "default";
35+
}
36+
37+
export async function POST(req: Request) {
38+
try {
39+
const supabase = await createClient();
40+
const { data: { user } } = await supabase.auth.getUser();
41+
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
42+
43+
const { data: profile } = await supabase.from("profiles").select("role").eq("id", user.id).single();
44+
if (profile?.role !== "admin") return NextResponse.json({ error: "Forbidden" }, { status: 403 });
45+
46+
const body = await req.json();
47+
const { title, imageBase64, mode } = body;
48+
49+
if (!title) return NextResponse.json({ error: "Title required" }, { status: 400 });
50+
51+
const apiKey = process.env.ANTHROPIC_API_KEY;
52+
if (!apiKey) return NextResponse.json({ error: "ANTHROPIC_API_KEY not configured in .env.local" }, { status: 500 });
53+
54+
const category = detectCategory(title);
55+
const variantData = CATEGORY_VARIANTS[category];
56+
57+
// Build messages for Claude
58+
const messages: any[] = [];
59+
const userContent: any[] = [];
60+
61+
if (imageBase64) {
62+
userContent.push({
63+
type: "image",
64+
source: { type: "base64", media_type: "image/jpeg", data: imageBase64 },
65+
});
66+
}
67+
68+
const promptMap: Record<string, string> = {
69+
description: `You are a luxury beauty copywriter for DINA COSMETIC, a high-end beauty brand.
70+
Write a compelling product description for: "${title}"
71+
${imageBase64 ? "Use the product image to inform your description." : ""}
72+
73+
Return a JSON object with:
74+
{
75+
"description": "2-3 sentence luxury product description (no markdown, plain text)",
76+
"shortDescription": "One punchy tagline under 10 words",
77+
"suggestedPrice": number (realistic USD price for luxury beauty),
78+
"keyBenefits": ["benefit 1", "benefit 2", "benefit 3"],
79+
"ingredients": "Key ingredient highlights (2-3 ingredients)",
80+
"howToUse": "Application instructions in 1-2 sentences"
81+
}
82+
83+
Write in the voice of Charlotte Tilbury or Dior Beauty. Sophisticated, sensual, confident. No generic phrases.
84+
Return ONLY the JSON object, no markdown.`,
85+
86+
variants: `You are helping create product variants for a luxury beauty brand.
87+
Product: "${title}"
88+
Category detected: ${category}
89+
90+
Return a JSON object with:
91+
{
92+
"variants": [
93+
{ "name": "Shade Name", "colorCode": "#hexcode", "suggestedPrice": number }
94+
]
95+
}
96+
97+
Generate exactly 6 realistic shade/variant names appropriate for this product type.
98+
Use the following as inspiration but create unique names: ${variantData.names.join(", ")}
99+
Make names sound luxurious and evocative (e.g., "Midnight Noir" not "Dark Black").
100+
Return ONLY the JSON object, no markdown.`,
101+
};
102+
103+
userContent.push({ type: "text", text: promptMap[mode] || promptMap.description });
104+
messages.push({ role: "user", content: userContent });
105+
106+
const response = await fetch("https://api.anthropic.com/v1/messages", {
107+
method: "POST",
108+
headers: {
109+
"Content-Type": "application/json",
110+
"x-api-key": apiKey,
111+
"anthropic-version": "2023-06-01",
112+
},
113+
body: JSON.stringify({
114+
model: "claude-3-5-sonnet-20240620",
115+
max_tokens: 1024,
116+
messages,
117+
}),
118+
});
119+
120+
if (!response.ok) {
121+
const err = await response.text();
122+
return NextResponse.json({ error: `Claude API error: ${err}` }, { status: 500 });
123+
}
124+
125+
const data = await response.json();
126+
const text = data.content?.[0]?.text || "{}";
127+
128+
let parsed: any = {};
129+
try {
130+
const clean = text.replace(/```json|```/g, "").trim();
131+
parsed = JSON.parse(clean);
132+
} catch {
133+
parsed = { description: text };
134+
}
135+
136+
return NextResponse.json({ success: true, result: parsed, category });
137+
} catch (err: any) {
138+
return NextResponse.json({ error: err.message }, { status: 500 });
139+
}
140+
}

app/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Playfair_Display, Inter } from "next/font/google";
33
import "@/styles/globals.css";
44
import { CartProvider } from "@/context/CartContext";
55
import Header from "@/components/Header";
6+
import AnnouncementBar from "@/components/AnnouncementBar";
67
import { ShoppingBagDrawer } from "@/components/ShoppingBagDrawer";
78
import { Analytics } from "@vercel/analytics/next";
89
import { createClient } from "@/lib/supabase/server";
@@ -147,6 +148,7 @@ export default async function RootLayout({
147148
>
148149
<CartProvider>
149150
<div className="relative flex flex-col min-h-screen">
151+
<AnnouncementBar />
150152
<Header navItems={headerNavItems} logoUrl={logoUrl} />
151153
<main className="flex-grow">
152154
{children}

0 commit comments

Comments
 (0)