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
|
if (typeof value !== "string") return null
|
||||||
const v = value.trim()
|
const v = value.trim()
|
||||||
if (!v) return null
|
if (!v) return null
|
||||||
|
if (v.startsWith("/") && !v.includes("..")) {
|
||||||
|
return v.slice(0, 500)
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(v)
|
const parsed = new URL(v)
|
||||||
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null
|
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: {
|
function buildStorefrontScript(params: {
|
||||||
appBaseUrl: string
|
appBaseUrl: string
|
||||||
chatName: string
|
chatName: string
|
||||||
@ -332,18 +348,33 @@ function buildStorefrontScript(params: {
|
|||||||
btn.style.width = "56px";
|
btn.style.width = "56px";
|
||||||
btn.style.height = "56px";
|
btn.style.height = "56px";
|
||||||
btn.style.borderRadius = "16px";
|
btn.style.borderRadius = "16px";
|
||||||
btn.style.border = "0";
|
|
||||||
btn.style.cursor = "pointer";
|
btn.style.cursor = "pointer";
|
||||||
btn.style.boxShadow = "0 12px 28px rgba(0,0,0,0.20)";
|
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.display = "flex";
|
||||||
btn.style.alignItems = "center";
|
btn.style.alignItems = "center";
|
||||||
btn.style.justifyContent = "center";
|
btn.style.justifyContent = "center";
|
||||||
btn.style.fontFamily = fontFamily;
|
btn.style.fontFamily = fontFamily;
|
||||||
btn.style.fontWeight = "800";
|
btn.style.fontWeight = "800";
|
||||||
btn.style.letterSpacing = "-0.02em";
|
btn.style.letterSpacing = "-0.02em";
|
||||||
|
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.innerHTML = "Chat";
|
||||||
|
}
|
||||||
btn.addEventListener("click", function () { openPanel(); });
|
btn.addEventListener("click", function () { openPanel(); });
|
||||||
container.appendChild(btn);
|
container.appendChild(btn);
|
||||||
}
|
}
|
||||||
@ -447,7 +478,7 @@ async function syncStorefrontTheme(params: {
|
|||||||
tagline: params.tagline,
|
tagline: params.tagline,
|
||||||
brandColor: params.brandColor,
|
brandColor: params.brandColor,
|
||||||
fontFamily: params.fontFamily,
|
fontFamily: params.fontFamily,
|
||||||
logoUrl: params.logoUrl,
|
logoUrl: resolveLogoUrlForInject(appBaseUrl, params.logoUrl),
|
||||||
})
|
})
|
||||||
|
|
||||||
const putAsset = await putThemeAsset(cleanDomain, accessToken, apiVersion, themeId, assetKey, js)
|
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 AddStoreModal from "@/components/AddStoreModal"
|
||||||
import DeleteStoreModal from "@/components/DeleteStoreModal"
|
import DeleteStoreModal from "@/components/DeleteStoreModal"
|
||||||
import StorefrontChatCustomizeModal, { type StorefrontChatCustomization } from "@/components/StorefrontChatCustomizeModal"
|
import StorefrontChatCustomizeModal, { type StorefrontChatCustomization } from "@/components/StorefrontChatCustomizeModal"
|
||||||
|
import { useTheme } from "@/components/ThemeProvider"
|
||||||
|
|
||||||
interface StoreData {
|
interface StoreData {
|
||||||
id: string
|
id: string
|
||||||
@ -38,6 +39,8 @@ export default function StoresPage() {
|
|||||||
const [customizeStoreId, setCustomizeStoreId] = useState<string | null>(null)
|
const [customizeStoreId, setCustomizeStoreId] = useState<string | null>(null)
|
||||||
const [injectingStoreId, setInjectingStoreId] = useState<string | null>(null)
|
const [injectingStoreId, setInjectingStoreId] = useState<string | null>(null)
|
||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false)
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false)
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const isLight = theme === "light"
|
||||||
/*
|
/*
|
||||||
* Alignment Fix Details:
|
* Alignment Fix Details:
|
||||||
* - Use React `createPortal` to render the modal backdrop and content at the end of `document.body`.
|
* - 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) => (
|
{stores.map((store) => (
|
||||||
<div
|
<div
|
||||||
key={store.id}
|
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
|
className={`backdrop-blur-xl p-4 md:p-6 rounded-xl md:rounded-2xl border transition-all group relative ${
|
||||||
? "border-indigo-500/30 bg-indigo-500/10"
|
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]"
|
: "border-white/10 hover:bg-white/[0.07]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between mb-3 md:mb-4">
|
<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">
|
<div
|
||||||
<Store className="w-5 h-5 md:w-6 md:h-6 text-indigo-400" />
|
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>
|
</div>
|
||||||
{store.isActive && (
|
{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" />
|
<Check className="w-2.5 h-2.5 md:w-3 md:h-3" />
|
||||||
Active
|
Active
|
||||||
</span>
|
</span>
|
||||||
@ -279,17 +297,23 @@ export default function StoresPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5 md:space-y-2 mb-3 md:mb-4">
|
<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>
|
<h3 className={`text-base md:text-lg font-black truncate ${isLight ? "text-slate-900" : "text-white"}`}>
|
||||||
<p className="text-xs text-slate-400 font-medium truncate">{store.shopifyDomain}</p>
|
{store.businessName}
|
||||||
<p className="text-[10px] text-slate-500">
|
</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()}
|
Connected {new Date(store.createdAt).toLocaleDateString()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between gap-3 mb-3">
|
<div className="flex items-center justify-between gap-3 mb-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-xs md:text-sm text-white font-bold truncate">Storefront Chat (Customers)</p>
|
<p className={`text-xs md:text-sm font-bold truncate ${isLight ? "text-slate-900" : "text-white"}`}>
|
||||||
<p className="text-[10px] md:text-[11px] text-slate-400">
|
Storefront Chat (Customers)
|
||||||
|
</p>
|
||||||
|
<p className={`text-[10px] md:text-[11px] ${isLight ? "text-slate-600" : "text-slate-400"}`}>
|
||||||
Show chat widget on the storefront
|
Show chat widget on the storefront
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -313,7 +337,11 @@ export default function StoresPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleSwitchStore(store.id)}
|
onClick={() => handleSwitchStore(store.id)}
|
||||||
disabled={switching === 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 ? (
|
{switching === store.id ? (
|
||||||
<>
|
<>
|
||||||
@ -328,18 +356,28 @@ export default function StoresPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => openCustomize(store.id)}
|
onClick={() => openCustomize(store.id)}
|
||||||
disabled={!store.storefrontChatEnabled || injectingStoreId === 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"}
|
{injectingStoreId === store.id ? "Injecting..." : "Customize"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDeleteClick(store)}
|
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"
|
title="Delete store"
|
||||||
aria-label="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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -179,16 +179,6 @@ export default function StorefrontChatWidgetPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-screen h-screen overflow-hidden bg-white text-slate-900 flex flex-col">
|
<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">
|
<div className="flex-1 overflow-y-auto p-4 bg-gradient-to-b from-white to-slate-50">
|
||||||
{sendError ? (
|
{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">
|
<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 { useEffect, useMemo, useState } from "react"
|
||||||
import { AnimatePresence, motion } from "framer-motion"
|
import { AnimatePresence, motion } from "framer-motion"
|
||||||
import { Loader2, Palette, Save, Upload, WandSparkles, X } from "lucide-react"
|
import { Loader2, Palette, Save, Upload, WandSparkles, X } from "lucide-react"
|
||||||
|
import { useTheme } from "@/components/ThemeProvider"
|
||||||
|
|
||||||
export type StorefrontChatCustomization = {
|
export type StorefrontChatCustomization = {
|
||||||
storefrontChatName: string
|
storefrontChatName: string
|
||||||
@ -37,6 +38,9 @@ export default function StorefrontChatCustomizeModal({
|
|||||||
onSave,
|
onSave,
|
||||||
onInject,
|
onInject,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const isLight = theme === "light"
|
||||||
|
|
||||||
const [form, setForm] = useState<StorefrontChatCustomization>(initialValues)
|
const [form, setForm] = useState<StorefrontChatCustomization>(initialValues)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [injecting, setInjecting] = useState(false)
|
const [injecting, setInjecting] = useState(false)
|
||||||
@ -122,68 +126,105 @@ export default function StorefrontChatCustomizeModal({
|
|||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
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}
|
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
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 12, scale: 0.98 }}
|
initial={{ opacity: 0, y: 12, scale: 0.98 }}
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
exit={{ opacity: 0, y: 12, scale: 0.98 }}
|
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 flex items-center justify-between border-b ${
|
||||||
|
isLight ? "border-slate-200 bg-white" : "border-white/10"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div className="px-5 py-4 border-b border-white/10 flex items-center justify-between">
|
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-white font-black text-lg">Customize Storefront Chat</p>
|
<p className={`font-black text-lg ${isLight ? "text-slate-900" : "text-white"}`}>
|
||||||
<p className="text-xs text-slate-400 truncate">{storeName}</p>
|
Customize Storefront Chat
|
||||||
|
</p>
|
||||||
|
<p className={`text-xs truncate ${isLight ? "text-slate-500" : "text-slate-400"}`}>{storeName}</p>
|
||||||
</div>
|
</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" />
|
<X className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-5 grid md:grid-cols-2 gap-4">
|
<div className="p-5 grid md:grid-cols-2 gap-4">
|
||||||
<label className="block">
|
<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
|
<input
|
||||||
value={form.storefrontChatName}
|
value={form.storefrontChatName}
|
||||||
onChange={(e) => update("storefrontChatName", e.target.value)}
|
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>
|
||||||
|
|
||||||
<label className="block">
|
<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
|
<input
|
||||||
value={form.storefrontChatTagline}
|
value={form.storefrontChatTagline}
|
||||||
onChange={(e) => update("storefrontChatTagline", e.target.value)}
|
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>
|
||||||
|
|
||||||
<label className="block">
|
<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">
|
<div className="mt-1 flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
value={form.storefrontChatBrandColor}
|
value={form.storefrontChatBrandColor}
|
||||||
onChange={(e) => update("storefrontChatBrandColor", e.target.value)}
|
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
|
<input
|
||||||
value={form.storefrontChatBrandColor}
|
value={form.storefrontChatBrandColor}
|
||||||
onChange={(e) => update("storefrontChatBrandColor", e.target.value)}
|
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>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label className="block">
|
<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
|
<select
|
||||||
value={form.storefrontChatFontFamily}
|
value={form.storefrontChatFontFamily}
|
||||||
onChange={(e) => update("storefrontChatFontFamily", e.target.value)}
|
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) => (
|
{FONT_OPTIONS.map((opt) => (
|
||||||
<option key={opt.value} value={opt.value} className="text-slate-900">
|
<option key={opt.value} value={opt.value} className="text-slate-900">
|
||||||
@ -194,9 +235,17 @@ export default function StorefrontChatCustomizeModal({
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="block md:col-span-2">
|
<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">
|
<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" />}
|
{uploadingLogo ? <Loader2 className="w-4 h-4 animate-spin" /> : <Upload className="w-4 h-4" />}
|
||||||
Upload image
|
Upload image
|
||||||
<input
|
<input
|
||||||
@ -214,14 +263,26 @@ export default function StorefrontChatCustomizeModal({
|
|||||||
value={form.storefrontChatLogoUrl}
|
value={form.storefrontChatLogoUrl}
|
||||||
onChange={(e) => update("storefrontChatLogoUrl", e.target.value)}
|
onChange={(e) => update("storefrontChatLogoUrl", e.target.value)}
|
||||||
placeholder="Or paste logo URL"
|
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>
|
</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>
|
||||||
|
|
||||||
<div className="md:col-span-2 rounded-xl border border-white/10 bg-white/5 p-3">
|
<div
|
||||||
<p className="text-[11px] font-bold text-slate-400 mb-2">Preview</p>
|
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="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="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` }}>
|
<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>
|
</div>
|
||||||
|
|
||||||
{error ? (
|
{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}
|
) : 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
|
<button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={saving || injecting}
|
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" />}
|
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
|
||||||
Save
|
Save
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user