feat: Customizable storefront chat
This commit is contained in:
parent
7b2cb3485e
commit
fae9e9ce7d
@ -146,6 +146,9 @@ function sanitizeLogoUrl(value: unknown) {
|
||||
if (typeof value !== "string") return null
|
||||
const v = value.trim()
|
||||
if (!v) return null
|
||||
if (v.startsWith("/") && !v.includes("..")) {
|
||||
return v.slice(0, 500)
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(v)
|
||||
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null
|
||||
@ -155,6 +158,19 @@ function sanitizeLogoUrl(value: unknown) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Storefront script runs on Shopify origin; relative /uploads/... must become absolute JUNO app URLs. */
|
||||
function resolveLogoUrlForInject(appBaseUrl: string, logoUrl: string | null) {
|
||||
if (!logoUrl) return null
|
||||
const v = logoUrl.trim()
|
||||
if (!v) return null
|
||||
if (/^https?:\/\//i.test(v)) return v
|
||||
if (v.startsWith("/") && !v.includes("..")) {
|
||||
const base = appBaseUrl.replace(/\/$/, "")
|
||||
return `${base}${v}`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function buildStorefrontScript(params: {
|
||||
appBaseUrl: string
|
||||
chatName: string
|
||||
@ -332,18 +348,33 @@ function buildStorefrontScript(params: {
|
||||
btn.style.width = "56px";
|
||||
btn.style.height = "56px";
|
||||
btn.style.borderRadius = "16px";
|
||||
btn.style.border = "0";
|
||||
btn.style.cursor = "pointer";
|
||||
btn.style.boxShadow = "0 12px 28px rgba(0,0,0,0.20)";
|
||||
btn.style.background = brandColor || "#4f46e5";
|
||||
btn.style.color = "#fff";
|
||||
btn.style.display = "flex";
|
||||
btn.style.alignItems = "center";
|
||||
btn.style.justifyContent = "center";
|
||||
btn.style.fontFamily = fontFamily;
|
||||
btn.style.fontWeight = "800";
|
||||
btn.style.letterSpacing = "-0.02em";
|
||||
btn.innerHTML = "Chat";
|
||||
btn.style.padding = "0";
|
||||
btn.style.overflow = "hidden";
|
||||
if (logoUrl) {
|
||||
btn.style.background = "#ffffff";
|
||||
btn.style.border = "2px solid " + (brandColor || "#4f46e5");
|
||||
var fabImg = document.createElement("img");
|
||||
fabImg.src = logoUrl;
|
||||
fabImg.alt = chatName || "Chat";
|
||||
fabImg.style.width = "36px";
|
||||
fabImg.style.height = "36px";
|
||||
fabImg.style.objectFit = "contain";
|
||||
fabImg.style.display = "block";
|
||||
btn.appendChild(fabImg);
|
||||
} else {
|
||||
btn.style.border = "0";
|
||||
btn.style.background = brandColor || "#4f46e5";
|
||||
btn.style.color = "#fff";
|
||||
btn.innerHTML = "Chat";
|
||||
}
|
||||
btn.addEventListener("click", function () { openPanel(); });
|
||||
container.appendChild(btn);
|
||||
}
|
||||
@ -447,7 +478,7 @@ async function syncStorefrontTheme(params: {
|
||||
tagline: params.tagline,
|
||||
brandColor: params.brandColor,
|
||||
fontFamily: params.fontFamily,
|
||||
logoUrl: params.logoUrl,
|
||||
logoUrl: resolveLogoUrlForInject(appBaseUrl, params.logoUrl),
|
||||
})
|
||||
|
||||
const putAsset = await putThemeAsset(cleanDomain, accessToken, apiVersion, themeId, assetKey, js)
|
||||
|
||||
@ -5,6 +5,7 @@ import { Store, Plus, Check, Loader2, AlertCircle, Building2, Trash2 } from "luc
|
||||
import AddStoreModal from "@/components/AddStoreModal"
|
||||
import DeleteStoreModal from "@/components/DeleteStoreModal"
|
||||
import StorefrontChatCustomizeModal, { type StorefrontChatCustomization } from "@/components/StorefrontChatCustomizeModal"
|
||||
import { useTheme } from "@/components/ThemeProvider"
|
||||
|
||||
interface StoreData {
|
||||
id: string
|
||||
@ -38,6 +39,8 @@ export default function StoresPage() {
|
||||
const [customizeStoreId, setCustomizeStoreId] = useState<string | null>(null)
|
||||
const [injectingStoreId, setInjectingStoreId] = useState<string | null>(null)
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false)
|
||||
const { theme } = useTheme()
|
||||
const isLight = theme === "light"
|
||||
/*
|
||||
* Alignment Fix Details:
|
||||
* - Use React `createPortal` to render the modal backdrop and content at the end of `document.body`.
|
||||
@ -261,17 +264,32 @@ export default function StoresPage() {
|
||||
{stores.map((store) => (
|
||||
<div
|
||||
key={store.id}
|
||||
className={`bg-white/5 backdrop-blur-xl p-4 md:p-6 rounded-xl md:rounded-2xl border transition-all group relative ${store.isActive
|
||||
? "border-indigo-500/30 bg-indigo-500/10"
|
||||
: "border-white/10 hover:bg-white/[0.07]"
|
||||
}`}
|
||||
className={`backdrop-blur-xl p-4 md:p-6 rounded-xl md:rounded-2xl border transition-all group relative ${
|
||||
store.isActive
|
||||
? isLight
|
||||
? "border-indigo-600/25 bg-indigo-600/5"
|
||||
: "border-indigo-500/30 bg-indigo-500/10"
|
||||
: isLight
|
||||
? "border-slate-200/70 bg-white/80 hover:bg-white/95"
|
||||
: "border-white/10 hover:bg-white/[0.07]"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3 md:mb-4">
|
||||
<div className="w-10 h-10 md:w-12 md:h-12 rounded-lg md:rounded-xl bg-indigo-500/20 border border-indigo-400/20 flex items-center justify-center shrink-0">
|
||||
<Store className="w-5 h-5 md:w-6 md:h-6 text-indigo-400" />
|
||||
<div
|
||||
className={`w-10 h-10 md:w-12 md:h-12 rounded-lg md:rounded-xl flex items-center justify-center shrink-0 ${
|
||||
isLight
|
||||
? "bg-indigo-100 border border-indigo-200"
|
||||
: "bg-indigo-500/20 border border-indigo-400/20"
|
||||
}`}
|
||||
>
|
||||
<Store className={`w-5 h-5 md:w-6 md:h-6 ${isLight ? "text-indigo-700" : "text-indigo-400"}`} />
|
||||
</div>
|
||||
{store.isActive && (
|
||||
<span className="px-2 md:px-2.5 py-1 bg-indigo-500/30 text-indigo-300 rounded-full text-[9px] md:text-[10px] font-black uppercase tracking-wider flex items-center gap-1 border border-indigo-400/20">
|
||||
<span
|
||||
className={`px-2 md:px-2.5 py-1 rounded-full text-[9px] md:text-[10px] font-black uppercase tracking-wider flex items-center gap-1 border ${
|
||||
isLight ? "bg-indigo-600/10 text-indigo-700 border-indigo-600/20" : "bg-indigo-500/30 text-indigo-300 border-indigo-400/20"
|
||||
}`}
|
||||
>
|
||||
<Check className="w-2.5 h-2.5 md:w-3 md:h-3" />
|
||||
Active
|
||||
</span>
|
||||
@ -279,17 +297,23 @@ export default function StoresPage() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 md:space-y-2 mb-3 md:mb-4">
|
||||
<h3 className="text-base md:text-lg font-black text-white truncate">{store.businessName}</h3>
|
||||
<p className="text-xs text-slate-400 font-medium truncate">{store.shopifyDomain}</p>
|
||||
<p className="text-[10px] text-slate-500">
|
||||
<h3 className={`text-base md:text-lg font-black truncate ${isLight ? "text-slate-900" : "text-white"}`}>
|
||||
{store.businessName}
|
||||
</h3>
|
||||
<p className={`text-xs font-medium truncate ${isLight ? "text-slate-600" : "text-slate-400"}`}>
|
||||
{store.shopifyDomain}
|
||||
</p>
|
||||
<p className={`text-[10px] ${isLight ? "text-slate-500" : "text-slate-500"}`}>
|
||||
Connected {new Date(store.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-3 mb-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs md:text-sm text-white font-bold truncate">Storefront Chat (Customers)</p>
|
||||
<p className="text-[10px] md:text-[11px] text-slate-400">
|
||||
<p className={`text-xs md:text-sm font-bold truncate ${isLight ? "text-slate-900" : "text-white"}`}>
|
||||
Storefront Chat (Customers)
|
||||
</p>
|
||||
<p className={`text-[10px] md:text-[11px] ${isLight ? "text-slate-600" : "text-slate-400"}`}>
|
||||
Show chat widget on the storefront
|
||||
</p>
|
||||
</div>
|
||||
@ -313,7 +337,11 @@ export default function StoresPage() {
|
||||
<button
|
||||
onClick={() => handleSwitchStore(store.id)}
|
||||
disabled={switching === store.id}
|
||||
className="flex-1 h-9 md:h-10 rounded-xl bg-white/10 text-white text-xs md:text-sm font-bold hover:bg-white/15 border border-white/10 transition-all disabled:opacity-50 flex items-center justify-center gap-2"
|
||||
className={`flex-1 h-9 md:h-10 rounded-xl text-xs md:text-sm font-bold transition-all disabled:opacity-50 flex items-center justify-center gap-2 ${
|
||||
isLight
|
||||
? "bg-indigo-600 text-white hover:bg-indigo-700 border border-indigo-600/20"
|
||||
: "bg-white/10 text-white hover:bg-white/15 border border-white/10"
|
||||
}`}
|
||||
>
|
||||
{switching === store.id ? (
|
||||
<>
|
||||
@ -328,18 +356,28 @@ export default function StoresPage() {
|
||||
<button
|
||||
onClick={() => openCustomize(store.id)}
|
||||
disabled={!store.storefrontChatEnabled || injectingStoreId === store.id}
|
||||
className="h-9 md:h-10 px-3 rounded-xl bg-indigo-500/20 text-indigo-200 text-xs font-bold hover:bg-indigo-500/30 border border-indigo-400/30 transition-all disabled:opacity-50"
|
||||
className={`h-9 md:h-10 px-3 rounded-xl text-xs font-bold transition-all disabled:opacity-50 ${
|
||||
isLight
|
||||
? "bg-indigo-700 text-white hover:bg-indigo-800 border border-indigo-600/25"
|
||||
: "bg-indigo-500/20 text-indigo-200 hover:bg-indigo-500/30 border border-indigo-400/30"
|
||||
}`}
|
||||
>
|
||||
{injectingStoreId === store.id ? "Injecting..." : "Customize"}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDeleteClick(store)}
|
||||
className="w-9 h-9 md:w-10 md:h-10 rounded-xl bg-white/10 hover:bg-red-500/20 border border-white/10 hover:border-red-500/30 flex items-center justify-center transition-all opacity-0 group-hover:opacity-100 shrink-0"
|
||||
className={`w-9 h-9 md:w-10 md:h-10 rounded-xl flex items-center justify-center transition-all opacity-0 group-hover:opacity-100 shrink-0 ${
|
||||
isLight ? "bg-red-50 border border-red-200 hover:bg-red-100 hover:border-red-300" : "bg-white/10 hover:bg-red-500/20 border border-white/10 hover:border-red-500/30"
|
||||
}`}
|
||||
title="Delete store"
|
||||
aria-label="Delete store"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 md:w-5 md:h-5 text-slate-400 group-hover:text-red-400 transition-colors" />
|
||||
<Trash2
|
||||
className={`w-4 h-4 md:w-5 md:h-5 ${
|
||||
isLight ? "text-slate-500 group-hover:text-red-600" : "text-slate-400 group-hover:text-red-400"
|
||||
} transition-colors`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -179,16 +179,6 @@ export default function StorefrontChatWidgetPage() {
|
||||
|
||||
return (
|
||||
<div className="w-screen h-screen overflow-hidden bg-white text-slate-900 flex flex-col">
|
||||
<div className="p-3 border-b border-slate-200 bg-white flex items-center gap-3 flex-shrink-0">
|
||||
<div className="w-10 h-10 rounded-xl bg-indigo-500/15 border border-indigo-500/20 flex items-center justify-center text-indigo-700">
|
||||
<MessageSquare className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="font-black text-sm truncate">JUNO Chat</p>
|
||||
<p className="text-[11px] text-slate-500">Ask about products</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 bg-gradient-to-b from-white to-slate-50">
|
||||
{sendError ? (
|
||||
<div className="mb-3 px-3 py-2 rounded-xl bg-red-500/10 border border-red-500/20 text-red-600 text-xs font-medium">
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { AnimatePresence, motion } from "framer-motion"
|
||||
import { Loader2, Palette, Save, Upload, WandSparkles, X } from "lucide-react"
|
||||
import { useTheme } from "@/components/ThemeProvider"
|
||||
|
||||
export type StorefrontChatCustomization = {
|
||||
storefrontChatName: string
|
||||
@ -37,6 +38,9 @@ export default function StorefrontChatCustomizeModal({
|
||||
onSave,
|
||||
onInject,
|
||||
}: Props) {
|
||||
const { theme } = useTheme()
|
||||
const isLight = theme === "light"
|
||||
|
||||
const [form, setForm] = useState<StorefrontChatCustomization>(initialValues)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [injecting, setInjecting] = useState(false)
|
||||
@ -122,68 +126,105 @@ export default function StorefrontChatCustomizeModal({
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm"
|
||||
className={`fixed inset-0 z-50 backdrop-blur-sm ${isLight ? "bg-slate-900/35" : "bg-black/70"}`}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 pointer-events-none">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 12, scale: 0.98 }}
|
||||
className="w-full max-w-2xl rounded-2xl border border-white/10 bg-slate-900 shadow-2xl overflow-hidden"
|
||||
className={`pointer-events-auto w-full max-w-2xl rounded-2xl shadow-2xl overflow-hidden ${
|
||||
isLight
|
||||
? "border border-slate-200 bg-white"
|
||||
: "border border-white/10 bg-slate-900"
|
||||
}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="px-5 py-4 border-b border-white/10 flex items-center justify-between">
|
||||
<div
|
||||
className={`px-5 py-4 flex items-center justify-between border-b ${
|
||||
isLight ? "border-slate-200 bg-white" : "border-white/10"
|
||||
}`}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="text-white font-black text-lg">Customize Storefront Chat</p>
|
||||
<p className="text-xs text-slate-400 truncate">{storeName}</p>
|
||||
<p className={`font-black text-lg ${isLight ? "text-slate-900" : "text-white"}`}>
|
||||
Customize Storefront Chat
|
||||
</p>
|
||||
<p className={`text-xs truncate ${isLight ? "text-slate-500" : "text-slate-400"}`}>{storeName}</p>
|
||||
</div>
|
||||
<button onClick={onClose} className="w-8 h-8 rounded-lg bg-white/10 hover:bg-white/15 text-slate-300 flex items-center justify-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className={`w-8 h-8 rounded-lg flex items-center justify-center ${
|
||||
isLight
|
||||
? "bg-slate-100 hover:bg-slate-200 text-slate-600"
|
||||
: "bg-white/10 hover:bg-white/15 text-slate-300"
|
||||
}`}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-5 grid md:grid-cols-2 gap-4">
|
||||
<label className="block">
|
||||
<span className="text-xs font-bold text-slate-300">Chat Name</span>
|
||||
<span className={`text-xs font-bold ${isLight ? "text-slate-700" : "text-slate-300"}`}>Chat Name</span>
|
||||
<input
|
||||
value={form.storefrontChatName}
|
||||
onChange={(e) => update("storefrontChatName", e.target.value)}
|
||||
className="mt-1 w-full h-10 rounded-xl bg-white/5 border border-white/10 px-3 text-sm text-white outline-none focus:ring-2 focus:ring-indigo-500/30"
|
||||
className={`mt-1 w-full h-10 rounded-xl border px-3 text-sm outline-none focus:ring-2 focus:ring-indigo-500/30 ${
|
||||
isLight
|
||||
? "bg-white border-slate-200 text-slate-900 placeholder:text-slate-400"
|
||||
: "bg-white/5 border-white/10 text-white"
|
||||
}`}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className="text-xs font-bold text-slate-300">Tagline</span>
|
||||
<span className={`text-xs font-bold ${isLight ? "text-slate-700" : "text-slate-300"}`}>Tagline</span>
|
||||
<input
|
||||
value={form.storefrontChatTagline}
|
||||
onChange={(e) => update("storefrontChatTagline", e.target.value)}
|
||||
className="mt-1 w-full h-10 rounded-xl bg-white/5 border border-white/10 px-3 text-sm text-white outline-none focus:ring-2 focus:ring-indigo-500/30"
|
||||
className={`mt-1 w-full h-10 rounded-xl border px-3 text-sm outline-none focus:ring-2 focus:ring-indigo-500/30 ${
|
||||
isLight
|
||||
? "bg-white border-slate-200 text-slate-900 placeholder:text-slate-400"
|
||||
: "bg-white/5 border-white/10 text-white"
|
||||
}`}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className="text-xs font-bold text-slate-300">Brand Color</span>
|
||||
<span className={`text-xs font-bold ${isLight ? "text-slate-700" : "text-slate-300"}`}>Brand Color</span>
|
||||
<div className="mt-1 flex gap-2">
|
||||
<input
|
||||
type="color"
|
||||
value={form.storefrontChatBrandColor}
|
||||
onChange={(e) => update("storefrontChatBrandColor", e.target.value)}
|
||||
className="w-12 h-10 rounded-lg bg-transparent border border-white/10"
|
||||
className={`w-12 h-10 rounded-lg bg-transparent border ${
|
||||
isLight ? "border-slate-200" : "border-white/10"
|
||||
}`}
|
||||
/>
|
||||
<input
|
||||
value={form.storefrontChatBrandColor}
|
||||
onChange={(e) => update("storefrontChatBrandColor", e.target.value)}
|
||||
className="flex-1 h-10 rounded-xl bg-white/5 border border-white/10 px-3 text-sm text-white outline-none focus:ring-2 focus:ring-indigo-500/30"
|
||||
className={`flex-1 h-10 rounded-xl border px-3 text-sm outline-none focus:ring-2 focus:ring-indigo-500/30 ${
|
||||
isLight
|
||||
? "bg-white border-slate-200 text-slate-900"
|
||||
: "bg-white/5 border-white/10 text-white"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className="text-xs font-bold text-slate-300">Font</span>
|
||||
<span className={`text-xs font-bold ${isLight ? "text-slate-700" : "text-slate-300"}`}>Font</span>
|
||||
<select
|
||||
value={form.storefrontChatFontFamily}
|
||||
onChange={(e) => update("storefrontChatFontFamily", e.target.value)}
|
||||
className="mt-1 w-full h-10 rounded-xl bg-white/5 border border-white/10 px-3 text-sm text-white outline-none focus:ring-2 focus:ring-indigo-500/30"
|
||||
className={`mt-1 w-full h-10 rounded-xl border px-3 text-sm outline-none focus:ring-2 focus:ring-indigo-500/30 ${
|
||||
isLight
|
||||
? "bg-white border-slate-200 text-slate-900"
|
||||
: "bg-white/5 border-white/10 text-white"
|
||||
}`}
|
||||
>
|
||||
{FONT_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value} className="text-slate-900">
|
||||
@ -194,9 +235,17 @@ export default function StorefrontChatCustomizeModal({
|
||||
</label>
|
||||
|
||||
<div className="block md:col-span-2">
|
||||
<span className="text-xs font-bold text-slate-300">Logo Image (optional)</span>
|
||||
<span className={`text-xs font-bold ${isLight ? "text-slate-700" : "text-slate-300"}`}>
|
||||
Logo Image (optional)
|
||||
</span>
|
||||
<div className="mt-1 flex flex-wrap gap-2">
|
||||
<label className="h-10 px-3 rounded-xl bg-white/10 text-slate-200 hover:bg-white/15 border border-white/10 cursor-pointer font-bold text-sm inline-flex items-center gap-2">
|
||||
<label
|
||||
className={`h-10 px-3 rounded-xl cursor-pointer font-bold text-sm inline-flex items-center gap-2 border ${
|
||||
isLight
|
||||
? "bg-indigo-50 text-indigo-800 hover:bg-indigo-100 border-indigo-200"
|
||||
: "bg-white/10 text-slate-200 hover:bg-white/15 border-white/10"
|
||||
}`}
|
||||
>
|
||||
{uploadingLogo ? <Loader2 className="w-4 h-4 animate-spin" /> : <Upload className="w-4 h-4" />}
|
||||
Upload image
|
||||
<input
|
||||
@ -214,14 +263,26 @@ export default function StorefrontChatCustomizeModal({
|
||||
value={form.storefrontChatLogoUrl}
|
||||
onChange={(e) => update("storefrontChatLogoUrl", e.target.value)}
|
||||
placeholder="Or paste logo URL"
|
||||
className="flex-1 min-w-[220px] h-10 rounded-xl bg-white/5 border border-white/10 px-3 text-sm text-white outline-none focus:ring-2 focus:ring-indigo-500/30"
|
||||
className={`flex-1 min-w-[220px] h-10 rounded-xl border px-3 text-sm outline-none focus:ring-2 focus:ring-indigo-500/30 ${
|
||||
isLight
|
||||
? "bg-white border-slate-200 text-slate-900 placeholder:text-slate-400"
|
||||
: "bg-white/5 border-white/10 text-white"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[11px] text-slate-500 mt-1">Supported: image files up to 3MB.</p>
|
||||
<p className={`text-[11px] mt-1 ${isLight ? "text-slate-500" : "text-slate-500"}`}>
|
||||
Supported: image files up to 3MB.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2 rounded-xl border border-white/10 bg-white/5 p-3">
|
||||
<p className="text-[11px] font-bold text-slate-400 mb-2">Preview</p>
|
||||
<div
|
||||
className={`md:col-span-2 rounded-xl border p-3 ${
|
||||
isLight ? "border-slate-200 bg-slate-50" : "border-white/10 bg-white/5"
|
||||
}`}
|
||||
>
|
||||
<p className={`text-[11px] font-bold mb-2 ${isLight ? "text-slate-600" : "text-slate-400"}`}>
|
||||
Preview
|
||||
</p>
|
||||
<div className="rounded-xl border border-slate-200 bg-white overflow-hidden">
|
||||
<div className="h-12 border-b border-slate-200 flex items-center gap-2 px-3" style={{ fontFamily: form.storefrontChatFontFamily }}>
|
||||
<div className="w-7 h-7 rounded-lg flex items-center justify-center" style={{ backgroundColor: `${form.storefrontChatBrandColor}22` }}>
|
||||
@ -243,14 +304,22 @@ export default function StorefrontChatCustomizeModal({
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div className="px-5 pb-2 text-xs text-red-300">{error}</div>
|
||||
<div className={`px-5 pb-2 text-xs ${isLight ? "text-red-600" : "text-red-300"}`}>{error}</div>
|
||||
) : null}
|
||||
|
||||
<div className="px-5 py-4 border-t border-white/10 flex items-center justify-end gap-2">
|
||||
<div
|
||||
className={`px-5 py-4 flex items-center justify-end gap-2 border-t ${
|
||||
isLight ? "border-slate-200 bg-white" : "border-white/10"
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving || injecting}
|
||||
className="h-10 px-4 rounded-xl bg-white/10 text-slate-200 hover:bg-white/15 disabled:opacity-50 font-bold text-sm flex items-center gap-2"
|
||||
className={`h-10 px-4 rounded-xl disabled:opacity-50 font-bold text-sm flex items-center gap-2 ${
|
||||
isLight
|
||||
? "bg-slate-100 text-slate-800 hover:bg-slate-200 border border-slate-200"
|
||||
: "bg-white/10 text-slate-200 hover:bg-white/15"
|
||||
}`}
|
||||
>
|
||||
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
|
||||
Save
|
||||
|
||||
Loading…
Reference in New Issue
Block a user