UI fixes and mobile responsive

This commit is contained in:
root 2026-02-14 17:28:19 +05:00
parent 59d90ed807
commit 4ad0ed7dbc
11 changed files with 1473 additions and 994 deletions

View File

@ -19,8 +19,12 @@ import {
X, X,
CheckCircle2, CheckCircle2,
ChevronLeft, ChevronLeft,
ChevronRight ChevronRight,
Plus
} from "lucide-react" } from "lucide-react"
import { createPortal } from "react-dom"
import { motion, AnimatePresence } from "framer-motion"
import { useTheme } from "@/components/ThemeProvider"
interface ShopifyProduct { interface ShopifyProduct {
id: number id: number
@ -79,7 +83,15 @@ export default function InventoryPage() {
const [statusFilter, setStatusFilter] = useState<string>("all") const [statusFilter, setStatusFilter] = useState<string>("all")
const [vendorFilter, setVendorFilter] = useState<string>("all") const [vendorFilter, setVendorFilter] = useState<string>("all")
const [showFilters, setShowFilters] = useState(false) const [showFilters, setShowFilters] = useState(false)
const menuRefs = useRef<{ [key: number]: HTMLDivElement | null }>({}) const [mounted, setMounted] = useState(false)
const [menuRect, setMenuRect] = useState<{ top: number; left: number } | null>(null)
const menuRefs = useRef<{ [key: number]: HTMLButtonElement | null }>({})
const { theme } = useTheme()
const isLight = theme === "light"
useEffect(() => {
setMounted(true)
}, [])
useEffect(() => { useEffect(() => {
const fetchStoreInfo = async () => { const fetchStoreInfo = async () => {
@ -201,8 +213,19 @@ export default function InventoryPage() {
} }
}, [openMenuId]) }, [openMenuId])
const handleMenuToggle = (productId: number) => { const handleMenuToggle = (productId: number, e: React.MouseEvent) => {
setOpenMenuId(openMenuId === productId ? null : productId) e.stopPropagation()
if (openMenuId === productId) {
setOpenMenuId(null)
setMenuRect(null)
} else {
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
setMenuRect({
top: rect.bottom + window.scrollY,
left: rect.right + window.scrollX
})
setOpenMenuId(productId)
}
} }
const handleViewProduct = (product: ShopifyProduct) => { const handleViewProduct = (product: ShopifyProduct) => {
@ -336,7 +359,8 @@ export default function InventoryPage() {
const uniqueVendors = Array.from(new Set(products.map(p => p.vendor))).sort() const uniqueVendors = Array.from(new Set(products.map(p => p.vendor))).sort()
return ( return (
<div className="flex flex-col flex-1 min-h-0 space-y-4 md:space-y-6"> <>
<div className="flex flex-col h-full min-h-0 space-y-4 md:space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 flex-shrink-0"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 flex-shrink-0">
<div> <div>
<h1 className="text-2xl md:text-3xl font-extrabold text-white tracking-tight">Inventory</h1> <h1 className="text-2xl md:text-3xl font-extrabold text-white tracking-tight">Inventory</h1>
@ -352,7 +376,7 @@ export default function InventoryPage() {
</div> </div>
</div> </div>
<div className="bg-white/5 backdrop-blur-xl rounded-xl md:rounded-2xl border border-white/10 flex flex-col flex-1 min-h-0 relative"> <div className="bg-white/5 backdrop-blur-xl rounded-xl md:rounded-2xl border border-white/10 flex flex-col flex-1 min-h-0 relative overflow-hidden">
<div className="p-4 md:p-6 border-b border-white/10 flex flex-col sm:flex-row gap-3 md:gap-4 justify-between bg-white/5 flex-shrink-0"> <div className="p-4 md:p-6 border-b border-white/10 flex flex-col sm:flex-row gap-3 md:gap-4 justify-between bg-white/5 flex-shrink-0">
<div className="relative flex-1 min-w-0"> <div className="relative flex-1 min-w-0">
<Search className="absolute left-3 md:left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" /> <Search className="absolute left-3 md:left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
@ -521,51 +545,17 @@ export default function InventoryPage() {
})()} })()}
</td> </td>
<td className="px-4 md:px-8 py-3 md:py-4 text-right"> <td className="px-4 md:px-8 py-3 md:py-4 text-right">
<div className="relative" ref={(el) => { menuRefs.current[product.id] = el }}> <div className="relative">
<button <button
onClick={() => handleMenuToggle(product.id)} ref={(el) => { menuRefs.current[product.id] = el }}
className="p-2 hover:bg-white/10 rounded-lg text-slate-400 hover:text-indigo-400 transition-all relative z-10" onClick={(e) => handleMenuToggle(product.id, e)}
className={`p-2 rounded-lg transition-all relative z-10 ${openMenuId === product.id
? (isLight ? "bg-indigo-100 text-indigo-600" : "bg-white/10 text-indigo-400")
: (isLight ? "text-slate-400 hover:text-slate-600 hover:bg-slate-100" : "text-slate-400 hover:text-indigo-400 hover:bg-white/10")
}`}
> >
<MoreHorizontal className="w-4 h-4 md:w-5 md:h-5" /> <MoreHorizontal className="w-4 h-4 md:w-5 md:h-5" />
</button> </button>
{openMenuId === product.id && (
<div className="absolute right-0 top-full mt-1 bg-white/95 backdrop-blur-xl border border-white/20 rounded-xl shadow-xl overflow-hidden z-50 min-w-[180px]">
<button
onClick={() => handleViewProduct(product)}
className="w-full px-4 py-2.5 text-left text-sm font-medium text-slate-700 hover:bg-indigo-500 hover:text-white transition-colors flex items-center gap-2"
>
<Eye className="w-4 h-4" />
View in Shopify
</button>
<button
onClick={() => handleCopyProductId(product.id)}
className="w-full px-4 py-2.5 text-left text-sm font-medium text-slate-700 hover:bg-indigo-500 hover:text-white transition-colors flex items-center gap-2"
>
<Copy className="w-4 h-4" />
Copy Product ID
</button>
<button
onClick={() => handleRefreshProduct()}
className="w-full px-4 py-2.5 text-left text-sm font-medium text-slate-700 hover:bg-indigo-500 hover:text-white transition-colors flex items-center gap-2"
>
<RefreshCw className="w-4 h-4" />
Refresh Product
</button>
<div className="border-t border-white/10 my-1"></div>
{storeDomain && (
<a
href={`https://${storeDomain}/products/${product.handle}`}
target="_blank"
rel="noopener noreferrer"
onClick={() => setOpenMenuId(null)}
className="w-full px-4 py-2.5 text-left text-sm font-medium text-slate-700 hover:bg-indigo-500 hover:text-white transition-colors flex items-center gap-2"
>
<ExternalLink className="w-4 h-4" />
View on Storefront
</a>
)}
</div>
)}
</div> </div>
</td> </td>
</tr> </tr>
@ -637,5 +627,134 @@ export default function InventoryPage() {
</div> </div>
)} )}
</div> </div>
{mounted && createPortal(
<AnimatePresence>
{openMenuId !== null && menuRect !== null && (
<>
<div
className="fixed inset-0 z-[100]"
onClick={() => {
setOpenMenuId(null)
setMenuRect(null)
}}
/>
<ActionsMenuContent
openMenuId={openMenuId}
menuRect={menuRect}
isLight={isLight}
products={products}
storeDomain={storeDomain}
handleViewProduct={handleViewProduct}
handleCopyProductId={handleCopyProductId}
handleRefreshProduct={handleRefreshProduct}
onClose={() => {
setOpenMenuId(null)
setMenuRect(null)
}}
/>
</>
)}
</AnimatePresence>,
document.body
)}
</>
) )
} }
function ActionsMenuContent({
openMenuId,
menuRect,
isLight,
products,
storeDomain,
handleViewProduct,
handleCopyProductId,
handleRefreshProduct,
onClose
}: {
openMenuId: number;
menuRect: { top: number; left: number };
isLight: boolean;
products: ShopifyProduct[];
storeDomain: string | null;
handleViewProduct: (product: ShopifyProduct) => void;
handleCopyProductId: (productId: number) => void;
handleRefreshProduct: () => void;
onClose: () => void;
}) {
const product = products.find(p => p.id === openMenuId);
if (!product) return null;
return (
<motion.div
initial={{ opacity: 0, scale: 0.95, y: -10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: -10 }}
key="inventory-actions-menu"
style={{
position: 'absolute',
top: menuRect.top + 4,
left: menuRect.left - 180,
zIndex: 101,
width: 180
}}
className={`rounded-xl shadow-2xl border overflow-hidden ${isLight
? "bg-white border-slate-200"
: "bg-slate-900 border-white/10"
}`}
>
<div className="flex flex-col">
<button
onClick={() => handleViewProduct(product)}
className={`w-full px-4 py-2.5 text-left text-sm font-medium flex items-center gap-2 transition-colors ${isLight
? "text-slate-700 hover:bg-slate-50 hover:text-indigo-600"
: "text-slate-300 hover:bg-white/5 hover:text-white"
}`}
>
<Eye className="w-4 h-4" />
View in Shopify
</button>
<button
onClick={() => handleCopyProductId(openMenuId)}
className={`w-full px-4 py-2.5 text-left text-sm font-medium flex items-center gap-2 transition-colors ${isLight
? "text-slate-700 hover:bg-slate-50 hover:text-indigo-600"
: "text-slate-300 hover:bg-white/5 hover:text-white"
}`}
>
<Copy className="w-4 h-4" />
Copy Product ID
</button>
<button
onClick={() => handleRefreshProduct()}
className={`w-full px-4 py-2.5 text-left text-sm font-medium flex items-center gap-2 transition-colors ${isLight
? "text-slate-700 hover:bg-slate-50 hover:text-indigo-600"
: "text-slate-300 hover:bg-white/5 hover:text-white"
}`}
>
<RefreshCw className="w-4 h-4" />
Refresh Product
</button>
{storeDomain && product.handle && (
<>
<div className={`border-t ${isLight ? "border-slate-100" : "border-white/5"}`} />
<a
href={`https://${storeDomain}/products/${product.handle}`}
target="_blank"
rel="noopener noreferrer"
onClick={onClose}
className={`w-full px-4 py-2.5 text-left text-sm font-medium flex items-center gap-2 transition-colors ${isLight
? "text-slate-700 hover:bg-slate-50 hover:text-indigo-600"
: "text-slate-300 hover:bg-white/5 hover:text-white"
}`}
>
<ExternalLink className="w-4 h-4" />
View on Storefront
</a>
</>
)}
</div>
</motion.div>
);
}

View File

@ -300,14 +300,16 @@ export default function EmailTemplatesPage() {
</div> </div>
</div> </div>
{mounted && createPortal(
<AnimatePresence> <AnimatePresence>
{editing && mounted && createPortal( {editing && (
<div className="fixed inset-0 z-[100] flex items-center justify-center pointer-events-none p-4"> <div className="fixed inset-0 z-[100] flex items-center justify-center pointer-events-none p-4">
{/* Backdrop */} {/* Backdrop */}
<motion.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
key="backdrop"
onClick={() => !saving && setEditing(null)} onClick={() => !saving && setEditing(null)}
className={`absolute inset-0 pointer-events-auto backdrop-blur-sm ${isLight ? "bg-black/40" : "bg-black/60" className={`absolute inset-0 pointer-events-auto backdrop-blur-sm ${isLight ? "bg-black/40" : "bg-black/60"
}`} }`}
@ -318,6 +320,7 @@ export default function EmailTemplatesPage() {
initial={{ scale: 0.95, opacity: 0, y: 20 }} initial={{ scale: 0.95, opacity: 0, y: 20 }}
animate={{ scale: 1, opacity: 1, y: 0 }} animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.95, opacity: 0, y: 20 }} exit={{ scale: 0.95, opacity: 0, y: 20 }}
key="modal"
className={`relative flex w-full max-h-[90vh] max-w-3xl flex-col rounded-2xl border shadow-2xl pointer-events-auto ${isLight className={`relative flex w-full max-h-[90vh] max-w-3xl flex-col rounded-2xl border shadow-2xl pointer-events-auto ${isLight
? "bg-white text-slate-900 border-slate-200" ? "bg-white text-slate-900 border-slate-200"
: "bg-slate-900 text-slate-50 border-white/10" : "bg-slate-900 text-slate-50 border-white/10"
@ -562,10 +565,11 @@ export default function EmailTemplatesPage() {
</div> </div>
</div> </div>
</motion.div> </motion.div>
</div>, </div>
)}
</AnimatePresence>,
document.body document.body
)} )}
</AnimatePresence>
</div> </div>
) )
} }

View File

@ -4,14 +4,18 @@ import { useEffect, useState } from "react"
import { DocumentsSection } from "@/components/kb/DocumentsSection" import { DocumentsSection } from "@/components/kb/DocumentsSection"
import { FaqSection } from "@/components/kb/FaqSection" import { FaqSection } from "@/components/kb/FaqSection"
import { AskSection } from "@/components/kb/AskSection" import { AskSection } from "@/components/kb/AskSection"
import { useTheme } from "@/components/ThemeProvider"
import { cn } from "@/lib/utils"
export default function KnowledgeBasePage() { export default function KnowledgeBasePage() {
const { theme } = useTheme()
const isLight = theme === "light"
const [activeStoreId, setActiveStoreId] = useState<string | null>(null) const [activeStoreId, setActiveStoreId] = useState<string | null>(null)
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
;(async () => { ; (async () => {
try { try {
const res = await fetch("/api/stores") const res = await fetch("/api/stores")
if (!res.ok) return if (!res.ok) return
@ -46,10 +50,10 @@ export default function KnowledgeBasePage() {
<div className="flex flex-col h-full space-y-6"> <div className="flex flex-col h-full space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-white"> <h1 className={cn("text-2xl font-bold", isLight ? "text-slate-900" : "text-white")}>
Knowledge Base Knowledge Base
</h1> </h1>
<p className="text-sm text-slate-400"> <p className={cn("text-sm", isLight ? "text-slate-500" : "text-slate-400")}>
Centralize your store&apos;s documents and FAQs so JUNO can Centralize your store&apos;s documents and FAQs so JUNO can
answer customer and vendor questions. answer customer and vendor questions.
</p> </p>
@ -59,10 +63,10 @@ export default function KnowledgeBasePage() {
{!hasStore ? ( {!hasStore ? (
<div className="flex-1 flex items-center justify-center"> <div className="flex-1 flex items-center justify-center">
<div className="text-center max-w-md"> <div className="text-center max-w-md">
<h2 className="text-lg font-semibold text-white mb-2"> <h2 className={cn("text-lg font-semibold mb-2", isLight ? "text-slate-900" : "text-white")}>
No active store selected No active store selected
</h2> </h2>
<p className="text-sm text-slate-400"> <p className={cn("text-sm", isLight ? "text-slate-500" : "text-slate-400")}>
Select a store from the top bar to manage its knowledge Select a store from the top bar to manage its knowledge
base. base.
</p> </p>
@ -70,37 +74,46 @@ export default function KnowledgeBasePage() {
</div> </div>
) : ( ) : (
<> <>
<div className="flex items-center space-x-2 border-b border-white/10"> <div className={cn("flex items-center space-x-2 border-b", isLight ? "border-slate-100" : "border-white/10")}>
<button <button
type="button" type="button"
onClick={() => setActiveTab("documents")} onClick={() => setActiveTab("documents")}
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors ${ className={cn(
"px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors",
activeTab === "documents" activeTab === "documents"
? "border-indigo-500 text-white" ? "border-indigo-500 text-indigo-600"
: isLight
? "border-transparent text-slate-500 hover:text-slate-900 hover:border-slate-300"
: "border-transparent text-slate-400 hover:text-white hover:border-slate-500" : "border-transparent text-slate-400 hover:text-white hover:border-slate-500"
}`} )}
> >
Documents Documents
</button> </button>
<button <button
type="button" type="button"
onClick={() => setActiveTab("faqs")} onClick={() => setActiveTab("faqs")}
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors ${ className={cn(
"px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors",
activeTab === "faqs" activeTab === "faqs"
? "border-indigo-500 text-white" ? "border-indigo-500 text-indigo-600"
: isLight
? "border-transparent text-slate-500 hover:text-slate-900 hover:border-slate-300"
: "border-transparent text-slate-400 hover:text-white hover:border-slate-500" : "border-transparent text-slate-400 hover:text-white hover:border-slate-500"
}`} )}
> >
FAQs FAQs
</button> </button>
<button <button
type="button" type="button"
onClick={() => setActiveTab("ask")} onClick={() => setActiveTab("ask")}
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors ${ className={cn(
"px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors",
activeTab === "ask" activeTab === "ask"
? "border-indigo-500 text-white" ? "border-indigo-500 text-indigo-600"
: isLight
? "border-transparent text-slate-500 hover:text-slate-900 hover:border-slate-300"
: "border-transparent text-slate-400 hover:text-white hover:border-slate-500" : "border-transparent text-slate-400 hover:text-white hover:border-slate-500"
}`} )}
> >
Ask Ask
</button> </button>

View File

@ -62,14 +62,13 @@ export default function JunoEngineLayout({
<button <button
key={tab.href} key={tab.href}
onClick={() => router.push(tab.href)} onClick={() => router.push(tab.href)}
className={`pb-3 md:pb-4 px-3 md:px-4 font-bold transition-all duration-200 ease-out whitespace-nowrap text-sm md:text-base flex items-center gap-2 relative ${ className={`pb-3 md:pb-4 px-3 md:px-4 font-bold transition-all duration-200 ease-out whitespace-nowrap text-sm md:text-base flex items-center gap-2 relative ${isActive
isActive
? "text-indigo-400 border-b-2 border-indigo-500" ? "text-indigo-400 border-b-2 border-indigo-500"
: "text-slate-400 hover:text-white" : "text-slate-400 hover:text-white"
}`} }`}
> >
<Icon className="w-4 h-4 transition-transform duration-200" /> <Icon className="w-4 h-5 md:w-5 md:h-5 transition-transform duration-200" />
{tab.label} <span className="hidden sm:inline">{tab.label}</span>
</button> </button>
) )
})} })}

View File

@ -3,6 +3,7 @@
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useParams, useRouter } from "next/navigation" import { useParams, useRouter } from "next/navigation"
import { ArrowLeft, Ticket, Loader2, MoreHorizontal } from "lucide-react" import { ArrowLeft, Ticket, Loader2, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
type TicketStatus = "open" | "pending" | "resolved" | "closed" type TicketStatus = "open" | "pending" | "resolved" | "closed"
@ -27,9 +28,13 @@ interface TicketData {
messages: TicketMessage[] messages: TicketMessage[]
} }
import { useTheme } from "@/components/ThemeProvider"
export default function TicketDetailPage() { export default function TicketDetailPage() {
const params = useParams<{ ticketId: string }>() const params = useParams<{ ticketId: string }>()
const router = useRouter() const router = useRouter()
const { theme } = useTheme()
const isLight = theme === "light"
const [ticket, setTicket] = useState<TicketData | null>(null) const [ticket, setTicket] = useState<TicketData | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
@ -69,33 +74,46 @@ export default function TicketDetailPage() {
const getStatusStyles = (status: TicketStatus | string) => { const getStatusStyles = (status: TicketStatus | string) => {
switch (status) { switch (status) {
case "open": case "open":
return "bg-blue-500/20 text-blue-400" return isLight ? "bg-blue-100 text-blue-700 border-blue-200" : "bg-blue-500/20 text-blue-400 border-blue-500/20"
case "pending": case "pending":
return "bg-yellow-500/20 text-yellow-400" return isLight ? "bg-amber-100 text-amber-700 border-amber-200" : "bg-yellow-500/20 text-yellow-400 border-yellow-500/20"
case "resolved": case "resolved":
return "bg-emerald-500/20 text-emerald-400" return isLight ? "bg-emerald-100 text-emerald-700 border-emerald-200" : "bg-emerald-500/20 text-emerald-400 border-emerald-500/20"
case "closed": case "closed":
return "bg-slate-500/20 text-slate-400" return isLight ? "bg-slate-100 text-slate-700 border-slate-200" : "bg-slate-500/20 text-slate-400 border-slate-500/20"
default: default:
return "bg-white/10 text-slate-400" return isLight ? "bg-slate-50 text-slate-600 border-slate-200" : "bg-white/10 text-slate-400 border-white/10"
} }
} }
return ( return (
<div className="h-full flex flex-col space-y-4 md:space-y-6"> <div className={cn("h-full flex flex-col min-h-0", isLight ? "bg-slate-50/50" : "")}>
<div className="w-full px-4 md:px-8 py-4 md:py-6 flex flex-col gap-6 min-h-0 h-full">
{/* Header */} {/* Header */}
<div className="flex items-center gap-3 flex-shrink-0"> <div className="flex items-center gap-4 flex-shrink-0">
<button <button
onClick={() => router.push("/dashboard/juno-engine/tickets")} onClick={() => router.push("/dashboard/juno-engine/tickets")}
className="inline-flex items-center justify-center w-9 h-9 rounded-full border border-white/10 bg-white/5 text-slate-300 hover:bg-white/10 hover:text-white transition-all" className={cn(
"inline-flex items-center justify-center w-10 h-10 rounded-full border transition-all shadow-sm",
isLight
? "border-slate-200 bg-white text-slate-600 hover:bg-slate-50 hover:text-indigo-600"
: "border-white/10 bg-white/5 text-slate-300 hover:bg-white/10 hover:text-white"
)}
title="Go back"
> >
<ArrowLeft className="w-4 h-4" /> <ArrowLeft className="w-5 h-5" />
</button> </button>
<div> <div className="min-w-0">
<h1 className="text-2xl md:text-3xl font-extrabold text-white tracking-tight"> <h1 className={cn(
"text-2xl md:text-3xl font-black tracking-tight truncate",
isLight ? "text-slate-900" : "text-white"
)}>
{ticket?.subject || "Ticket"} {ticket?.subject || "Ticket"}
</h1> </h1>
<p className="text-sm md:text-base text-slate-400 mt-1"> <p className={cn(
"text-sm mt-0.5 truncate font-medium",
isLight ? "text-slate-500" : "text-slate-400"
)}>
{ticket?.customerEmail || "Full automated email history for this customer."} {ticket?.customerEmail || "Full automated email history for this customer."}
</p> </p>
</div> </div>
@ -111,23 +129,31 @@ export default function TicketDetailPage() {
<p className="font-medium">{error || "Ticket not found."}</p> <p className="font-medium">{error || "Ticket not found."}</p>
</div> </div>
) : ( ) : (
<div className="flex-1 overflow-y-auto min-h-0"> <div className="flex-1 flex flex-col min-h-0 gap-8">
<div className="max-w-4xl mx-auto px-2 md:px-0 space-y-4">
{/* Meta badges */} {/* Meta badges */}
<div className="flex flex-wrap items-center justify-between gap-3 text-xs"> <div className="flex flex-wrap items-center justify-between gap-4 text-xs flex-shrink-0">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-3">
<span <span
className={`px-2.5 py-1 rounded-full text-[10px] font-black uppercase tracking-wider ${getStatusStyles( className={cn(
ticket.status as TicketStatus "px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-wider shadow-sm border",
)}`} getStatusStyles(ticket.status as TicketStatus)
)}
> >
{ticket.status} {ticket.status}
</span> </span>
<span className="px-2.5 py-1 rounded-full text-[10px] font-black uppercase tracking-wider bg-white/5 text-slate-300 border border-white/10"> <span className={cn(
"px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-wider shadow-sm border",
isLight
? "bg-white border-slate-200 text-slate-600"
: "bg-white/5 border-white/10 text-slate-300"
)}>
{ticket.messagesCount} messages {ticket.messagesCount} messages
</span> </span>
</div> </div>
<div className="text-[11px] text-slate-500"> <div className={cn(
"text-[11px] font-bold",
isLight ? "text-slate-400" : "text-slate-500"
)}>
Updated{" "} Updated{" "}
{new Date(ticket.updatedAt).toLocaleString(undefined, { {new Date(ticket.updatedAt).toLocaleString(undefined, {
dateStyle: "medium", dateStyle: "medium",
@ -136,8 +162,8 @@ export default function TicketDetailPage() {
</div> </div>
</div> </div>
{/* Conversation timeline - chat style */} {/* Conversation timeline - Full width corners chat style */}
<div className="pt-2 space-y-4"> <div className="flex-1 overflow-y-auto min-h-0 pr-4 -mr-4 space-y-8 pb-10 custom-scrollbar">
{ticket.messages.map((message) => { {ticket.messages.map((message) => {
const isIncoming = const isIncoming =
message.trigger === "GMAIL_INCOMING" || message.status === "RECEIVED" message.trigger === "GMAIL_INCOMING" || message.status === "RECEIVED"
@ -152,33 +178,54 @@ export default function TicketDetailPage() {
return ( return (
<div <div
key={message.id} key={message.id}
className={`flex w-full ${isIncoming ? "justify-start" : "justify-end"}`} className={cn(
"flex w-full animate-in fade-in slide-in-from-bottom-4 duration-500",
isIncoming ? "justify-start" : "justify-end"
)}
> >
<div <div
className={`w-full sm:max-w-[88%] rounded-2xl px-4 py-3 text-xs shadow-sm flex flex-col gap-1 className={cn(
${ "w-full sm:max-w-[70%] lg:max-w-[60%] xl:max-w-[50%] rounded-2xl p-6 text-[14px] shadow-xl flex flex-col gap-3 relative transition-all border",
isIncoming isIncoming
? "bg-slate-900/70 border border-white/10 rounded-bl-sm" ? isLight
: "bg-indigo-500/90 border border-indigo-300/60 text-white rounded-br-sm" ? "bg-white border-slate-200 text-slate-700 rounded-bl-none shadow-slate-200/50"
}`} : "bg-slate-800/40 border-white/10 text-slate-200 rounded-bl-none backdrop-blur-md shadow-black/20"
: isLight
? "bg-indigo-600 border-indigo-500 text-white rounded-br-none shadow-indigo-100"
: "bg-indigo-600 border-indigo-400/30 text-white rounded-br-none shadow-indigo-900/20"
)}
> >
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-6 mb-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-3">
<span className="text-[10px] font-semibold uppercase tracking-wide opacity-80"> <span className={cn(
"text-[11px] font-black uppercase tracking-widest",
isIncoming
? (isLight ? "text-indigo-600" : "text-indigo-400")
: "text-indigo-100"
)}>
{isIncoming ? "Customer" : "JUNOHUB"} {isIncoming ? "Customer" : "JUNOHUB"}
</span> </span>
<span <span
className={`px-2 py-0.5 rounded-full text-[9px] font-semibold tracking-wide className={cn(
${ "px-2.5 py-0.5 rounded-md text-[9px] font-black tracking-widest uppercase border",
isIncoming isIncoming
? "bg-slate-800/80 text-slate-200 border border-white/10" ? isLight
: "bg-indigo-600/90 text-indigo-50 border border-indigo-300/70" ? "bg-slate-100 border-slate-200 text-slate-500"
}`} : "bg-slate-700/50 text-slate-400 border-white/5"
: isLight
? "bg-indigo-800/20 border-indigo-400/30 text-indigo-100"
: "bg-indigo-700/50 text-indigo-50 border-indigo-400/30"
)}
> >
{message.status} {message.status}
</span> </span>
</div> </div>
<span className="text-[10px] text-slate-400 whitespace-nowrap"> <span className={cn(
"text-[10px] whitespace-nowrap font-bold",
isIncoming
? (isLight ? "text-slate-400" : "text-slate-500")
: "text-indigo-200"
)}>
{new Date(message.sentAt).toLocaleTimeString(undefined, { {new Date(message.sentAt).toLocaleTimeString(undefined, {
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
@ -188,9 +235,12 @@ export default function TicketDetailPage() {
{fullText && ( {fullText && (
<p <p
className={`mt-0.5 leading-snug whitespace-pre-wrap break-words ${ className={cn(
isIncoming ? "text-slate-200" : "text-indigo-50" "leading-relaxed whitespace-pre-wrap break-words font-medium",
}`} isIncoming
? (isLight ? "text-slate-600" : "text-slate-200")
: "text-white"
)}
> >
{displayText} {displayText}
</p> </p>
@ -205,9 +255,12 @@ export default function TicketDetailPage() {
[message.id]: !isExpanded, [message.id]: !isExpanded,
})) }))
} }
className={`self-end mt-1 text-[10px] font-medium underline underline-offset-2 ${ className={cn(
isIncoming ? "text-slate-300" : "text-indigo-100" "self-start mt-2 text-[10px] font-black uppercase tracking-widest hover:underline transition-all",
}`} isIncoming
? (isLight ? "text-indigo-600" : "text-indigo-400")
: "text-indigo-100"
)}
> >
{isExpanded ? "Show less" : "Show more"} {isExpanded ? "Show less" : "Show more"}
</button> </button>
@ -218,15 +271,15 @@ export default function TicketDetailPage() {
})} })}
{ticket.messages.length === 0 && ( {ticket.messages.length === 0 && (
<p className="text-[11px] text-slate-500 text-center"> <div className="flex flex-col items-center justify-center py-20 text-slate-500">
No messages logged yet for this ticket. <Ticket className="w-12 h-12 mb-4 opacity-10" />
</p> <p className="text-sm font-medium">No messages logged yet for this ticket.</p>
)}
</div> </div>
)}
</div> </div>
</div> </div>
)} )}
</div> </div>
</div>
) )
} }

View File

@ -7,7 +7,9 @@ import {
ShoppingCart, ShoppingCart,
Users, Users,
ArrowUpRight, ArrowUpRight,
Loader2 Loader2,
Calendar,
ChevronDown
} from "lucide-react" } from "lucide-react"
import { import {
XAxis, XAxis,
@ -18,8 +20,14 @@ import {
AreaChart, AreaChart,
Area Area
} from "recharts" } from "recharts"
import { useTheme } from "@/components/ThemeProvider"
import { cn } from "@/lib/utils"
type TimeRange = "7days" | "30days" | "1year"
export default function DashboardPage() { export default function DashboardPage() {
const { theme } = useTheme()
const isLight = theme === "light"
const [stats, setStats] = useState([ const [stats, setStats] = useState([
{ label: "Total Sales", value: "$0.00", icon: TrendingUp, color: "text-indigo-600", bg: "bg-indigo-100" }, { label: "Total Sales", value: "$0.00", icon: TrendingUp, color: "text-indigo-600", bg: "bg-indigo-100" },
{ label: "Active Products", value: "0", icon: Package, color: "text-emerald-600", bg: "bg-emerald-100" }, { label: "Active Products", value: "0", icon: Package, color: "text-emerald-600", bg: "bg-emerald-100" },
@ -28,10 +36,11 @@ export default function DashboardPage() {
]) ])
const [chartData, setChartData] = useState<{ date: string; amount: number }[]>([]) const [chartData, setChartData] = useState<{ date: string; amount: number }[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [timeRange, setTimeRange] = useState<TimeRange>("7days")
useEffect(() => {
const fetchDashboardData = async () => { const fetchDashboardData = async () => {
try { try {
setLoading(true)
const [productsRes, ordersRes, customersRes] = await Promise.all([ const [productsRes, ordersRes, customersRes] = await Promise.all([
fetch("/api/shopify/inventory").catch(() => ({ ok: false, json: async () => ({ products: [], message: "Failed to fetch products" }) })), fetch("/api/shopify/inventory").catch(() => ({ ok: false, json: async () => ({ products: [], message: "Failed to fetch products" }) })),
fetch("/api/shopify/orders").catch(() => ({ ok: false, json: async () => ({ orders: [], message: "Failed to fetch orders" }) })), fetch("/api/shopify/orders").catch(() => ({ ok: false, json: async () => ({ orders: [], message: "Failed to fetch orders" }) })),
@ -44,30 +53,50 @@ export default function DashboardPage() {
customersRes.json().catch(() => ({ customers: [] })) customersRes.json().catch(() => ({ customers: [] }))
]) ])
// Check for error messages
if (productsData.message && !productsRes.ok) {
console.warn("Products fetch error:", productsData.message)
}
if (ordersData.message && !ordersRes.ok) {
console.warn("Orders fetch error:", ordersData.message)
}
if (customersData.message && !customersRes.ok) {
console.warn("Customers fetch error:", customersData.message)
}
const orders = ordersData.orders || [] const orders = ordersData.orders || []
const totalSales = orders.reduce((sum: number, order: { total_price?: string }) => sum + parseFloat(order.total_price ?? "0"), 0) || 0 const totalSales = orders.reduce((sum: number, order: { total_price?: string }) => sum + parseFloat(order.total_price ?? "0"), 0) || 0
// Process chart data (Recent 7 days sales) // Helper to generate date range
const generateDateRange = (range: TimeRange) => {
const dates = []
const now = new Date()
if (range === "1year") {
for (let i = 11; i >= 0; i--) {
const d = new Date(now.getFullYear(), now.getMonth() - i, 1)
dates.push(d.toLocaleDateString("en-US", { month: "short", year: "2-digit" }))
}
} else {
const count = range === "30days" ? 30 : 7
for (let i = count - 1; i >= 0; i--) {
const d = new Date(now.getFullYear(), now.getMonth(), now.getDate() - i)
dates.push(d.toLocaleDateString("en-US", { month: "short", day: "numeric" }))
}
}
return dates
}
const rangeLabels = generateDateRange(timeRange)
const salesByDate: Record<string, number> = {} const salesByDate: Record<string, number> = {}
orders.forEach((order: { created_at?: string; total_price?: string }) => { orders.forEach((order: { created_at?: string; total_price?: string }) => {
const date = new Date(order.created_at ?? "").toLocaleDateString("en-US", { month: "short", day: "numeric" }) const orderDate = new Date(order.created_at ?? "")
salesByDate[date] = (salesByDate[date] || 0) + parseFloat(order.total_price ?? "0") let label = ""
if (timeRange === "1year") {
label = orderDate.toLocaleDateString("en-US", { month: "short", year: "2-digit" })
} else {
label = orderDate.toLocaleDateString("en-US", { month: "short", day: "numeric" })
}
if (rangeLabels.includes(label)) {
salesByDate[label] = (salesByDate[label] || 0) + parseFloat(order.total_price ?? "0")
}
}) })
const processedChartData = Object.entries(salesByDate) const processedChartData = rangeLabels.map(date => ({
.map(([date, amount]) => ({ date, amount })) date,
.slice(-7) // Last 7 days amount: Math.round((salesByDate[date] || 0) * 100) / 100
}))
setChartData(processedChartData) setChartData(processedChartData)
@ -83,37 +112,103 @@ export default function DashboardPage() {
setLoading(false) setLoading(false)
} }
} }
fetchDashboardData()
}, [])
const statCardStyle: Record<string, { bg: string; icon: string }> = { useEffect(() => {
"Total Sales": { bg: "bg-indigo-500/20 border-indigo-400/30", icon: "text-indigo-400" }, fetchDashboardData()
"Active Products": { bg: "bg-emerald-500/20 border-emerald-400/30", icon: "text-emerald-400" }, }, [timeRange])
"Total Orders": { bg: "bg-blue-500/20 border-blue-400/30", icon: "text-blue-400" },
"Customers": { bg: "bg-orange-500/20 border-orange-400/30", icon: "text-orange-400" }, const statCardStyle: Record<string, { bg: string; icon: string; lightBg: string; lightIcon: string }> = {
"Total Sales": {
bg: "bg-indigo-500/20 border-indigo-400/30",
icon: "text-indigo-400",
lightBg: "bg-indigo-50 border-indigo-100",
lightIcon: "text-indigo-600"
},
"Active Products": {
bg: "bg-emerald-500/20 border-emerald-400/30",
icon: "text-emerald-400",
lightBg: "bg-emerald-50 border-emerald-100",
lightIcon: "text-emerald-600"
},
"Total Orders": {
bg: "bg-blue-500/20 border-blue-400/30",
icon: "text-blue-400",
lightBg: "bg-blue-50 border-blue-100",
lightIcon: "text-blue-600"
},
"Customers": {
bg: "bg-orange-500/20 border-orange-400/30",
icon: "text-orange-400",
lightBg: "bg-orange-50 border-orange-100",
lightIcon: "text-orange-600"
},
} }
return ( return (
<div className="space-y-6 md:space-y-8"> <div className="space-y-6 md:space-y-8">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div> <div>
<h1 className="text-2xl md:text-3xl font-extrabold text-white tracking-tight">Overview</h1> <h1 className={cn("text-2xl md:text-3xl font-extrabold tracking-tight", isLight ? "text-slate-900" : "text-white")}>
<p className="text-sm md:text-base text-slate-400 mt-1">Welcome back! Here&apos;s a quick look at your live Shopify stats.</p> Overview
</h1>
<p className={cn("text-sm md:text-base mt-1", isLight ? "text-slate-500" : "text-slate-400")}>
Welcome back! Here&apos;s a quick look at your live Shopify stats.
</p>
</div> </div>
<button className="h-11 px-4 md:px-6 bg-white/10 text-white rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-white/15 border border-white/10 backdrop-blur-sm transition-all text-sm md:text-base shrink-0"> <div className="flex items-center gap-3">
Export Report <ArrowUpRight className="w-4 h-4" /> <div className="relative group">
<select
value={timeRange}
onChange={(e) => setTimeRange(e.target.value as TimeRange)}
className={cn(
"appearance-none h-11 pl-10 pr-10 rounded-xl font-bold border transition-all text-sm cursor-pointer focus:outline-none focus:ring-2 focus:ring-indigo-500/20",
isLight
? "bg-white border-slate-200 text-slate-700 hover:border-slate-300 shadow-sm"
: "bg-white/10 border-white/10 text-white hover:bg-white/15 backdrop-blur-sm"
)}
>
<option value="7days">Last 7 Days</option>
<option value="30days">Last 1 Month</option>
<option value="1year">Last 1 Year</option>
</select>
<Calendar className={cn("absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4", isLight ? "text-slate-400" : "text-slate-400")} />
<ChevronDown className={cn("absolute right-3.5 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none", isLight ? "text-slate-400" : "text-slate-400")} />
</div>
<button className={cn(
"h-11 px-4 md:px-6 rounded-xl font-bold flex items-center justify-center gap-2 border transition-all text-sm md:text-base shrink-0",
isLight
? "bg-indigo-600 text-white border-indigo-700 hover:bg-indigo-700 shadow-sm"
: "bg-white/10 text-white border-white/10 hover:bg-white/15 backdrop-blur-sm"
)}>
Export <ArrowUpRight className="hidden sm:block w-4 h-4" />
</button> </button>
</div> </div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6">
{stats.map((stat: (typeof stats)[number]) => ( {stats.map((stat: (typeof stats)[number]) => (
<div key={stat.label} className="bg-white/5 backdrop-blur-xl p-4 md:p-6 rounded-xl md:rounded-2xl border border-white/10 flex items-center gap-4 md:gap-6 hover:bg-white/[0.07] transition-colors"> <div key={stat.label} className={cn(
<div className={`w-12 h-12 md:w-14 md:h-14 rounded-xl md:rounded-2xl border ${statCardStyle[stat.label]?.bg ?? "bg-indigo-500/20 border-indigo-400/30"} flex items-center justify-center shrink-0 ${statCardStyle[stat.label]?.icon ?? "text-indigo-400"}`}> "p-4 md:p-6 rounded-xl md:rounded-2xl border flex items-center gap-4 md:gap-6 transition-all hover:translate-y-[-2px]",
isLight
? "bg-white border-slate-100 shadow-sm hover:shadow-md"
: "bg-white/5 border-white/10 backdrop-blur-xl hover:bg-white/[0.07]"
)}>
<div className={cn(
"w-12 h-12 md:w-14 md:h-14 rounded-xl md:rounded-2xl border flex items-center justify-center shrink-0 shadow-inner",
isLight ? statCardStyle[stat.label]?.lightBg : statCardStyle[stat.label]?.bg,
isLight ? statCardStyle[stat.label]?.lightIcon : statCardStyle[stat.label]?.icon
)}>
{loading ? <Loader2 className="w-6 h-6 md:w-7 md:h-7 animate-spin" /> : <stat.icon className="w-6 h-6 md:w-7 md:h-7" />} {loading ? <Loader2 className="w-6 h-6 md:w-7 md:h-7 animate-spin" /> : <stat.icon className="w-6 h-6 md:w-7 md:h-7" />}
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="text-[10px] uppercase font-bold text-slate-400 tracking-widest">{stat.label}</p> <p className={cn("text-[10px] uppercase font-bold tracking-widest", isLight ? "text-slate-500" : "text-slate-400")}>
<p className={`text-xl md:text-2xl font-black text-white tracking-tight truncate ${loading ? "animate-pulse opacity-50" : ""}`}> {stat.label}
</p>
<p className={cn(
"text-xl md:text-2xl font-black tracking-tight truncate",
isLight ? "text-slate-900" : "text-white",
loading && "animate-pulse opacity-50"
)}>
{stat.value} {stat.value}
</p> </p>
</div> </div>
@ -121,58 +216,95 @@ export default function DashboardPage() {
))} ))}
</div> </div>
<div className="bg-white/5 backdrop-blur-xl p-4 md:p-8 rounded-xl md:rounded-2xl border border-white/10"> <div className={cn(
<h3 className="text-lg md:text-xl font-bold text-white mb-4 md:mb-6 font-display">Recent Sales Trend</h3> "p-4 md:p-8 rounded-xl md:rounded-2xl border transition-all",
<div className="h-[250px] md:h-[300px] w-full"> isLight ? "bg-white border-slate-100 shadow-sm" : "bg-white/5 border-white/10 backdrop-blur-xl"
)}>
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6 md:mb-8">
<div>
<h3 className={cn("text-lg md:text-xl font-bold font-display", isLight ? "text-slate-900" : "text-white")}>
Recent Sales Trend
</h3>
<p className={cn("text-xs mt-1", isLight ? "text-slate-500" : "text-slate-400")}>
Revenue analysis over time
</p>
</div>
<div className="flex items-center gap-2 bg-slate-100 dark:bg-white/5 p-1 rounded-lg w-fit">
{(["7days", "30days", "1year"] as const).map((r) => (
<button
key={r}
onClick={() => setTimeRange(r)}
className={cn(
"px-3 py-1.5 rounded-md text-[10px] font-bold uppercase tracking-wider transition-all",
timeRange === r
? "bg-white dark:bg-indigo-600 text-indigo-600 dark:text-white shadow-sm"
: "text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-white"
)}
>
{r === "7days" ? "7D" : r === "30days" ? "1M" : "1Y"}
</button>
))}
</div>
</div>
<div className="h-[250px] md:h-[350px] w-full">
{loading ? ( {loading ? (
<div className="w-full h-full flex items-center justify-center rounded-2xl bg-white/5 border border-white/5"> <div className="w-full h-full flex items-center justify-center rounded-2xl bg-slate-50 dark:bg-white/5 border border-slate-100 dark:border-white/5">
<Loader2 className="w-8 h-8 animate-spin text-indigo-400" /> <Loader2 className="w-8 h-8 animate-spin text-indigo-400" />
</div> </div>
) : chartData.length > 0 ? ( ) : chartData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData}> <AreaChart data={chartData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
<defs> <defs>
<linearGradient id="colorAmount" x1="0" y1="0" x2="0" y2="1"> <linearGradient id="colorAmount" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#818cf8" stopOpacity={0.25} /> <stop offset="5%" stopColor="#818cf8" stopOpacity={isLight ? 0.3 : 0.25} />
<stop offset="95%" stopColor="#4f46e5" stopOpacity={0} /> <stop offset="95%" stopColor="#818cf8" stopOpacity={0} />
</linearGradient> </linearGradient>
</defs> </defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="rgba(255,255,255,0.06)" /> <CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke={isLight ? "rgba(0,0,0,0.04)" : "rgba(255,255,255,0.06)"}
/>
<XAxis <XAxis
dataKey="date" dataKey="date"
axisLine={false} axisLine={false}
tickLine={false} tickLine={false}
tick={{ fill: "#94a3b8", fontSize: 10, fontWeight: 600 }} tick={{ fill: isLight ? "#64748b" : "#94a3b8", fontSize: 10, fontWeight: 600 }}
dy={10} dy={10}
/> />
<YAxis <YAxis
axisLine={false} axisLine={false}
tickLine={false} tickLine={false}
tick={{ fill: "#94a3b8", fontSize: 10, fontWeight: 600 }} tick={{ fill: isLight ? "#64748b" : "#94a3b8", fontSize: 10, fontWeight: 600 }}
tickFormatter={(val) => `$${val > 999 ? (val / 1000).toFixed(1) + "k" : val}`}
/> />
<Tooltip <Tooltip
contentStyle={{ contentStyle={{
borderRadius: "16px", borderRadius: "12px",
border: "1px solid rgba(255,255,255,0.1)", border: isLight ? "1px solid #e2e8f0" : "1px solid rgba(255,255,255,0.1)",
background: "rgba(15, 23, 42, 0.9)", background: isLight ? "#ffffff" : "#0f172a",
color: "#f1f5f9", color: isLight ? "#1e293b" : "#f1f5f9",
fontWeight: 700, fontWeight: 700,
backdropFilter: "blur(12px)", boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1)",
backdropFilter: isLight ? "none" : "blur(12px)",
}} }}
formatter={(value: number | undefined) => [value !== undefined ? `$${value.toLocaleString()}` : "$0", "Revenue"]}
/> />
<Area <Area
type="monotone" type="monotone"
dataKey="amount" dataKey="amount"
stroke="#818cf8" stroke="#4f46e5"
strokeWidth={3} strokeWidth={3}
fillOpacity={1} fillOpacity={1}
fill="url(#colorAmount)" fill="url(#colorAmount)"
animationDuration={1500}
/> />
</AreaChart> </AreaChart>
</ResponsiveContainer> </ResponsiveContainer>
) : ( ) : (
<div className="w-full h-full flex items-center justify-center rounded-2xl bg-white/5 border border-white/5 text-slate-400 font-semibold"> <div className="w-full h-full flex items-center justify-center rounded-2xl bg-slate-50 dark:bg-white/5 border border-slate-100 dark:border-white/5 text-slate-400 font-semibold italic">
No sales data found for the last 7 days. No sales data found for this period.
</div> </div>
)} )}
</div> </div>

View File

@ -7,7 +7,12 @@ import { motion, AnimatePresence } from "framer-motion"
import { X, Menu, Zap } from "lucide-react" import { X, Menu, Zap } from "lucide-react"
import { Sidebar } from "./Sidebar" import { Sidebar } from "./Sidebar"
import { useTheme } from "./ThemeProvider"
import { cn } from "@/lib/utils"
export function MobileSidebar() { export function MobileSidebar() {
const { theme } = useTheme()
const isLight = theme === "light"
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [mounted, setMounted] = useState(false) const [mounted, setMounted] = useState(false)
const pathname = usePathname() const pathname = usePathname()
@ -38,20 +43,27 @@ export function MobileSidebar() {
{/* Hamburger Button */} {/* Hamburger Button */}
<button <button
onClick={() => setIsOpen(true)} onClick={() => setIsOpen(true)}
className="md:hidden p-2 rounded-lg bg-white/10 border border-white/10 text-white hover:bg-white/15 transition-all" className={cn(
"md:hidden p-2 rounded-lg transition-all border shadow-sm",
isLight
? "bg-white border-slate-200 text-slate-600 hover:bg-slate-50"
: "bg-white/10 border-white/10 text-white hover:bg-white/15"
)}
aria-label="Open menu" aria-label="Open menu"
> >
<Menu className="w-6 h-6" /> <Menu className="w-6 h-6" />
</button> </button>
{/* Mobile Sidebar Overlay */} {/* Mobile Sidebar Overlay */}
{mounted && createPortal(
<AnimatePresence> <AnimatePresence>
{isOpen && mounted && createPortal( {isOpen && (
<> <>
<motion.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
key="sidebar-backdrop"
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[999] md:hidden pointer-events-auto" className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[999] md:hidden pointer-events-auto"
onClick={() => setIsOpen(false)} onClick={() => setIsOpen(false)}
@ -60,20 +72,42 @@ export function MobileSidebar() {
initial={{ x: -288 }} initial={{ x: -288 }}
animate={{ x: 0 }} animate={{ x: 0 }}
exit={{ x: -288 }} exit={{ x: -288 }}
key="sidebar-content"
transition={{ type: "spring", damping: 25, stiffness: 200 }} transition={{ type: "spring", damping: 25, stiffness: 200 }}
className="fixed inset-y-0 left-0 w-72 bg-slate-900/95 backdrop-blur-xl border-r border-white/10 z-[1000] md:hidden flex flex-col pointer-events-auto" className={cn(
"fixed inset-y-0 left-0 w-72 backdrop-blur-xl border-r z-[1000] md:hidden flex flex-col pointer-events-auto",
isLight
? "bg-white/95 border-slate-200"
: "bg-slate-900/95 border-white/10"
)}
> >
{/* Header with close button */} {/* Header with close button */}
<div className="flex items-center justify-between p-4 border-b border-white/10"> <div className={cn(
"flex items-center justify-between p-4 border-b",
isLight ? "border-slate-100" : "border-white/10"
)}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg bg-indigo-500/40 border border-white/20 flex items-center justify-center backdrop-blur-sm"> <div className={cn(
<Zap className="w-5 h-5 text-white" /> "w-8 h-8 rounded-lg flex items-center justify-center backdrop-blur-sm border shadow-sm",
isLight
? "bg-indigo-50 border-indigo-100"
: "bg-indigo-500/40 border-white/20"
)}>
<Zap className={cn("w-5 h-5", isLight ? "text-indigo-600" : "text-white")} />
</div> </div>
<h1 className="text-xl font-bold tracking-tight text-white">JUNOHUB</h1> <h1 className={cn(
"text-xl font-bold tracking-tight",
isLight ? "text-slate-900" : "text-white"
)}>JUNOHUB</h1>
</div> </div>
<button <button
onClick={() => setIsOpen(false)} onClick={() => setIsOpen(false)}
className="p-2 rounded-lg bg-white/10 border border-white/10 text-white hover:bg-white/15 transition-all" className={cn(
"p-2 rounded-lg transition-all border shadow-sm",
isLight
? "bg-slate-50 border-slate-200 text-slate-600 hover:bg-slate-100"
: "bg-white/10 border-white/10 text-white hover:bg-white/15"
)}
aria-label="Close menu" aria-label="Close menu"
> >
<X className="w-5 h-5" /> <X className="w-5 h-5" />
@ -81,13 +115,14 @@ export function MobileSidebar() {
</div> </div>
{/* Sidebar Content */} {/* Sidebar Content */}
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
<Sidebar /> <Sidebar showLogo={false} className="!bg-transparent !border-none" />
</div> </div>
</motion.div> </motion.div>
</>, </>
)}
</AnimatePresence>,
document.body document.body
)} )}
</AnimatePresence>
</> </>
) )
} }

View File

@ -18,6 +18,7 @@ import {
HelpCircle HelpCircle
} from "lucide-react" } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { useTheme } from "@/components/ThemeProvider"
const routes = [ const routes = [
{ {
@ -75,25 +76,47 @@ const routes = [
} }
] ]
export function Sidebar() { interface SidebarProps {
showLogo?: boolean;
className?: string;
}
export function Sidebar({ showLogo = true, className }: SidebarProps) {
const pathname = usePathname() const pathname = usePathname()
const { theme } = useTheme()
const isLight = theme === "light"
return ( return (
<div className="space-y-4 py-4 flex flex-col h-full text-white"> <div className={cn(
"space-y-4 py-4 flex flex-col h-full transition-colors duration-300",
isLight ? "bg-white border-r border-slate-100 text-slate-900" : "text-white",
className
)}>
{showLogo && (
<div className="px-6 py-2 flex items-center mb-6"> <div className="px-6 py-2 flex items-center mb-6">
<div className="w-8 h-8 rounded-lg bg-indigo-500/40 border border-white/20 flex items-center justify-center mr-3 backdrop-blur-sm"> <div className={cn(
<Zap className="w-5 h-5 text-white fill-white" /> "w-8 h-8 rounded-lg border flex items-center justify-center mr-3 backdrop-blur-sm",
isLight ? "bg-indigo-50 border-indigo-100" : "bg-indigo-500/40 border-white/20"
)}>
<Zap className={cn("w-5 h-5", isLight ? "text-indigo-600 fill-indigo-600" : "text-white fill-white")} />
</div> </div>
<h1 className="text-xl font-bold tracking-tight">JUNOHUB</h1> <h1 className={cn("text-xl font-bold tracking-tight", isLight ? "text-slate-900" : "text-white")}>JUNOHUB</h1>
</div> </div>
)}
<div className="flex-1 px-3 space-y-1 overflow-y-auto"> <div className="flex-1 px-3 space-y-1 overflow-y-auto">
{routes.map((route) => ( {routes.map((route) => (
<div key={route.href}> <div key={route.href}>
<Link <Link
href={route.href} href={route.href}
className={cn( className={cn(
"text-sm group flex p-3 w-full justify-start font-medium cursor-pointer hover:text-white hover:bg-white/10 rounded-xl transition-all duration-200 ease-out border border-transparent", "text-sm group flex p-3 w-full justify-start font-medium cursor-pointer rounded-xl transition-all duration-200 ease-out border border-transparent",
pathname.startsWith(route.href) ? "text-white bg-white/10 border-white/10" : "text-slate-400" isLight
? pathname.startsWith(route.href)
? "text-indigo-600 bg-indigo-50 border-indigo-100"
: "text-slate-600 hover:text-indigo-600 hover:bg-slate-50"
: pathname.startsWith(route.href)
? "text-white bg-white/10 border-white/10"
: "text-slate-400 hover:text-white hover:bg-white/10"
)} )}
> >
<div className="flex items-center flex-1"> <div className="flex items-center flex-1">
@ -108,8 +131,14 @@ export function Sidebar() {
key={sub.href} key={sub.href}
href={sub.href} href={sub.href}
className={cn( className={cn(
"text-xs group flex p-2 w-full justify-start font-medium cursor-pointer hover:text-white hover:bg-white/5 rounded-lg transition-all duration-200 ease-out", "text-xs group flex p-2 w-full justify-start font-medium cursor-pointer rounded-lg transition-all duration-200 ease-out",
pathname === sub.href ? "text-white bg-white/5" : "text-slate-500" isLight
? pathname === sub.href
? "text-indigo-600 bg-indigo-50/50"
: "text-slate-500 hover:text-indigo-600 hover:bg-slate-50"
: pathname === sub.href
? "text-white bg-white/5"
: "text-slate-500 hover:text-white hover:bg-white/5"
)} )}
> >
<div className="flex items-center flex-1"> <div className="flex items-center flex-1">
@ -123,12 +152,18 @@ export function Sidebar() {
</div> </div>
))} ))}
</div> </div>
<div className="px-3 pt-4 border-t border-white/10"> <div className={cn("px-3 pt-4 border-t", isLight ? "border-slate-100" : "border-white/10")}>
<Link <Link
href="/dashboard/help-support" href="/dashboard/help-support"
className={cn( className={cn(
"text-sm group flex p-3 w-full justify-start font-medium cursor-pointer hover:text-white hover:bg-white/10 rounded-xl transition-all duration-200 ease-out border border-transparent", "text-sm group flex p-3 w-full justify-start font-medium cursor-pointer rounded-xl transition-all duration-200 ease-out border border-transparent",
pathname === "/dashboard/help-support" ? "text-white bg-white/10 border-white/10" : "text-slate-400" isLight
? pathname === "/dashboard/help-support"
? "text-blue-600 bg-blue-50 border-blue-100"
: "text-slate-600 hover:text-blue-600 hover:bg-slate-50"
: pathname === "/dashboard/help-support"
? "text-white bg-white/10 border-white/10"
: "text-slate-400 hover:text-white hover:bg-white/10"
)} )}
> >
<div className="flex items-center flex-1"> <div className="flex items-center flex-1">

View File

@ -1,6 +1,8 @@
"use client" "use client"
import { useState } from "react" import { useState } from "react"
import { useTheme } from "@/components/ThemeProvider"
import { cn } from "@/lib/utils"
type AskSectionProps = { type AskSectionProps = {
storeId: string storeId: string
@ -15,6 +17,8 @@ type Source = {
} }
export function AskSection({ storeId }: AskSectionProps) { export function AskSection({ storeId }: AskSectionProps) {
const { theme } = useTheme()
const isLight = theme === "light"
const [query, setQuery] = useState("") const [query, setQuery] = useState("")
const [includeFaq, setIncludeFaq] = useState(true) const [includeFaq, setIncludeFaq] = useState(true)
const [includeDocuments, setIncludeDocuments] = useState(true) const [includeDocuments, setIncludeDocuments] = useState(true)
@ -69,89 +73,118 @@ export function AskSection({ storeId }: AskSectionProps) {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<h3 className="text-lg font-semibold text-white">Ask JUNO</h3> <h3 className={cn("text-lg font-semibold", isLight ? "text-slate-900" : "text-white")}>Ask JUNO</h3>
<p className="text-xs text-slate-400"> <p className={cn("text-xs", isLight ? "text-slate-500" : "text-slate-400")}>
Ask questions across your store&apos;s documents and FAQs. The Ask questions across your store&apos;s documents and FAQs. The
AI will answer using only your knowledge base. AI will answer using only your knowledge base.
</p> </p>
</div> </div>
{error && ( {error && (
<div className="text-xs text-red-400 bg-red-950/40 border border-red-900/50 rounded-md px-3 py-2"> <div className="text-xs text-red-500 bg-red-50 border border-red-100 rounded-lg px-3 py-2">
{error} {error}
</div> </div>
)} )}
<form <form
onSubmit={handleAsk} onSubmit={handleAsk}
className="border border-white/5 rounded-xl bg-slate-900/40 backdrop-blur-sm p-4 space-y-3" className={cn(
"border rounded-xl p-4 space-y-4 transition-all",
isLight ? "bg-white border-slate-100 shadow-sm" : "bg-slate-900/40 border-white/5 backdrop-blur-sm"
)}
> >
<textarea <textarea
value={query} value={query}
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
placeholder="e.g. How long do customers have to return an item?" placeholder="e.g. How long do customers have to return an item?"
className="w-full px-3 py-2 rounded-lg bg-slate-950/60 border border-white/10 text-sm text-white placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/70 focus:border-indigo-500/70 min-h-[80px]" className={cn(
"w-full px-4 py-3 rounded-lg border text-sm transition-all focus:outline-none focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500/50 min-h-[120px]",
isLight
? "bg-slate-50 border-slate-200 text-slate-900 placeholder:text-slate-400"
: "bg-slate-950/60 border-white/10 text-white placeholder:text-slate-500"
)}
/> />
<div className="flex items-center justify-between gap-4 text-xs text-slate-300"> <div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-4 text-xs">
<label className="inline-flex items-center gap-2"> <label className={cn(
"inline-flex items-center gap-2 cursor-pointer transition-colors",
isLight ? "text-slate-600 hover:text-slate-900" : "text-slate-400 hover:text-white"
)}>
<input <input
type="checkbox" type="checkbox"
className="rounded border-slate-600 bg-slate-900 text-indigo-500" className="rounded border-slate-300 bg-white text-indigo-600 focus:ring-indigo-500/20"
checked={includeDocuments} checked={includeDocuments}
onChange={(e) => onChange={(e) =>
setIncludeDocuments(e.target.checked) setIncludeDocuments(e.target.checked)
} }
/> />
<span>Use documents</span> <span className="font-medium">Use documents</span>
</label> </label>
<label className="inline-flex items-center gap-2"> <label className={cn(
"inline-flex items-center gap-2 cursor-pointer transition-colors",
isLight ? "text-slate-600 hover:text-slate-900" : "text-slate-400 hover:text-white"
)}>
<input <input
type="checkbox" type="checkbox"
className="rounded border-slate-600 bg-slate-900 text-indigo-500" className="rounded border-slate-300 bg-white text-indigo-600 focus:ring-indigo-500/20"
checked={includeFaq} checked={includeFaq}
onChange={(e) => setIncludeFaq(e.target.checked)} onChange={(e) => setIncludeFaq(e.target.checked)}
/> />
<span>Use FAQs</span> <span className="font-medium">Use FAQs</span>
</label> </label>
</div> </div>
<button <button
type="submit" type="submit"
disabled={isLoading} disabled={isLoading}
className="inline-flex items-center px-3 py-2 rounded-lg bg-indigo-600/80 hover:bg-indigo-600 text-xs font-medium text-white disabled:opacity-60 disabled:cursor-not-allowed transition-colors" className="inline-flex items-center px-6 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-700 text-xs font-bold text-white disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-sm"
> >
{isLoading ? "Thinking..." : "Ask"} {isLoading ? (
<span className="flex items-center gap-2">
<svg className="animate-spin h-3 w-3 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Thinking...
</span>
) : "Ask JUNO"}
</button> </button>
</div> </div>
</form> </form>
{answer && ( {answer && (
<div className="border border-white/5 rounded-xl bg-slate-900/60 backdrop-blur-sm p-4 space-y-3"> <div className={cn(
"border rounded-xl p-6 space-y-4 animate-in fade-in slide-in-from-top-2 duration-300",
isLight ? "bg-indigo-50/30 border-indigo-100" : "bg-slate-900/60 border-white/5 backdrop-blur-sm"
)}>
<div> <div>
<h4 className="text-xs font-semibold text-slate-300 uppercase tracking-wide mb-1"> <h4 className={cn("text-[11px] font-bold uppercase tracking-wider mb-2", isLight ? "text-indigo-600" : "text-slate-400")}>
Answer Answer
</h4> </h4>
<p className="text-sm text-slate-100 whitespace-pre-wrap"> <p className={cn("text-[15px] leading-relaxed whitespace-pre-wrap", isLight ? "text-slate-800" : "text-slate-100")}>
{answer} {answer}
</p> </p>
</div> </div>
{sources.length > 0 && ( {sources.length > 0 && (
<div className="border-t border-white/10 pt-3"> <div className={cn("border-t pt-4", isLight ? "border-indigo-100" : "border-white/10")}>
<h5 className="text-xs font-semibold text-slate-300 uppercase tracking-wide mb-1"> <h5 className={cn("text-[11px] font-bold uppercase tracking-wider mb-2", isLight ? "text-indigo-600" : "text-slate-400")}>
Sources Sources
</h5> </h5>
<ul className="space-y-1 text-xs text-slate-400"> <ul className="space-y-2 text-xs">
{sources.map((source, index) => ( {sources.map((source, index) => (
<li key={source.id}> <li key={source.id} className="flex items-center gap-2">
<span className="font-medium text-slate-200"> <span className={cn(
"inline-flex items-center px-1.5 py-0.5 rounded bg-slate-100 text-[10px] font-bold",
isLight ? "text-slate-600" : "text-slate-400 bg-slate-800"
)}>
{source.type === "FAQ" {source.type === "FAQ"
? "FAQ" ? "FAQ"
: "Document"}{" "} : "Document"}
#{index + 1}
</span> </span>
<span className="ml-2 text-slate-500"> <span className={isLight ? "text-slate-500" : "text-slate-400"}>
(similarity:{" "} Source #{index + 1}
{source.similarity.toFixed(2)}) </span>
<span className="text-slate-400 ml-auto">
Similarity: {(source.similarity * 100).toFixed(0)}%
</span> </span>
</li> </li>
))} ))}

View File

@ -1,6 +1,8 @@
"use client" "use client"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useTheme } from "@/components/ThemeProvider"
import { cn } from "@/lib/utils"
type KnowledgeDocument = { type KnowledgeDocument = {
id: string id: string
@ -18,6 +20,8 @@ type DocumentsSectionProps = {
} }
export function DocumentsSection({ storeId }: DocumentsSectionProps) { export function DocumentsSection({ storeId }: DocumentsSectionProps) {
const { theme } = useTheme()
const isLight = theme === "light"
const [documents, setDocuments] = useState<KnowledgeDocument[]>([]) const [documents, setDocuments] = useState<KnowledgeDocument[]>([])
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [isUploading, setIsUploading] = useState(false) const [isUploading, setIsUploading] = useState(false)
@ -116,14 +120,14 @@ export function DocumentsSection({ storeId }: DocumentsSectionProps) {
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h3 className="text-lg font-semibold text-white"> <h3 className={cn("text-lg font-semibold", isLight ? "text-slate-900" : "text-white")}>
Documents Documents
</h3> </h3>
<p className="text-xs text-slate-400"> <p className={cn("text-xs", isLight ? "text-slate-500" : "text-slate-400")}>
Upload PDFs or DOCX files to power AI answers. Upload PDFs or DOCX files to power AI answers.
</p> </p>
</div> </div>
<label className="inline-flex items-center px-3 py-2 rounded-lg bg-indigo-600/80 hover:bg-indigo-600 text-sm font-medium text-white cursor-pointer transition-colors"> <label className="inline-flex items-center px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-700 text-sm font-medium text-white cursor-pointer transition-colors shadow-sm">
<span>{isUploading ? "Uploading..." : "Upload document"}</span> <span>{isUploading ? "Uploading..." : "Upload document"}</span>
<input <input
type="file" type="file"
@ -136,60 +140,76 @@ export function DocumentsSection({ storeId }: DocumentsSectionProps) {
</div> </div>
{error && ( {error && (
<div className="text-xs text-red-400 bg-red-950/40 border border-red-900/50 rounded-md px-3 py-2"> <div className="text-xs text-red-500 bg-red-50 border border-red-100 rounded-lg px-3 py-2">
{error} {error}
</div> </div>
)} )}
<div className="border border-white/5 rounded-xl bg-slate-900/40 backdrop-blur-sm"> <div className={cn(
<div className="px-4 py-3 border-b border-white/5 flex items-center justify-between"> "border rounded-xl",
<span className="text-xs font-medium text-slate-300 uppercase tracking-wide"> isLight ? "border-slate-100 bg-white" : "border-white/5 bg-slate-900/40 backdrop-blur-sm"
)}>
<div className={cn(
"px-4 py-3 border-b flex items-center justify-between",
isLight ? "border-slate-100" : "border-white/5"
)}>
<span className={cn(
"text-xs font-semibold uppercase tracking-wide",
isLight ? "text-slate-500" : "text-slate-300"
)}>
Uploaded documents Uploaded documents
</span> </span>
{isLoading && ( {isLoading && (
<span className="text-xs text-slate-400">Loading...</span> <span className="text-xs text-slate-400 animate-pulse">Loading...</span>
)} )}
</div> </div>
<div className="divide-y divide-white/5"> <div className={cn(
"divide-y",
isLight ? "divide-slate-50" : "divide-white/5"
)}>
{documents.length === 0 && !isLoading ? ( {documents.length === 0 && !isLoading ? (
<div className="px-4 py-6 text-sm text-slate-400 text-center"> <div className="px-4 py-10 text-sm text-slate-400 text-center">
No documents yet. Upload your first knowledge base file. No documents yet. Upload your first knowledge base file.
</div> </div>
) : ( ) : (
documents.map((doc) => ( documents.map((doc) => (
<div <div
key={doc.id} key={doc.id}
className="px-4 py-3 flex items-center justify-between gap-4 text-sm" className="px-4 py-4 flex items-center justify-between gap-4 text-sm hover:bg-slate-50/50 transition-colors"
> >
<div> <div className="min-w-0 flex-1">
<p className="font-medium text-white"> <p className={cn("font-medium truncate", isLight ? "text-slate-900" : "text-white")}>
{doc.title} {doc.title}
</p> </p>
<p className="text-xs text-slate-400"> <p className="text-xs text-slate-400 mt-0.5">
{doc.fileName} •{" "} {doc.fileName} •{" "}
{(doc.sizeBytes / 1024).toFixed(1)} KB {(doc.sizeBytes / 1024).toFixed(1)} KB
</p> </p>
</div> </div>
<div className="flex items-center gap-3 text-xs text-right"> <div className="flex items-center gap-4 text-xs">
<span <span
className={`inline-flex items-center px-2 py-1 rounded-full border ${ className={cn(
"inline-flex items-center px-2.5 py-1 rounded-full border text-[10px] font-bold uppercase tracking-wider",
doc.status === "READY" doc.status === "READY"
? "border-emerald-500/60 text-emerald-400 bg-emerald-500/10" ? "border-emerald-500/20 text-emerald-600 bg-emerald-50"
: doc.status === "PROCESSING" : doc.status === "PROCESSING"
? "border-amber-500/60 text-amber-400 bg-amber-500/10" ? "border-amber-500/20 text-amber-600 bg-amber-50"
: doc.status === "FAILED" : doc.status === "FAILED"
? "border-red-500/60 text-red-400 bg-red-500/10" ? "border-red-500/20 text-red-600 bg-red-50"
: "border-slate-600 text-slate-300 bg-slate-800/60" : "border-slate-200 text-slate-500 bg-slate-50"
}`} )}
> >
{doc.status.toLowerCase()} {doc.status.toLowerCase()}
</span> </span>
<button <button
type="button" type="button"
onClick={() => void handleDelete(doc.id)} onClick={() => void handleDelete(doc.id)}
className="px-2 py-1 rounded-md border border-red-500/60 text-red-400 hover:bg-red-500/10 transition-colors" className="p-1.5 rounded-lg border border-red-200 text-red-500 hover:bg-red-50 transition-colors"
title="Delete document"
> >
Delete <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,6 +1,8 @@
"use client" "use client"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useTheme } from "@/components/ThemeProvider"
import { cn } from "@/lib/utils"
type FaqItem = { type FaqItem = {
id: string id: string
@ -15,6 +17,8 @@ type FaqSectionProps = {
} }
export function FaqSection({ storeId }: FaqSectionProps) { export function FaqSection({ storeId }: FaqSectionProps) {
const { theme } = useTheme()
const isLight = theme === "light"
const [faqs, setFaqs] = useState<FaqItem[]>([]) const [faqs, setFaqs] = useState<FaqItem[]>([])
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
@ -25,7 +29,7 @@ export function FaqSection({ storeId }: FaqSectionProps) {
useEffect(() => { useEffect(() => {
if (!storeId) return if (!storeId) return
;(async () => { ; (async () => {
try { try {
setIsLoading(true) setIsLoading(true)
setError(null) setError(null)
@ -165,13 +169,18 @@ export function FaqSection({ storeId }: FaqSectionProps) {
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h3 className="text-lg font-semibold text-white">FAQs</h3> <h3 className={cn("text-lg font-semibold", isLight ? "text-slate-900" : "text-white")}>FAQs</h3>
<p className="text-xs text-slate-400"> <p className={cn("text-xs", isLight ? "text-slate-500" : "text-slate-400")}>
Manage frequently asked questions that your AI can use as Manage frequently asked questions that your AI can use as
trusted answers. trusted answers.
</p> </p>
</div> </div>
<label className="inline-flex items-center px-3 py-2 rounded-lg bg-slate-800/80 hover:bg-slate-800 text-xs font-medium text-slate-100 cursor-pointer transition-colors border border-white/10"> <label className={cn(
"inline-flex items-center px-3 py-2 rounded-lg text-xs font-medium cursor-pointer transition-colors border shadow-sm",
isLight
? "bg-white border-slate-200 text-slate-700 hover:bg-slate-50"
: "bg-slate-800/80 hover:bg-slate-800 border-white/10 text-slate-100 shadow-xl"
)}>
<span> <span>
{isImporting ? "Importing..." : "Import from JSON file"} {isImporting ? "Importing..." : "Import from JSON file"}
</span> </span>
@ -186,70 +195,97 @@ export function FaqSection({ storeId }: FaqSectionProps) {
</div> </div>
{error && ( {error && (
<div className="text-xs text-red-400 bg-red-950/40 border border-red-900/50 rounded-md px-3 py-2"> <div className="text-xs text-red-500 bg-red-50 border border-red-100 rounded-lg px-3 py-2">
{error} {error}
</div> </div>
)} )}
<form <form
onSubmit={handleAddFaq} onSubmit={handleAddFaq}
className="border border-white/5 rounded-xl bg-slate-900/40 backdrop-blur-sm p-4 space-y-3" className={cn(
"border rounded-xl p-4 space-y-4 transition-all",
isLight ? "bg-white border-slate-100 shadow-sm" : "bg-slate-900/40 border-white/5 backdrop-blur-sm"
)}
> >
<div className="grid grid-cols-1 gap-4">
<div> <div>
<label className="block text-xs font-medium text-slate-300 mb-1"> <label className={cn("block text-[11px] font-bold uppercase tracking-wider mb-1.5", isLight ? "text-slate-500" : "text-slate-400")}>
Question Question
</label> </label>
<input <input
value={question} value={question}
onChange={(e) => setQuestion(e.target.value)} onChange={(e) => setQuestion(e.target.value)}
placeholder="e.g. What is your return policy?" placeholder="e.g. What is your return policy?"
className="w-full px-3 py-2 rounded-lg bg-slate-950/60 border border-white/10 text-sm text-white placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/70 focus:border-indigo-500/70" className={cn(
"w-full px-3 py-2 rounded-lg border text-sm transition-all focus:outline-none focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500/50",
isLight
? "bg-slate-50 border-slate-200 text-slate-900 placeholder:text-slate-400"
: "bg-slate-950/60 border-white/10 text-white placeholder:text-slate-500"
)}
/> />
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-slate-300 mb-1"> <label className={cn("block text-[11px] font-bold uppercase tracking-wider mb-1.5", isLight ? "text-slate-500" : "text-slate-400")}>
Answer Answer
</label> </label>
<textarea <textarea
value={answer} value={answer}
onChange={(e) => setAnswer(e.target.value)} onChange={(e) => setAnswer(e.target.value)}
placeholder="Provide a clear, friendly answer that can be shown directly to customers." placeholder="Provide a clear, friendly answer that can be shown directly to customers."
className="w-full px-3 py-2 rounded-lg bg-slate-950/60 border border-white/10 text-sm text-white placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/70 focus:border-indigo-500/70 min-h-[80px]" className={cn(
"w-full px-3 py-2 rounded-lg border text-sm transition-all focus:outline-none focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500/50 min-h-[100px]",
isLight
? "bg-slate-50 border-slate-200 text-slate-900 placeholder:text-slate-400"
: "bg-slate-950/60 border-white/10 text-white placeholder:text-slate-500"
)}
/> />
</div> </div>
<div className="flex justify-end"> </div>
<div className="flex justify-end pt-2">
<button <button
type="submit" type="submit"
disabled={isSaving} disabled={isSaving}
className="inline-flex items-center px-3 py-2 rounded-lg bg-indigo-600/80 hover:bg-indigo-600 text-xs font-medium text-white disabled:opacity-60 disabled:cursor-not-allowed transition-colors" className="inline-flex items-center px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-700 text-xs font-bold text-white disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-sm"
> >
{isSaving ? "Saving..." : "Add FAQ"} {isSaving ? "Saving..." : "Add FAQ"}
</button> </button>
</div> </div>
</form> </form>
<div className="border border-white/5 rounded-xl bg-slate-900/40 backdrop-blur-sm"> <div className={cn(
<div className="px-4 py-3 border-b border-white/5 flex items-center justify-between"> "border rounded-xl transition-all",
<span className="text-xs font-medium text-slate-300 uppercase tracking-wide"> isLight ? "bg-white border-slate-100 shadow-sm" : "bg-slate-900/40 border-white/5 backdrop-blur-sm"
)}>
<div className={cn(
"px-4 py-3 border-b flex items-center justify-between",
isLight ? "border-slate-100" : "border-white/5"
)}>
<span className={cn(
"text-xs font-semibold uppercase tracking-wide",
isLight ? "text-slate-500" : "text-slate-300"
)}>
Existing FAQs Existing FAQs
</span> </span>
{isLoading && ( {isLoading && (
<span className="text-xs text-slate-400">Loading...</span> <span className="text-xs text-slate-400 animate-pulse">Loading...</span>
)} )}
</div> </div>
<div className="divide-y divide-white/5"> <div className={cn(
"divide-y",
isLight ? "divide-slate-50" : "divide-white/5"
)}>
{faqs.length === 0 && !isLoading ? ( {faqs.length === 0 && !isLoading ? (
<div className="px-4 py-6 text-sm text-slate-400 text-center"> <div className="px-4 py-10 text-sm text-slate-400 text-center">
No FAQs yet. Add your first common question above or No FAQs yet. Add your first common question above or
import from JSON. import from JSON.
</div> </div>
) : ( ) : (
faqs.map((faq) => ( faqs.map((faq) => (
<div key={faq.id} className="px-4 py-3 text-sm"> <div key={faq.id} className="px-4 py-4 text-sm hover:bg-slate-50/50 transition-colors group">
<p className="font-medium text-white"> <p className={cn("font-semibold", isLight ? "text-slate-900" : "text-white")}>
{faq.question} {faq.question}
</p> </p>
<p className="mt-1 text-xs text-slate-300"> <p className={cn("mt-1.5 text-[13px] leading-relaxed", isLight ? "text-slate-600" : "text-slate-300")}>
{faq.answer} {faq.answer}
</p> </p>
</div> </div>