fixed UI and MFA

This commit is contained in:
root 2026-02-14 16:02:43 +05:00
parent 9d2ca7f592
commit 59d90ed807
22 changed files with 1309 additions and 1092 deletions

View File

@ -1,5 +1,5 @@
import { AdminSidebar } from "@/components/AdminSidebar"
import { MobileSidebar } from "@/components/MobileSidebar"
import { MobileAdminSidebar } from "@/components/MobileAdminSidebar"
import UserAccountMenu from "@/components/UserAccountMenu"
import NotificationBell from "@/components/NotificationBell"
import PageTransition from "@/components/PageTransition"
@ -41,7 +41,7 @@ export default async function AdminLayout({
<header className="h-16 px-4 md:px-8 border-b border-white/10 bg-slate-900/50 backdrop-blur-xl flex items-center justify-between flex-shrink-0 z-50">
<div className="flex items-center gap-2 md:gap-4 flex-1 min-w-0">
{/* Reuse vendor mobile sidebar trigger for consistency */}
<MobileSidebar />
<MobileAdminSidebar />
<div className="flex flex-col">
<h2 className="text-xs md:text-sm font-semibold text-slate-300 uppercase tracking-wider">
Admin Portal

View File

@ -1,6 +1,8 @@
"use client"
import { useEffect, useMemo, useState } from "react"
import { useState, useEffect, useMemo } from "react"
import { createPortal } from "react-dom"
import { motion, AnimatePresence } from "framer-motion"
import { FileText, Search, Plus, Loader2, Edit, Trash2 } from "lucide-react"
import { renderTemplate } from "@/lib/templates/simpleRenderer"
import { useTheme } from "@/components/ThemeProvider"
@ -52,9 +54,14 @@ export default function EmailTemplatesPage() {
const [editing, setEditing] = useState<Template | null>(null)
const [saving, setSaving] = useState(false)
const [mounted, setMounted] = useState(false)
const { theme } = useTheme()
const isLight = theme === "light"
useEffect(() => {
setMounted(true)
}, [])
const loadTemplates = async () => {
try {
setLoading(true)
@ -186,8 +193,10 @@ export default function EmailTemplatesPage() {
return (
<div className="h-full flex flex-col space-y-4 md:space-y-6 mt-4 md:mt-6">
<div className="bg-white/5 backdrop-blur-xl rounded-xl md:rounded-2xl border border-white/10 overflow-hidden flex flex-col flex-1 min-h-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={`backdrop-blur-xl rounded-xl md:rounded-2xl border overflow-hidden flex flex-col flex-1 min-h-0 ${isLight ? "bg-white border-slate-200 shadow-sm" : "bg-white/5 border-white/10"
}`}>
<div className={`p-4 md:p-6 border-b flex flex-col sm:flex-row gap-3 md:gap-4 justify-between flex-shrink-0 ${isLight ? "border-slate-100 bg-slate-50/50" : "border-white/10 bg-white/5"
}`}>
<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" />
<input
@ -195,7 +204,10 @@ export default function EmailTemplatesPage() {
placeholder="Search templates by name, intent, or mood..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full h-10 md:h-11 pl-10 md:pl-11 pr-4 bg-white/5 border border-white/10 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500/30 focus:border-indigo-500/50 text-white placeholder:text-slate-500 text-sm font-medium transition-all"
className={`w-full h-10 md:h-11 pl-10 md:pl-11 pr-4 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500/30 transition-all text-sm font-medium ${isLight
? "bg-white border-slate-200 text-slate-900 placeholder:text-slate-400 focus:border-indigo-500"
: "bg-white/5 border-white/10 text-white placeholder:text-slate-500 focus:border-indigo-500/50"
}`}
/>
</div>
<button
@ -232,26 +244,31 @@ export default function EmailTemplatesPage() {
key={template.id}
type="button"
onClick={() => startEditTemplate(template)}
className="bg-white/5 backdrop-blur-xl p-4 md:p-6 rounded-xl md:rounded-2xl border border-white/10 hover:bg-white/[0.07] hover:border-indigo-500/30 transition-all group text-left"
className={`backdrop-blur-xl p-4 md:p-6 rounded-xl md:rounded-2xl border transition-all group text-left ${isLight
? "bg-white border-slate-200 hover:bg-slate-50 hover:border-indigo-300 shadow-sm"
: "bg-white/5 border-white/10 hover:bg-white/[0.07] hover:border-indigo-500/30"
}`}
>
<div className="flex items-start justify-between mb-4">
<div className="flex-1 min-w-0">
<h3 className="font-bold text-base md:text-lg text-white truncate group-hover:text-indigo-400 transition-colors">
<h3 className={`font-bold text-base md:text-lg truncate group-hover:text-indigo-500 transition-colors ${isLight ? "text-slate-900" : "text-white"
}`}>
{template.name}
</h3>
<p className="text-xs text-slate-400 mt-1 truncate">
<p className={`text-xs mt-1 truncate ${isLight ? "text-slate-500" : "text-slate-400"}`}>
Intent: {template.intent} • Mood:{" "}
{template.mood}
</p>
</div>
</div>
<p className="text-xs text-slate-300 line-clamp-3">
<p className={`text-xs line-clamp-3 ${isLight ? "text-slate-600" : "text-slate-300"}`}>
{template.description ||
template.body.split("\n").slice(0, 3).join(" ")}
</p>
<div className="flex items-center justify-between pt-4 border-t border-white/10 mt-4">
<div className={`flex items-center justify-between pt-4 border-t mt-4 ${isLight ? "border-slate-100" : "border-white/10"
}`}>
<span className="text-[10px] text-slate-500 font-medium">
Updated{" "}
{new Date(
@ -283,230 +300,272 @@ export default function EmailTemplatesPage() {
</div>
</div>
{editing && (
<div
className={`fixed inset-0 z-40 flex items-start justify-center px-4 pt-20 md:pt-24 ${
isLight ? "bg-slate-900/10" : "bg-slate-950/60"
}`}
>
<div
className={`flex w-full max-h-[90vh] max-w-3xl flex-col rounded-2xl border shadow-2xl ${
isLight
? "bg-white text-slate-900 border-slate-200"
: "bg-slate-950 text-slate-50 border-white/10"
}`}
>
<div
className={`flex items-center justify-between px-5 py-4 border-b ${
isLight ? "border-slate-200" : "border-white/10"
}`}
>
<div>
<h2 className="text-lg font-bold">
{editing.id ? "Edit template" : "New template"}
</h2>
<p
className={`mt-0.5 text-xs ${
isLight ? "text-slate-500" : "text-slate-400"
}`}
>
Use {"{{variable}}"} placeholders to personalize replies.
</p>
</div>
<button
type="button"
onClick={() => setEditing(null)}
className={`text-sm ${
isLight
? "text-slate-500 hover:text-slate-900"
: "text-slate-400 hover:text-white"
<AnimatePresence>
{editing && mounted && createPortal(
<div className="fixed inset-0 z-[100] flex items-center justify-center pointer-events-none p-4">
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => !saving && setEditing(null)}
className={`absolute inset-0 pointer-events-auto backdrop-blur-sm ${isLight ? "bg-black/40" : "bg-black/60"
}`}
/>
{/* Modal */}
<motion.div
initial={{ scale: 0.95, opacity: 0, y: 20 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.95, opacity: 0, y: 20 }}
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-slate-900 text-slate-50 border-white/10"
}`}
>
<div
className={`flex items-center justify-between px-6 py-5 border-b ${isLight ? "border-slate-100" : "border-white/10"
}`}
>
Close
</button>
</div>
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-3">
<div>
<label className="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
Name
</label>
<input
type="text"
value={editing.name}
onChange={(e) =>
setEditing((prev) =>
prev
? { ...prev, name: e.target.value }
: prev,
)
}
className="w-full h-9 px-3 rounded-lg bg-slate-50 dark:bg-slate-900/60 border border-slate-200 dark:border-white/10 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40"
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
Intent
</label>
<select
value={editing.intent}
onChange={(e) =>
setEditing((prev) =>
prev
? {
...prev,
intent: e.target.value,
}
: prev,
)
}
className="w-full h-9 px-3 rounded-lg bg-slate-50 dark:bg-slate-900/60 border border-slate-200 dark:border-white/10 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40"
>
{INTENT_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
Mood
</label>
<select
value={editing.mood}
onChange={(e) =>
setEditing((prev) =>
prev
? {
...prev,
mood: e.target.value,
}
: prev,
)
}
className="w-full h-9 px-3 rounded-lg bg-slate-50 dark:bg-slate-900/60 border border-slate-200 dark:border-white/10 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40"
>
{MOOD_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
Description (optional)
</label>
<input
type="text"
value={editing.description || ""}
onChange={(e) =>
setEditing((prev) =>
prev
? {
...prev,
description: e.target.value,
}
: prev,
)
}
className="w-full h-9 px-3 rounded-lg bg-slate-50 dark:bg-slate-900/60 border border-slate-200 dark:border-white/10 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40"
placeholder="Short summary for your team"
/>
</div>
</div>
<div className="space-y-2 text-xs text-slate-600 dark:text-slate-400">
<p className="font-semibold text-slate-800 dark:text-slate-200">
Available variables
<div>
<h2 className="text-xl font-bold">
{editing.id ? "Edit template" : "New template"}
</h2>
<p
className={`mt-1 text-sm ${isLight ? "text-slate-500" : "text-slate-400"
}`}
>
Use {"{{variable}}"} placeholders to personalize replies.
</p>
<ul className="space-y-1">
<li>
<code className="px-1.5 py-0.5 bg-slate-100 dark:bg-slate-900/80 rounded border border-slate-200 dark:border-white/10 text-[11px]">
{"{{customer_name}}"}
</code>{" "}
– Customer’s name
</li>
<li>
<code className="px-1.5 py-0.5 bg-slate-100 dark:bg-slate-900/80 rounded border border-slate-200 dark:border-white/10 text-[11px]">
{"{{customer_email}}"}
</code>{" "}
– Customer’s email
</li>
<li>
<code className="px-1.5 py-0.5 bg-slate-100 dark:bg-slate-900/80 rounded border border-slate-200 dark:border-white/10 text-[11px]">
{"{{store_name}}"}
</code>{" "}
– Store name
</li>
<li>
<code className="px-1.5 py-0.5 bg-slate-100 dark:bg-slate-900/80 rounded border border-slate-200 dark:border-white/10 text-[11px]">
{"{{ticket_summary}}"}
</code>{" "}
– Short summary of the customer’s email
</li>
</ul>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
Template body
</label>
<textarea
value={editing.body}
onChange={(e) =>
setEditing((prev) =>
prev ? { ...prev, body: e.target.value } : prev,
)
}
rows={8}
className="w-full rounded-lg bg-slate-50 dark:bg-slate-900/60 border border-slate-200 dark:border-white/10 text-xs text-slate-900 dark:text-slate-100 font-mono p-3 focus:outline-none focus:ring-2 focus:ring-indigo-500/40"
/>
</div>
<div>
<p className="text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
Preview with example data
</p>
<div className="rounded-lg border border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-slate-900/80 p-4 text-xs text-slate-900 dark:text-slate-100 whitespace-pre-wrap">
{currentPreview || "Preview will appear here."}
</div>
</div>
</div>
<div className="px-5 py-3 border-t border-slate-200 dark:border-white/10 flex items-center justify-between">
<p className="text-[11px] text-slate-500 dark:text-slate-500">
Templates use simple {"{{variable}}"} placeholders only.
</p>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setEditing(null)}
className="h-8 px-3 rounded-lg border border-slate-200 dark:border-white/10 text-xs text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-white/10"
className={`w-10 h-10 rounded-xl flex items-center justify-center transition-colors ${isLight
? "bg-slate-100 hover:bg-slate-200 text-slate-600"
: "bg-white/10 hover:bg-white/15 text-slate-300"
}`}
>
Cancel
</button>
<button
type="button"
disabled={saving}
onClick={() => void handleSave()}
className="h-8 px-3 rounded-lg bg-indigo-500 text-xs font-semibold text-white hover:bg-indigo-600 border border-indigo-400/30 disabled:opacity-60 disabled:cursor-not-allowed"
>
{saving ? "Saving..." : "Save template"}
<Plus className="w-5 h-5 rotate-45" />
</button>
</div>
</div>
</div>
</div>
)}
<div className="flex-1 overflow-y-auto px-6 py-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">
<div>
<label className={`block text-xs font-bold uppercase tracking-wider mb-2 ${isLight ? "text-slate-500" : "text-slate-400"
}`}>
Template Name
</label>
<input
type="text"
value={editing.name}
onChange={(e) =>
setEditing((prev) =>
prev
? { ...prev, name: e.target.value }
: prev,
)
}
placeholder="e.g. Shipping Update"
className={`w-full h-11 px-4 rounded-xl transition-all focus:outline-none focus:ring-2 focus:ring-indigo-500/30 ${isLight
? "bg-slate-50 border border-slate-200 text-slate-900 placeholder:text-slate-400 focus:border-indigo-500"
: "bg-white/5 border border-white/10 text-white placeholder:text-slate-500 focus:border-indigo-500"
}`}
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className={`block text-xs font-bold uppercase tracking-wider mb-2 ${isLight ? "text-slate-500" : "text-slate-400"
}`}>
Intent
</label>
<select
value={editing.intent}
onChange={(e) =>
setEditing((prev) =>
prev
? {
...prev,
intent: e.target.value,
}
: prev,
)
}
className={`w-full h-11 px-4 rounded-xl transition-all focus:outline-none focus:ring-2 focus:ring-indigo-500/30 ${isLight
? "bg-slate-50 border border-slate-200 text-slate-900 focus:border-indigo-500"
: "bg-white/5 border border-white/10 text-white focus:border-indigo-500"
}`}
>
{INTENT_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
<div>
<label className={`block text-xs font-bold uppercase tracking-wider mb-2 ${isLight ? "text-slate-500" : "text-slate-400"
}`}>
Mood
</label>
<select
value={editing.mood}
onChange={(e) =>
setEditing((prev) =>
prev
? {
...prev,
mood: e.target.value,
}
: prev,
)
}
className={`w-full h-11 px-4 rounded-xl transition-all focus:outline-none focus:ring-2 focus:ring-indigo-500/30 ${isLight
? "bg-slate-50 border border-slate-200 text-slate-900 focus:border-indigo-500"
: "bg-white/5 border border-white/10 text-white focus:border-indigo-500"
}`}
>
{MOOD_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
</div>
<div>
<label className={`block text-xs font-bold uppercase tracking-wider mb-2 ${isLight ? "text-slate-500" : "text-slate-400"
}`}>
Description (optional)
</label>
<input
type="text"
value={editing.description || ""}
onChange={(e) =>
setEditing((prev) =>
prev
? {
...prev,
description: e.target.value,
}
: prev,
)
}
placeholder="Short summary for your team"
className={`w-full h-11 px-4 rounded-xl transition-all focus:outline-none focus:ring-2 focus:ring-indigo-500/30 ${isLight
? "bg-slate-50 border border-slate-200 text-slate-900 placeholder:text-slate-400 focus:border-indigo-500"
: "bg-white/5 border border-white/10 text-white placeholder:text-slate-500 focus:border-indigo-500"
}`}
/>
</div>
</div>
<div className={`p-4 rounded-2xl border ${isLight ? "bg-indigo-50/50 border-indigo-100" : "bg-indigo-500/5 border-indigo-500/10"
}`}>
<p className={`text-xs font-bold uppercase tracking-wider mb-3 ${isLight ? "text-indigo-600" : "text-indigo-400"
}`}>
Available variables
</p>
<div className="grid grid-cols-1 gap-2">
{[
{ key: "{{customer_name}}", label: "Customer's name" },
{ key: "{{customer_email}}", label: "Customer's email" },
{ key: "{{store_name}}", label: "Store name" },
{ key: "{{ticket_summary}}", label: "Email summary" }
].map((v) => (
<div key={v.key} className="flex items-center justify-between text-[11px]">
<code className={`px-2 py-0.5 rounded font-mono font-bold ${isLight ? "bg-white border border-indigo-200 text-indigo-700" : "bg-white/10 text-indigo-300"
}`}>
{v.key}
</code>
<span className={isLight ? "text-slate-500" : "text-slate-400"}>{v.label}</span>
</div>
))}
</div>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<label className={`block text-xs font-bold uppercase tracking-wider ${isLight ? "text-slate-500" : "text-slate-400"
}`}>
Template body
</label>
<span className={`text-[10px] ${isLight ? "text-slate-400" : "text-slate-500"}`}>
Markdown & HTML supported
</span>
</div>
<textarea
value={editing.body}
onChange={(e) =>
setEditing((prev) =>
prev ? { ...prev, body: e.target.value } : prev,
)
}
rows={8}
className={`w-full rounded-xl transition-all focus:outline-none focus:ring-2 focus:ring-indigo-500/30 p-4 font-mono text-xs leading-relaxed ${isLight
? "bg-slate-50 border border-slate-200 text-slate-900 focus:border-indigo-500"
: "bg-white/5 border border-white/10 text-white focus:border-indigo-500"
}`}
/>
</div>
<div className="space-y-4">
<label className={`block text-xs font-bold uppercase tracking-wider ${isLight ? "text-slate-500" : "text-slate-400"
}`}>
Preview with example data
</label>
<div className={`rounded-xl border p-5 text-sm leading-relaxed whitespace-pre-wrap ${isLight
? "bg-slate-50 border-slate-200 text-slate-800"
: "bg-slate-950 border-white/5 text-slate-300 shadow-inner"
}`}>
{currentPreview || "Preview will appear here..."}
</div>
</div>
</div>
<div className={`px-6 py-4 border-t flex items-center justify-between ${isLight ? "border-slate-100 bg-slate-50/50" : "border-white/10 bg-white/[0.02]"
}`}>
<p className={`text-[11px] font-medium ${isLight ? "text-slate-500" : "text-slate-500"}`}>
Autosaved to your draft templates.
</p>
<div className="flex items-center gap-3">
<button
type="button"
onClick={() => setEditing(null)}
className={`h-10 px-6 rounded-xl font-bold transition-all border ${isLight
? "bg-white text-slate-700 hover:bg-slate-50 border-slate-200"
: "bg-white/10 text-slate-300 hover:bg-white/20 border-white/10"
}`}
>
Cancel
</button>
<button
type="button"
disabled={saving}
onClick={() => void handleSave()}
className="h-10 px-6 bg-indigo-500 text-sm font-bold text-white hover:bg-indigo-600 border border-indigo-400/20 shadow-lg shadow-indigo-500/20 transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 rounded-xl"
>
{saving ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Saving...
</>
) : (
"Save template"
)}
</button>
</div>
</div>
</motion.div>
</div>,
document.body
)}
</AnimatePresence>
</div>
)
}

View File

@ -42,21 +42,23 @@ export default async function DashboardLayout({
<div className="hidden h-full md:flex md:w-72 md:flex-col md:fixed md:inset-y-0 z-[80] bg-slate-900/70 border-r border-white/10 backdrop-blur-xl">
<Sidebar />
</div>
{/* Main Content Area */}
<main className="md:pl-72 flex flex-col flex-1 h-full min-w-0">
{/* Fixed Header */}
<header className="h-16 px-4 md:px-8 border-b border-white/10 bg-slate-900/50 backdrop-blur-xl flex items-center justify-between flex-shrink-0 z-50">
<div className="flex items-center gap-2 md:gap-4 flex-1 min-w-0">
<div className="flex items-center gap-1.5 md:gap-4 flex-1 min-w-0">
<MobileSidebar />
<h2 className="hidden sm:block text-xs md:text-sm font-semibold text-slate-400 uppercase tracking-wider">Vendor Portal</h2>
<div className="flex-1 min-w-0">
<h2 className="hidden lg:block text-xs md:text-sm font-semibold text-slate-400 uppercase tracking-wider shrink-0">Vendor Portal</h2>
<div className="flex-1 min-w-0 max-w-[200px] md:max-w-none">
<StoreSwitcher />
</div>
</div>
<div className="flex items-center gap-2 md:gap-4 shrink-0">
<div className="flex items-center gap-1.5 md:gap-4 shrink-0">
<ThemeToggle />
<NotificationBell />
<div className="hidden xs:block">
<NotificationBell />
</div>
<div className="hidden sm:flex flex-col text-right">
<span className="text-xs font-bold text-white truncate max-w-[120px]">
{session.user?.name || "Vendor User"}

View File

@ -81,7 +81,7 @@ export default function OrdersPage() {
setLoading(false)
}
}
// Fetch store info and orders
fetchStoreInfo()
fetchOrders()
@ -147,12 +147,12 @@ export default function OrdersPage() {
o.financial_status,
o.fulfillment_status || "unfulfilled"
])
const csvContent = [
csvHeaders.join(","),
...csvRows.map(row => row.join(","))
].join("\n")
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" })
const link = document.createElement("a")
const url = URL.createObjectURL(blob)
@ -168,7 +168,7 @@ export default function OrdersPage() {
const filteredOrders = orders.filter(o => {
const matchesSearch = o.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(o.customer && `${o.customer.first_name} ${o.customer.last_name}`.toLowerCase().includes(searchTerm.toLowerCase()))
const matchesStatus = statusFilter === "all" ||
const matchesStatus = statusFilter === "all" ||
(statusFilter === "fulfilled" && o.fulfillment_status === "fulfilled") ||
(statusFilter === "unfulfilled" && (!o.fulfillment_status || o.fulfillment_status !== "fulfilled")) ||
(statusFilter === "paid" && o.financial_status === "paid") ||
@ -195,7 +195,7 @@ export default function OrdersPage() {
<button className="h-10 md:h-11 px-3 md:px-6 bg-white/10 border border-white/10 rounded-xl font-bold text-white hover:bg-white/15 backdrop-blur-sm transition-all flex items-center gap-2 text-sm">
<Calendar className="w-4 h-4" /> <span className="hidden sm:inline">This Month</span>
</button>
<button
<button
onClick={handleExport}
className="h-10 md:h-11 px-4 md:px-6 bg-indigo-500 text-white rounded-xl font-bold flex items-center gap-2 hover:bg-indigo-600 border border-indigo-400/20 transition-all text-sm"
>
@ -235,120 +235,119 @@ export default function OrdersPage() {
{/* Orders Grid */}
<div className="flex-1 overflow-y-auto overflow-x-hidden min-h-0">
{loading ? (
<div className="h-full flex flex-col items-center justify-center text-slate-400 gap-3">
<Loader2 className="w-8 h-8 animate-spin text-indigo-400" />
<p className="font-medium animate-pulse">Fetching latest orders...</p>
{loading ? (
<div className="h-full flex flex-col items-center justify-center text-slate-400 gap-3">
<Loader2 className="w-8 h-8 animate-spin text-indigo-400" />
<p className="font-medium animate-pulse">Fetching latest orders...</p>
</div>
) : error ? (
<div className="h-full flex flex-col items-center justify-center text-red-400 gap-4 p-8 text-center">
<div className="w-12 h-12 bg-red-500/10 border border-red-500/20 rounded-2xl flex items-center justify-center">
<AlertCircle className="w-6 h-6" />
</div>
) : error ? (
<div className="h-full flex flex-col items-center justify-center text-red-400 gap-4 p-8 text-center">
<div className="w-12 h-12 bg-red-500/10 border border-red-500/20 rounded-2xl flex items-center justify-center">
<AlertCircle className="w-6 h-6" />
</div>
<p className="font-bold">{error}</p>
</div>
) : filteredOrders.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center text-slate-400 gap-3">
<ShoppingCart className="w-10 h-10 opacity-20" />
<p className="font-medium">No orders found.</p>
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 md:gap-6 h-full content-start">
{filteredOrders.map((order) => (
<div
key={order.id}
className="bg-white/5 rounded-xl border border-white/10 p-5 hover:bg-white/10 hover:border-indigo-500/30 transition-all cursor-pointer group relative"
onClick={() => handleViewDetails(order)}
>
{/* Order Header */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="w-10 h-10 rounded-lg bg-indigo-500/20 border border-indigo-400/20 flex items-center justify-center text-indigo-400 group-hover:bg-indigo-500/30 transition-all shrink-0">
<ShoppingBag className="w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<h3 className="font-black text-base text-white truncate">{order.name}</h3>
<p className="text-xs text-slate-400 mt-0.5">
{new Date(order.created_at).toLocaleDateString()} • {new Date(order.created_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
</p>
</div>
<p className="font-bold">{error}</p>
</div>
) : filteredOrders.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center text-slate-400 gap-3">
<ShoppingCart className="w-10 h-10 opacity-20" />
<p className="font-medium">No orders found.</p>
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 md:gap-6 h-full content-start">
{filteredOrders.map((order) => (
<div
key={order.id}
className="bg-white/5 rounded-xl border border-white/10 p-5 hover:bg-white/10 hover:border-indigo-500/30 transition-all cursor-pointer group relative"
onClick={() => handleViewDetails(order)}
>
{/* Order Header */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="w-10 h-10 rounded-lg bg-indigo-500/20 border border-indigo-400/20 flex items-center justify-center text-indigo-400 group-hover:bg-indigo-500/30 transition-all shrink-0">
<ShoppingBag className="w-5 h-5" />
</div>
<div className="relative shrink-0" onClick={(e) => e.stopPropagation()} ref={(el) => { menuRefs.current[order.id] = el }}>
<button
onClick={() => handleMenuToggle(order.id)}
className="p-2 hover:bg-white/10 rounded-lg text-slate-400 hover:text-indigo-400 transition-all"
>
<MoreHorizontal className="w-4 h-4" />
</button>
{openMenuId === order.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-[200] min-w-[180px] whitespace-nowrap">
<button
onClick={() => handleViewDetails(order)}
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" />
Details
</button>
<button
onClick={() => handleViewInShopify(order.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"
>
<ExternalLink className="w-4 h-4" />
Show on Shopify
</button>
<button
onClick={() => handleCopyOrderId(order.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 Order ID
</button>
</div>
)}
<div className="flex-1 min-w-0">
<h3 className="font-black text-base text-white truncate">{order.name}</h3>
<p className="text-xs text-slate-400 mt-0.5">
{new Date(order.created_at).toLocaleDateString()} • {new Date(order.created_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
</p>
</div>
</div>
{/* Customer Info */}
{order.customer && (
<div className="flex items-center gap-2 mb-4">
<div className="w-7 h-7 rounded-full bg-white/10 flex items-center justify-center shrink-0">
<User className="w-3.5 h-3.5 text-slate-400" />
<div className="relative shrink-0" onClick={(e) => e.stopPropagation()} ref={(el) => { menuRefs.current[order.id] = el }}>
<button
onClick={() => handleMenuToggle(order.id)}
className="p-2 hover:bg-white/10 rounded-lg text-slate-400 hover:text-indigo-400 transition-all"
>
<MoreHorizontal className="w-4 h-4" />
</button>
{openMenuId === order.id && (
<div className="absolute right-0 top-full mt-1 bg-slate-800/95 backdrop-blur-xl border border-white/10 rounded-xl shadow-xl overflow-hidden z-[200] min-w-[180px] whitespace-nowrap">
<button
onClick={() => handleViewDetails(order)}
className="w-full px-4 py-2.5 text-left text-sm font-medium text-slate-200 hover:bg-indigo-500 hover:text-white transition-colors flex items-center gap-2"
>
<Eye className="w-4 h-4" />
Details
</button>
<button
onClick={() => handleViewInShopify(order.id)}
className="w-full px-4 py-2.5 text-left text-sm font-medium text-slate-200 hover:bg-indigo-500 hover:text-white transition-colors flex items-center gap-2"
>
<ExternalLink className="w-4 h-4" />
Show on Shopify
</button>
<button
onClick={() => handleCopyOrderId(order.id)}
className="w-full px-4 py-2.5 text-left text-sm font-medium text-slate-200 hover:bg-indigo-500 hover:text-white transition-colors flex items-center gap-2"
>
<Copy className="w-4 h-4" />
Copy Order ID
</button>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-bold text-white truncate">
{order.customer.first_name} {order.customer.last_name}
</p>
<p className="text-xs text-slate-400 truncate">{order.email}</p>
</div>
</div>
)}
{/* Order Details */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-xs font-bold text-slate-400 uppercase tracking-wider">Total</span>
<span className="text-lg font-black text-white">{order.total_price} {order.currency}</span>
</div>
<div className="flex items-center gap-2 flex-wrap">
<span className={`px-2.5 py-1 rounded-full text-[10px] font-black uppercase tracking-wider inline-flex items-center gap-1.5 ${
order.financial_status === "paid"
? "bg-emerald-500/20 text-emerald-400 border border-emerald-500/30"
: "bg-red-500/20 text-red-400 border border-red-500/30"
}`}>
{order.financial_status === "paid" && <CheckCircle2 className="w-3 h-3" />}
Payment: {order.financial_status}
</span>
<span className={`px-2.5 py-1 rounded-full text-[10px] font-black uppercase tracking-wider inline-flex items-center gap-1.5 ${getStatusStyles(order.fulfillment_status)}`}>
{order.fulfillment_status === "fulfilled" ? <CheckCircle2 className="w-3 h-3" /> : <Clock className="w-3 h-3" />}
{order.fulfillment_status || "Unfulfilled"}
</span>
</div>
)}
</div>
</div>
))}
</div>
)}
{/* Customer Info */}
{order.customer && (
<div className="flex items-center gap-2 mb-4">
<div className="w-7 h-7 rounded-full bg-white/10 flex items-center justify-center shrink-0">
<User className="w-3.5 h-3.5 text-slate-400" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-bold text-white truncate">
{order.customer.first_name} {order.customer.last_name}
</p>
<p className="text-xs text-slate-400 truncate">{order.email}</p>
</div>
</div>
)}
{/* Order Details */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-xs font-bold text-slate-400 uppercase tracking-wider">Total</span>
<span className="text-lg font-black text-white">{order.total_price} {order.currency}</span>
</div>
<div className="flex items-center gap-2 flex-wrap">
<span className={`px-2.5 py-1 rounded-full text-[10px] font-black uppercase tracking-wider inline-flex items-center gap-1.5 ${order.financial_status === "paid"
? "bg-emerald-500/20 text-emerald-400 border border-emerald-500/30"
: "bg-red-500/20 text-red-400 border border-red-500/30"
}`}>
{order.financial_status === "paid" && <CheckCircle2 className="w-3 h-3" />}
Payment: {order.financial_status}
</span>
<span className={`px-2.5 py-1 rounded-full text-[10px] font-black uppercase tracking-wider inline-flex items-center gap-1.5 ${getStatusStyles(order.fulfillment_status)}`}>
{order.fulfillment_status === "fulfilled" ? <CheckCircle2 className="w-3 h-3" /> : <Clock className="w-3 h-3" />}
{order.fulfillment_status || "Unfulfilled"}
</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
)

View File

@ -218,7 +218,7 @@ export default function SecuritySettingsPage() {
{/* MFA Section */}
<div className="bg-white/5 rounded-xl p-6 border border-white/10">
<div className="flex items-start justify-between mb-4">
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4 mb-4">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<Shield className="w-5 h-5 text-indigo-400" />
@ -232,7 +232,7 @@ export default function SecuritySettingsPage() {
<button
onClick={handleDisableMFA}
disabled={mfaLoading}
className="px-4 py-2 rounded-lg font-bold text-sm transition-colors flex items-center gap-2 bg-red-500/20 text-red-400 border border-red-500/30 hover:bg-red-500/30 disabled:opacity-50 disabled:cursor-not-allowed"
className="w-full sm:w-auto px-4 py-2 rounded-lg font-bold text-sm transition-colors flex items-center justify-center gap-2 bg-red-500/20 text-red-400 border border-red-500/30 hover:bg-red-500/30 disabled:opacity-50 disabled:cursor-not-allowed"
>
{mfaLoading ? (
<>
@ -247,7 +247,7 @@ export default function SecuritySettingsPage() {
<button
onClick={handleSetupMFA}
disabled={setupLoading || showSetup}
className="px-4 py-2 rounded-lg font-bold text-sm transition-colors flex items-center gap-2 bg-indigo-500 text-white hover:bg-indigo-600 disabled:opacity-50 disabled:cursor-not-allowed"
className="w-full sm:w-auto px-4 py-2 rounded-lg font-bold text-sm transition-colors flex items-center justify-center gap-2 bg-indigo-500 text-white hover:bg-indigo-600 disabled:opacity-50 disabled:cursor-not-allowed"
>
{setupLoading ? (
<>

View File

@ -20,6 +20,12 @@ export default function StoresPage() {
const [error, setError] = useState("")
const [switching, setSwitching] = useState<string | null>(null)
const [isAddModalOpen, setIsAddModalOpen] = useState(false)
/*
* Alignment Fix Details:
* - Use React `createPortal` to render the modal backdrop and content at the end of `document.body`.
* - This ensures `fixed inset-0` is always relative to the viewport, bypassing any parent `transform` styles (like in `PageTransition`) that create new containing blocks.
* - Verify that the `fixed inset-0` flex container correctly centers its child vertically and horizontally.
*/
const [deleteModal, setDeleteModal] = useState<{ isOpen: boolean; storeId: string | null; storeName: string; isActive: boolean }>({
isOpen: false,
storeId: null,
@ -103,10 +109,10 @@ export default function StoresPage() {
// Refresh stores list
await fetchStores()
// Notify StoreSwitcher to refresh
window.dispatchEvent(new CustomEvent("storeUpdated"))
// If deleted store was active, reload page to refresh dashboard data
if (deleteModal.isActive) {
window.location.reload()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@ -179,13 +179,19 @@ html {
will-change: auto;
}
/* Smooth transitions for all interactive elements */
* {
/* Smooth transitions for interactive elements */
a, button, input, select, textarea, [role="button"], [tabindex] {
transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
/* Fix select dropdown option readability across themes */
select option {
background-color: var(--background);
color: var(--foreground);
}
/* Optimize performance for animations */
button, a, input, select, textarea {
will-change: transform;

4
src/app/icon.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="8" fill="#4F46E5"/>
<path d="M16 6L10 18H14L12 26L22 14H18L20 6H16Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 224 B

View File

@ -15,10 +15,6 @@ const geistMono = Geist_Mono({
export const metadata: Metadata = {
title: "JUNOHUB",
description: "JUNOHUB - Your dropshipping and e-commerce management platform",
icons: {
icon: "/icon.svg",
apple: "/icon.svg",
},
};
export const viewport = {

View File

@ -205,25 +205,26 @@ export default function SignupPage() {
Log in here
</Link>
</p>
<div className="mt-8 pt-6 border-t border-white/10 flex flex-col items-center gap-1 text-[9px] text-slate-500 font-semibold uppercase tracking-widest">
<div className="flex items-center gap-4">
<Link
href="/privacy-policy"
className="hover:text-indigo-300 underline-offset-4 hover:underline normal-case"
>
Privacy Policy
</Link>
<span className="text-slate-600">•</span>
<Link
href="/terms-of-service"
className="hover:text-indigo-300 underline-offset-4 hover:underline normal-case"
>
Terms of Service
</Link>
</div>
</div>
</div>
</motion.div>
<div className="mt-6 flex flex-col items-center gap-1 text-[9px] text-slate-500 font-semibold uppercase tracking-widest">
<div className="flex items-center gap-4">
<Link
href="/privacy-policy"
className="hover:text-indigo-300 underline-offset-4 hover:underline normal-case"
>
Privacy Policy
</Link>
<span className="text-slate-600">•</span>
<Link
href="/terms-of-service"
className="hover:text-indigo-300 underline-offset-4 hover:underline normal-case"
>
Terms of Service
</Link>
</div>
</div>
</div>
)
}

View File

@ -1,6 +1,7 @@
"use client"
import { useState } from "react"
import { useState, useEffect } from "react"
import { createPortal } from "react-dom"
import { motion, AnimatePresence } from "framer-motion"
import {
X,
@ -11,6 +12,7 @@ import {
Cloud,
CheckCircle2
} from "lucide-react"
import { useTheme } from "@/components/ThemeProvider"
interface AddStoreModalProps {
isOpen: boolean
@ -23,6 +25,13 @@ export default function AddStoreModal({ isOpen, onClose, onSuccess }: AddStoreMo
const [loading, setLoading] = useState(false)
const [error, setError] = useState("")
const [success, setSuccess] = useState(false)
const [mounted, setMounted] = useState(false)
const { theme } = useTheme()
const isLight = theme === "light"
useEffect(() => {
setMounted(true)
}, [])
const [formData, setFormData] = useState({
businessName: "",
@ -133,271 +142,315 @@ export default function AddStoreModal({ isOpen, onClose, onSuccess }: AddStoreMo
}
}
return (
if (!mounted) return null
return createPortal(
<AnimatePresence>
{isOpen && (
<>
<div className="fixed inset-0 z-[100] flex items-center justify-center pointer-events-none">
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={handleClose}
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50"
className="absolute inset-0 bg-black/60 backdrop-blur-sm pointer-events-auto"
/>
{/* Modal */}
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 pointer-events-none">
<motion.div
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
className="bg-slate-900/95 backdrop-blur-xl rounded-2xl border border-white/10 shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden pointer-events-auto flex flex-col"
>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-white/10">
<div>
<h2 className="text-2xl font-bold text-white">Add New Store</h2>
<p className="text-sm text-slate-400 mt-1">Connect a Shopify store to your account</p>
</div>
<button
onClick={handleClose}
disabled={loading || success}
className="w-10 h-10 rounded-xl bg-white/10 hover:bg-white/15 flex items-center justify-center transition-colors disabled:opacity-50 text-slate-300"
>
<X className="w-5 h-5" />
</button>
<motion.div
initial={{ scale: 0.95, opacity: 0, y: 20 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.95, opacity: 0, y: 20 }}
className={`relative rounded-2xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden pointer-events-auto flex flex-col mx-4 ${isLight
? "bg-white border border-slate-200"
: "bg-slate-900 border border-white/10"
}`}
>
{/* Header */}
<div className={`flex items-center justify-between p-6 border-b ${isLight ? "border-slate-100" : "border-white/10"
}`}>
<div>
<h2 className={`text-2xl font-bold ${isLight ? "text-slate-900" : "text-white"}`}>Add New Store</h2>
<p className={`text-sm mt-1 ${isLight ? "text-slate-500" : "text-slate-400"}`}>Connect a Shopify store to your account</p>
</div>
<button
onClick={handleClose}
disabled={loading || success}
className={`w-10 h-10 rounded-xl flex items-center justify-center transition-colors disabled:opacity-50 ${isLight
? "bg-slate-100 hover:bg-slate-200 text-slate-600"
: "bg-white/10 hover:bg-white/15 text-slate-300"
}`}
>
<X className="w-5 h-5" />
</button>
</div>
{/* Progress Bar */}
<div className="px-6 pt-6">
<div className="flex justify-between text-[10px] font-bold text-slate-500 mb-2 uppercase tracking-wider">
<span className={step >= 1 ? "text-indigo-400" : ""}>Store Details</span>
<span className={step >= 2 ? "text-indigo-400" : ""}>API Credentials</span>
<span className={step >= 3 ? "text-indigo-400" : ""}>Complete</span>
</div>
<div className="h-1.5 bg-white/10 rounded-full overflow-hidden">
<motion.div
initial={{ width: "0%" }}
animate={{ width: `${(step / 3) * 100}%` }}
className="h-full bg-indigo-500 transition-all duration-500"
/>
</div>
{/* Progress Bar */}
<div className="px-6 pt-6">
<div className="flex justify-between text-[10px] font-bold text-slate-500 mb-2 uppercase tracking-wider">
<span className={step >= 1 ? "text-indigo-500" : ""}>Store Details</span>
<span className={step >= 2 ? "text-indigo-500" : ""}>API Credentials</span>
<span className={step >= 3 ? "text-indigo-500" : ""}>Complete</span>
</div>
<div className={`h-1.5 rounded-full overflow-hidden ${isLight ? "bg-slate-100" : "bg-white/10"
}`}>
<motion.div
initial={{ width: "0%" }}
animate={{ width: `${(step / 3) * 100}%` }}
className="h-full bg-indigo-500 transition-all duration-500"
/>
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-6">
<AnimatePresence mode="wait">
{success ? (
<motion.div
key="success"
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="flex flex-col items-center justify-center py-12 text-center"
>
<div className="w-16 h-16 rounded-full bg-emerald-500/20 border border-emerald-400/30 flex items-center justify-center mb-4">
<CheckCircle2 className="w-8 h-8 text-emerald-400" />
</div>
<h3 className="text-xl font-bold text-white mb-2">Store Added Successfully!</h3>
<p className="text-slate-400">Your store has been connected and is ready to use.</p>
</motion.div>
) : step === 1 ? (
<motion.div
key="step1"
initial={{ x: 20, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: -20, opacity: 0 }}
className="space-y-6"
>
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-indigo-500/20 border border-indigo-400/20 flex items-center justify-center">
<Globe className="w-6 h-6 text-indigo-400" />
</div>
<div>
<h3 className="text-lg font-bold text-white">Store Information</h3>
<p className="text-sm text-slate-400">Enter your store details</p>
</div>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-bold text-slate-300 mb-2">
Business Name <span className="text-red-400">*</span>
</label>
<input
name="businessName"
value={formData.businessName}
onChange={handleInputChange}
placeholder="e.g. My Store"
className="w-full h-12 px-4 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-slate-500 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/30 transition-all"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-bold text-slate-300 mb-2">
Shopify Store Name <span className="text-red-400">*</span>
</label>
<input
name="storeName"
value={formData.storeName}
onChange={handleInputChange}
placeholder="my-store"
className="w-full h-12 px-4 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-slate-500 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/30 transition-all"
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-300 mb-2">
Domain <span className="text-red-400">*</span>
</label>
<input
name="domain"
value={formData.domain}
onChange={handleInputChange}
placeholder="my-store.myshopify.com"
className="w-full h-12 px-4 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-slate-500 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/30 transition-all"
/>
</div>
</div>
</div>
</motion.div>
) : step === 2 ? (
<motion.div
key="step2"
initial={{ x: 20, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: -20, opacity: 0 }}
className="space-y-6"
>
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-indigo-500/20 border border-indigo-400/20 flex items-center justify-center">
<Key className="w-6 h-6 text-indigo-400" />
</div>
<div>
<h3 className="text-lg font-bold text-white">API Credentials</h3>
<p className="text-sm text-slate-400">Provide your Shopify Admin API credentials</p>
</div>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-bold text-slate-300 mb-2">
Admin API Key <span className="text-red-400">*</span>
</label>
<input
name="apiKey"
type="password"
value={formData.apiKey}
onChange={handleInputChange}
placeholder="Enter API Key"
className="w-full h-12 px-4 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-slate-500 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/30 transition-all"
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-300 mb-2">
Admin API Secret <span className="text-red-400">*</span>
</label>
<input
name="apiSecret"
type="password"
value={formData.apiSecret}
onChange={handleInputChange}
placeholder="Enter API Secret"
className="w-full h-12 px-4 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-slate-500 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/30 transition-all"
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-300 mb-2">
Access Token <span className="text-red-400">*</span>
</label>
<input
name="accessToken"
type="password"
value={formData.accessToken}
onChange={handleInputChange}
placeholder="shpat_..."
className="w-full h-12 px-4 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-slate-500 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/30 transition-all"
/>
</div>
</div>
</motion.div>
) : (
<motion.div
key="step3"
initial={{ x: 20, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: -20, opacity: 0 }}
className="flex flex-col items-center justify-center py-8 text-center"
>
<div className="w-16 h-16 rounded-full bg-indigo-500/20 border border-indigo-400/20 flex items-center justify-center mb-4 relative">
<Cloud className={`w-8 h-8 ${loading ? "text-indigo-400 animate-pulse" : "text-indigo-400"}`} />
{loading && (
<motion.div
animate={{ rotate: 360 }}
transition={{ repeat: Infinity, duration: 2, ease: "linear" }}
className="absolute inset-0 border-2 border-indigo-500 border-t-transparent rounded-full"
/>
)}
</div>
<h3 className="text-lg font-bold text-white mb-2">Connecting Store</h3>
<p className="text-sm text-slate-400 max-w-sm">
We&apos;re connecting your store and setting up the integration...
</p>
</motion.div>
)}
</AnimatePresence>
{error && (
{/* Content */}
<div className="flex-1 overflow-y-auto p-6">
<AnimatePresence mode="wait">
{success ? (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="mt-4 p-4 rounded-xl bg-red-500/10 border border-red-500/20 flex items-start gap-3"
key="success"
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="flex flex-col items-center justify-center py-12 text-center"
>
<AlertCircle className="w-5 h-5 text-red-400 shrink-0 mt-0.5" />
<p className="text-sm text-red-400">{error}</p>
<div className={`w-16 h-16 rounded-full flex items-center justify-center mb-4 ${isLight
? "bg-emerald-100 border border-emerald-200"
: "bg-emerald-500/20 border border-emerald-400/30"
}`}>
<CheckCircle2 className={`w-8 h-8 ${isLight ? "text-emerald-600" : "text-emerald-400"}`} />
</div>
<h3 className={`text-xl font-bold mb-2 ${isLight ? "text-slate-900" : "text-white"}`}>Store Added Successfully!</h3>
<p className={`${isLight ? "text-slate-600" : "text-slate-400"}`}>Your store has been connected and is ready to use.</p>
</motion.div>
) : step === 1 ? (
<motion.div
key="step1"
initial={{ x: 20, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: -20, opacity: 0 }}
className="space-y-6"
>
<div className="flex items-center gap-4">
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${isLight
? "bg-indigo-50 border border-indigo-100"
: "bg-indigo-500/20 border border-indigo-400/20"
}`}>
<Globe className="w-6 h-6 text-indigo-500" />
</div>
<div>
<h3 className={`text-lg font-bold ${isLight ? "text-slate-900" : "text-white"}`}>Store Information</h3>
<p className={`text-sm ${isLight ? "text-slate-500" : "text-slate-400"}`}>Enter your store details</p>
</div>
</div>
<div className="space-y-4">
<div>
<label className={`block text-sm font-bold mb-2 ${isLight ? "text-slate-700" : "text-slate-300"}`}>
Business Name <span className="text-red-500">*</span>
</label>
<input
name="businessName"
value={formData.businessName}
onChange={handleInputChange}
placeholder="e.g. My Store"
className={`w-full h-12 px-4 rounded-xl transition-all focus:outline-none focus:ring-2 focus:ring-indigo-500/30 ${isLight
? "bg-slate-50 border border-slate-200 text-slate-900 placeholder:text-slate-400 focus:border-indigo-500"
: "bg-white/5 border border-white/10 text-white placeholder:text-slate-500 focus:border-indigo-500"
}`}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className={`block text-sm font-bold mb-2 ${isLight ? "text-slate-700" : "text-slate-300"}`}>
Shopify Store Name <span className="text-red-500">*</span>
</label>
<input
name="storeName"
value={formData.storeName}
onChange={handleInputChange}
placeholder="my-store"
className={`w-full h-12 px-4 rounded-xl transition-all focus:outline-none focus:ring-2 focus:ring-indigo-500/30 ${isLight
? "bg-slate-50 border border-slate-200 text-slate-900 placeholder:text-slate-400 focus:border-indigo-500"
: "bg-white/5 border border-white/10 text-white placeholder:text-slate-500 focus:border-indigo-500"
}`}
/>
</div>
<div>
<label className={`block text-sm font-bold mb-2 ${isLight ? "text-slate-700" : "text-slate-300"}`}>
Domain <span className="text-red-500">*</span>
</label>
<input
name="domain"
value={formData.domain}
onChange={handleInputChange}
placeholder="my-store.myshopify.com"
className={`w-full h-12 px-4 rounded-xl transition-all focus:outline-none focus:ring-2 focus:ring-indigo-500/30 ${isLight
? "bg-slate-50 border border-slate-200 text-slate-900 placeholder:text-slate-400 focus:border-indigo-500"
: "bg-white/5 border border-white/10 text-white placeholder:text-slate-500 focus:border-indigo-500"
}`}
/>
</div>
</div>
</div>
</motion.div>
) : step === 2 ? (
<motion.div
key="step2"
initial={{ x: 20, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: -20, opacity: 0 }}
className="space-y-6"
>
<div className="flex items-center gap-4">
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${isLight
? "bg-indigo-50 border border-indigo-100"
: "bg-indigo-500/20 border border-indigo-400/20"
}`}>
<Key className="w-6 h-6 text-indigo-500" />
</div>
<div>
<h3 className={`text-lg font-bold ${isLight ? "text-slate-900" : "text-white"}`}>API Credentials</h3>
<p className={`text-sm ${isLight ? "text-slate-500" : "text-slate-400"}`}>Provide your Shopify Admin API credentials</p>
</div>
</div>
<div className="space-y-4">
<div>
<label className={`block text-sm font-bold mb-2 ${isLight ? "text-slate-700" : "text-slate-300"}`}>
Admin API Key <span className="text-red-500">*</span>
</label>
<input
name="apiKey"
type="password"
value={formData.apiKey}
onChange={handleInputChange}
placeholder="Enter API Key"
className={`w-full h-12 px-4 rounded-xl transition-all focus:outline-none focus:ring-2 focus:ring-indigo-500/30 ${isLight
? "bg-slate-50 border border-slate-200 text-slate-900 placeholder:text-slate-400 focus:border-indigo-500"
: "bg-white/5 border border-white/10 text-white placeholder:text-slate-500 focus:border-indigo-500"
}`}
/>
</div>
<div>
<label className={`block text-sm font-bold mb-2 ${isLight ? "text-slate-700" : "text-slate-300"}`}>
Admin API Secret <span className="text-red-500">*</span>
</label>
<input
name="apiSecret"
type="password"
value={formData.apiSecret}
onChange={handleInputChange}
placeholder="Enter API Secret"
className={`w-full h-12 px-4 rounded-xl transition-all focus:outline-none focus:ring-2 focus:ring-indigo-500/30 ${isLight
? "bg-slate-50 border border-slate-200 text-slate-900 placeholder:text-slate-400 focus:border-indigo-500"
: "bg-white/5 border border-white/10 text-white placeholder:text-slate-500 focus:border-indigo-500"
}`}
/>
</div>
<div>
<label className={`block text-sm font-bold mb-2 ${isLight ? "text-slate-700" : "text-slate-300"}`}>
Access Token <span className="text-red-500">*</span>
</label>
<input
name="accessToken"
type="password"
value={formData.accessToken}
onChange={handleInputChange}
placeholder="shpat_..."
className={`w-full h-12 px-4 rounded-xl transition-all focus:outline-none focus:ring-2 focus:ring-indigo-500/30 ${isLight
? "bg-slate-50 border border-slate-200 text-slate-900 placeholder:text-slate-400 focus:border-indigo-500"
: "bg-white/5 border border-white/10 text-white placeholder:text-slate-500 focus:border-indigo-500"
}`}
/>
</div>
</div>
</motion.div>
) : (
<motion.div
key="step3"
initial={{ x: 20, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: -20, opacity: 0 }}
className="flex flex-col items-center justify-center py-8 text-center"
>
<div className={`w-16 h-16 rounded-full flex items-center justify-center mb-4 relative ${isLight ? "bg-indigo-50 border border-indigo-100" : "bg-indigo-500/20 border border-indigo-400/20"
}`}>
<Cloud className={`w-8 h-8 ${loading ? "text-indigo-500 animate-pulse" : "text-indigo-500"}`} />
{loading && (
<motion.div
animate={{ rotate: 360 }}
transition={{ repeat: Infinity, duration: 2, ease: "linear" }}
className="absolute inset-0 border-2 border-indigo-500 border-t-transparent rounded-full"
/>
)}
</div>
<h3 className={`text-lg font-bold mb-2 ${isLight ? "text-slate-900" : "text-white"}`}>Connecting Store</h3>
<p className={`text-sm max-w-sm ${isLight ? "text-slate-500" : "text-slate-400"}`}>
We&apos;re connecting your store and setting up the integration...
</p>
</motion.div>
)}
</div>
</AnimatePresence>
{/* Footer */}
{!success && (
<div className="p-6 border-t border-white/10 flex items-center justify-between gap-4">
<button
onClick={prevStep}
disabled={step === 1 || loading}
className="h-11 px-6 bg-white/10 text-slate-300 rounded-xl font-bold hover:bg-white/15 border border-white/10 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
Back
</button>
{step < 3 ? (
<button
onClick={nextStep}
disabled={loading}
className="h-11 px-6 bg-indigo-500 text-white rounded-xl font-bold hover:bg-indigo-600 border border-indigo-400/20 transition-all disabled:opacity-50 flex items-center gap-2 ml-auto"
>
Continue
</button>
) : (
<button
onClick={handleSubmit}
disabled={loading}
className="h-11 px-6 bg-indigo-500 text-white rounded-xl font-bold hover:bg-indigo-600 border border-indigo-400/20 transition-all disabled:opacity-50 flex items-center gap-2 ml-auto"
>
{loading ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Connecting...
</>
) : (
"Connect Store"
)}
</button>
)}
</div>
{error && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className={`mt-4 p-4 rounded-xl flex items-start gap-3 ${isLight
? "bg-red-50 border border-red-100"
: "bg-red-500/10 border border-red-500/20"
}`}
>
<AlertCircle className="w-5 h-5 text-red-500 shrink-0 mt-0.5" />
<p className={`text-sm ${isLight ? "text-red-700" : "text-red-400"}`}>{error}</p>
</motion.div>
)}
</motion.div>
</div>
</>
</div>
{/* Footer */}
{!success && (
<div className={`p-6 border-t flex items-center justify-between gap-4 ${isLight ? "border-slate-100" : "border-white/10"
}`}>
<button
onClick={prevStep}
disabled={step === 1 || loading}
className={`h-11 px-6 rounded-xl font-bold transition-all disabled:opacity-50 disabled:cursor-not-allowed border ${isLight
? "bg-white text-slate-700 hover:bg-slate-50 border-slate-200"
: "bg-white/10 text-slate-300 hover:bg-white/15 border-white/10"
}`}
>
Back
</button>
{step < 3 ? (
<button
onClick={nextStep}
disabled={loading}
className="h-11 px-6 bg-indigo-500 text-white rounded-xl font-bold hover:bg-indigo-600 border border-indigo-400/20 transition-all disabled:opacity-50 flex items-center gap-2 ml-auto"
>
Continue
</button>
) : (
<button
onClick={handleSubmit}
disabled={loading}
className="h-11 px-6 bg-indigo-500 text-white rounded-xl font-bold hover:bg-indigo-600 border border-indigo-400/20 transition-all disabled:opacity-50 flex items-center gap-2 ml-auto"
>
{loading ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Connecting...
</>
) : (
"Connect Store"
)}
</button>
)}
</div>
)}
</motion.div>
</div>
)}
</AnimatePresence>
</AnimatePresence>,
document.body
)
}

View File

@ -3,6 +3,7 @@
import { useState } from "react"
import { motion, AnimatePresence } from "framer-motion"
import { X, AlertTriangle, Loader2, Trash2 } from "lucide-react"
import { useTheme } from "@/components/ThemeProvider"
interface DeleteStoreModalProps {
isOpen: boolean
@ -21,6 +22,8 @@ export default function DeleteStoreModal({
}: DeleteStoreModalProps) {
const [loading, setLoading] = useState(false)
const [error, setError] = useState("")
const { theme } = useTheme()
const isLight = theme === "light"
const handleDelete = async () => {
setLoading(true)
@ -55,26 +58,33 @@ export default function DeleteStoreModal({
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
className="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden pointer-events-auto"
className={`rounded-2xl shadow-2xl w-full max-w-md overflow-hidden pointer-events-auto ${isLight
? "bg-white"
: "bg-slate-900/95 backdrop-blur-xl border border-white/10"
}`}
>
{/* Header */}
<div className="p-6 border-b border-slate-100">
<div className={`p-6 border-b ${isLight ? "border-slate-100" : "border-white/10"}`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-red-100 flex items-center justify-center">
<AlertTriangle className="w-6 h-6 text-red-600" />
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${isLight ? "bg-red-100" : "bg-red-500/20 border border-red-500/30"
}`}>
<AlertTriangle className={`w-6 h-6 ${isLight ? "text-red-600" : "text-red-400"}`} />
</div>
<div>
<h2 className="text-xl font-bold text-slate-900">Delete Store</h2>
<p className="text-sm text-slate-500 mt-0.5">This action cannot be undone</p>
<h2 className={`text-xl font-bold ${isLight ? "text-slate-900" : "text-white"}`}>Delete Store</h2>
<p className={`text-sm mt-0.5 ${isLight ? "text-slate-500" : "text-slate-400"}`}>This action cannot be undone</p>
</div>
</div>
<button
onClick={onClose}
disabled={loading}
className="w-8 h-8 rounded-lg bg-slate-100 hover:bg-slate-200 flex items-center justify-center transition-colors disabled:opacity-50"
className={`w-8 h-8 rounded-lg flex items-center justify-center transition-colors disabled:opacity-50 ${isLight
? "bg-slate-100 hover:bg-slate-200 text-slate-600"
: "bg-white/10 hover:bg-white/15 text-slate-400"
}`}
>
<X className="w-4 h-4 text-slate-600" />
<X className="w-4 h-4" />
</button>
</div>
</div>
@ -82,27 +92,33 @@ export default function DeleteStoreModal({
{/* Content */}
<div className="p-6">
<div className="mb-6">
<p className="text-slate-700 mb-4">
Are you sure you want to delete <span className="font-bold text-slate-900">{storeName}</span>?
<p className={`mb-4 ${isLight ? "text-slate-700" : "text-slate-300"}`}>
Are you sure you want to delete <span className={`font-bold ${isLight ? "text-slate-900" : "text-white"}`}>{storeName}</span>?
</p>
{isActive && (
<div className="p-4 rounded-xl bg-amber-50 border border-amber-200 mb-4">
<div className={`p-4 rounded-xl border mb-4 ${isLight
? "bg-amber-50 border-amber-200"
: "bg-amber-500/10 border-amber-500/20"
}`}>
<div className="flex items-start gap-3">
<AlertTriangle className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" />
<AlertTriangle className={`w-5 h-5 shrink-0 mt-0.5 ${isLight ? "text-amber-600" : "text-amber-400"}`} />
<div>
<p className="text-sm font-bold text-amber-900 mb-1">Active Store</p>
<p className="text-sm text-amber-700">
<p className={`text-sm font-bold mb-1 ${isLight ? "text-amber-900" : "text-amber-300"}`}>Active Store</p>
<p className={`text-sm ${isLight ? "text-amber-700" : "text-amber-400/80"}`}>
This is your active store. We&apos;ll automatically switch to another store after deletion.
</p>
</div>
</div>
</div>
)}
<div className="p-4 rounded-xl bg-slate-50 border border-slate-200">
<p className="text-sm text-slate-600">
<div className={`p-4 rounded-xl border ${isLight
? "bg-slate-50 border-slate-200"
: "bg-white/5 border-white/10"
}`}>
<p className={`text-sm ${isLight ? "text-slate-600" : "text-slate-400"}`}>
This will permanently delete:
</p>
<ul className="mt-2 space-y-1 text-sm text-slate-600 ml-4 list-disc">
<ul className={`mt-2 space-y-1 text-sm ml-4 list-disc ${isLight ? "text-slate-600" : "text-slate-400"}`}>
<li>Store configuration and credentials</li>
<li>All cached products, orders, and customers</li>
<li>Sync metadata and webhook connections</li>
@ -114,20 +130,27 @@ export default function DeleteStoreModal({
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="mb-4 p-4 rounded-xl bg-red-50 border border-red-200 flex items-start gap-3"
className={`mb-4 p-4 rounded-xl border flex items-start gap-3 ${isLight
? "bg-red-50 border-red-200"
: "bg-red-500/10 border-red-500/20"
}`}
>
<AlertTriangle className="w-5 h-5 text-red-600 shrink-0 mt-0.5" />
<p className="text-sm text-red-700">{error}</p>
<AlertTriangle className={`w-5 h-5 shrink-0 mt-0.5 ${isLight ? "text-red-600" : "text-red-400"}`} />
<p className={`text-sm ${isLight ? "text-red-700" : "text-red-400"}`}>{error}</p>
</motion.div>
)}
</div>
{/* Footer */}
<div className="p-6 border-t border-slate-100 flex items-center justify-end gap-3">
<div className={`p-6 border-t flex items-center justify-end gap-3 ${isLight ? "border-slate-100" : "border-white/10"
}`}>
<button
onClick={onClose}
disabled={loading}
className="h-11 px-6 bg-slate-100 text-slate-700 rounded-xl font-bold hover:bg-slate-200 transition-all disabled:opacity-50"
className={`h-11 px-6 rounded-xl font-bold transition-all disabled:opacity-50 ${isLight
? "bg-slate-100 text-slate-700 hover:bg-slate-200"
: "bg-white/10 text-slate-300 hover:bg-white/15"
}`}
>
Cancel
</button>

View File

@ -0,0 +1,72 @@
"use client"
import { useState, useEffect } from "react"
import { Menu, X } from "lucide-react"
import { motion, AnimatePresence } from "framer-motion"
import { AdminSidebar } from "@/components/AdminSidebar"
export function MobileAdminSidebar() {
const [isOpen, setIsOpen] = useState(false)
useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden"
} else {
document.body.style.overflow = ""
}
return () => {
document.body.style.overflow = ""
}
}, [isOpen])
return (
<>
<button
onClick={() => setIsOpen(true)}
className="md:hidden p-2 hover:bg-white/10 rounded-lg transition-colors"
aria-label="Open admin menu"
>
<Menu className="w-5 h-5 text-white" />
</button>
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[90] md:hidden"
onClick={() => setIsOpen(false)}
/>
{/* Sidebar Panel */}
<motion.div
initial={{ x: -288 }}
animate={{ x: 0 }}
exit={{ x: -288 }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
className="fixed inset-y-0 left-0 w-72 bg-slate-900/95 backdrop-blur-xl border-r border-white/10 z-[100] md:hidden flex flex-col"
>
<div className="flex items-center justify-between p-4 border-b border-white/10">
<span className="text-sm font-bold text-white tracking-wide">Admin Panel</span>
<button
onClick={() => setIsOpen(false)}
className="p-1.5 hover:bg-white/10 rounded-lg transition-colors"
aria-label="Close admin menu"
>
<X className="w-4 h-4 text-slate-400" />
</button>
</div>
<div className="flex-1 overflow-y-auto">
<AdminSidebar />
</div>
</motion.div>
</>
)}
</AnimatePresence>
</>
)
}

View File

@ -1,6 +1,7 @@
"use client"
import { useState, useEffect } from "react"
import { createPortal } from "react-dom"
import { usePathname } from "next/navigation"
import { motion, AnimatePresence } from "framer-motion"
import { X, Menu, Zap } from "lucide-react"
@ -8,8 +9,13 @@ import { Sidebar } from "./Sidebar"
export function MobileSidebar() {
const [isOpen, setIsOpen] = useState(false)
const [mounted, setMounted] = useState(false)
const pathname = usePathname()
useEffect(() => {
setMounted(true)
}, [])
// Close sidebar when route changes
useEffect(() => {
setIsOpen(false)
@ -40,14 +46,14 @@ export function MobileSidebar() {
{/* Mobile Sidebar Overlay */}
<AnimatePresence>
{isOpen && (
{isOpen && mounted && createPortal(
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[90] md:hidden"
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[999] md:hidden pointer-events-auto"
onClick={() => setIsOpen(false)}
/>
<motion.div
@ -55,30 +61,31 @@ export function MobileSidebar() {
animate={{ x: 0 }}
exit={{ x: -288 }}
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-[100] md:hidden flex flex-col"
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"
>
{/* Header with close button */}
<div className="flex items-center justify-between p-4 border-b border-white/10">
<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">
<Zap className="w-5 h-5 text-white" />
{/* Header with close button */}
<div className="flex items-center justify-between p-4 border-b border-white/10">
<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">
<Zap className="w-5 h-5 text-white" />
</div>
<h1 className="text-xl font-bold tracking-tight text-white">JUNOHUB</h1>
</div>
<h1 className="text-xl font-bold tracking-tight text-white">JUNOHUB</h1>
<button
onClick={() => setIsOpen(false)}
className="p-2 rounded-lg bg-white/10 border border-white/10 text-white hover:bg-white/15 transition-all"
aria-label="Close menu"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Sidebar Content */}
<div className="flex-1 overflow-y-auto">
<Sidebar />
</div>
<button
onClick={() => setIsOpen(false)}
className="p-2 rounded-lg bg-white/10 border border-white/10 text-white hover:bg-white/15 transition-all"
aria-label="Close menu"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Sidebar Content */}
<div className="flex-1 overflow-y-auto">
<Sidebar />
</div>
</motion.div>
</>
</>,
document.body
)}
</AnimatePresence>
</>

View File

@ -50,9 +50,9 @@ export function MobileSupplierSidebar() {
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-[90] md:hidden"
/>
<motion.div
initial={{ x: -300 }}
initial={{ x: -288 }}
animate={{ x: 0 }}
exit={{ x: -300 }}
exit={{ x: -288 }}
transition={{ type: "spring", damping: 25, stiffness: 200 }}
className="fixed left-0 top-0 bottom-0 w-72 bg-slate-900/95 backdrop-blur-xl border-r border-white/10 z-[100] md:hidden overflow-y-auto"
>

View File

@ -3,6 +3,7 @@
import { useEffect, useState, useRef } from "react"
import { Bell, Loader2, CheckCircle2, Clock, XCircle, Mail, Package, ShoppingCart, X } from "lucide-react"
import { formatDistanceToNow } from "date-fns"
import { motion, AnimatePresence } from "framer-motion"
import { useTheme } from "@/components/ThemeProvider"
interface Notification {
@ -27,12 +28,12 @@ export default function NotificationBell() {
useEffect(() => {
fetchNotifications()
// Poll for new notifications every 5 seconds
const interval = setInterval(() => {
fetchNotifications()
}, 5000)
return () => clearInterval(interval)
}, [])
@ -125,14 +126,12 @@ export default function NotificationBell() {
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className={`relative p-2 rounded-lg transition-colors ${
isLight ? "hover:bg-slate-100" : "hover:bg-white/10"
}`}
className={`relative p-2 rounded-lg transition-colors ${isLight ? "hover:bg-slate-100" : "hover:bg-white/10"
}`}
>
<Bell
className={`w-5 h-5 ${
isLight ? "text-slate-600" : "text-slate-400"
}`}
className={`w-5 h-5 ${isLight ? "text-slate-600" : "text-slate-400"
}`}
/>
{unreadCount > 0 && (
<span className="absolute top-1 right-1 flex h-4 w-4 items-center justify-center rounded-full bg-red-500 text-[10px] font-bold text-white">
@ -141,178 +140,167 @@ export default function NotificationBell() {
)}
</button>
{isOpen && (
<div
className={`absolute right-0 top-full z-50 mt-2 flex max-h-[600px] w-80 sm:w-96 max-w-[calc(100vw-2rem)] flex-col rounded-xl border shadow-xl ${
isLight
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, scale: 0.95, y: -4 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: -4 }}
transition={{ duration: 0.15, ease: "easeOut" }}
className={`absolute right-0 top-full z-50 mt-2 flex max-h-[600px] w-80 sm:w-96 max-w-[calc(100vw-2rem)] flex-col rounded-xl border shadow-xl ${isLight
? "bg-white border-slate-200"
: "bg-slate-800 border-white/10"
}`}
>
<div
className={`flex items-center justify-between border-b p-4 ${
isLight ? "border-slate-200" : "border-white/10"
}`}
>
<h3
className={`font-bold ${
isLight ? "text-slate-900" : "text-white"
}`}
>
<div
className={`flex items-center justify-between border-b p-4 ${isLight ? "border-slate-200" : "border-white/10"
}`}
>
Notifications
</h3>
{unreadCount > 0 && (
<button
onClick={markAllAsRead}
className={`text-xs font-medium ${
isLight
<h3
className={`font-bold ${isLight ? "text-slate-900" : "text-white"
}`}
>
Notifications
</h3>
{unreadCount > 0 && (
<button
onClick={markAllAsRead}
className={`text-xs font-medium ${isLight
? "text-indigo-600 hover:text-indigo-500"
: "text-indigo-400 hover:text-indigo-300"
}`}
>
Mark all as read
</button>
)}
</div>
}`}
>
Mark all as read
</button>
)}
</div>
<div className="overflow-y-auto flex-1">
{loading ? (
<div className="flex items-center justify-center py-8">
<Loader2
className={`h-6 w-6 animate-spin ${
isLight
<div className="overflow-y-auto flex-1">
{loading ? (
<div className="flex items-center justify-center py-8">
<Loader2
className={`h-6 w-6 animate-spin ${isLight
? "text-indigo-600"
: "text-indigo-400"
}`}
/>
</div>
) : notifications.length === 0 ? (
<div className="p-8 text-center">
<Bell
className={`mx-auto mb-3 h-12 w-12 opacity-50 ${
isLight
}`}
/>
</div>
) : notifications.length === 0 ? (
<div className="p-8 text-center">
<Bell
className={`mx-auto mb-3 h-12 w-12 opacity-50 ${isLight
? "text-slate-400"
: "text-slate-400"
}`}
/>
<p
className={`text-sm ${
isLight
}`}
/>
<p
className={`text-sm ${isLight
? "text-slate-500"
: "text-slate-400"
}`}
>
No notifications yet
</p>
</div>
) : (
<div
className={`divide-y ${
isLight
}`}
>
No notifications yet
</p>
</div>
) : (
<div
className={`divide-y ${isLight
? "divide-slate-100"
: "divide-white/10"
}`}
>
{notifications.map((notification) => (
<div
key={notification.id}
className={`p-4 transition-colors ${
isLight
}`}
>
{notifications.map((notification) => (
<div
key={notification.id}
className={`p-4 transition-colors ${isLight
? "hover:bg-slate-50"
: "hover:bg-white/5"
} ${
!notification.read
? isLight
? "bg-indigo-50"
: "bg-indigo-500/10"
: ""
}`}
>
<div className="flex items-start gap-3">
<div className="mt-0.5 flex-shrink-0">
{getNotificationIcon(notification.type)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<h4
className={`mb-1 text-sm font-semibold ${
isLight
} ${!notification.read
? isLight
? "bg-indigo-50"
: "bg-indigo-500/10"
: ""
}`}
>
<div className="flex items-start gap-3">
<div className="mt-0.5 flex-shrink-0">
{getNotificationIcon(notification.type)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<h4
className={`mb-1 text-sm font-semibold ${isLight
? "text-slate-900"
: "text-white"
}`}
>
{notification.title}
</h4>
<p
className={`mb-2 text-xs ${
isLight
}`}
>
{notification.title}
</h4>
<p
className={`mb-2 text-xs ${isLight
? "text-slate-600"
: "text-slate-300"
}`}
>
{notification.message}
</p>
<p
className={`text-[10px] ${
isLight
}`}
>
{notification.message}
</p>
<p
className={`text-[10px] ${isLight
? "text-slate-400"
: "text-slate-500"
}`}
>
{formatDistanceToNow(new Date(notification.createdAt), {
addSuffix: true
})}
</p>
</div>
<div className="flex items-center gap-1 flex-shrink-0">
{!notification.read && (
<button
onClick={() => markAsRead(notification.id)}
className={`rounded p-1 transition-colors ${
isLight
}`}
>
{formatDistanceToNow(new Date(notification.createdAt), {
addSuffix: true
})}
</p>
</div>
<div className="flex items-center gap-1 flex-shrink-0">
{!notification.read && (
<button
onClick={() => markAsRead(notification.id)}
className={`rounded p-1 transition-colors ${isLight
? "hover:bg-slate-100"
: "hover:bg-white/10"
}`}
title="Mark as read"
>
<CheckCircle2
className={`h-3 w-3 ${
isLight
}`}
title="Mark as read"
>
<CheckCircle2
className={`h-3 w-3 ${isLight
? "text-slate-400"
: "text-slate-400"
}`}
/>
</button>
)}
<button
onClick={() => deleteNotification(notification.id)}
className={`rounded p-1 transition-colors ${
isLight
}`}
/>
</button>
)}
<button
onClick={() => deleteNotification(notification.id)}
className={`rounded p-1 transition-colors ${isLight
? "hover:bg-slate-100"
: "hover:bg-white/10"
}`}
title="Delete"
>
<X
className={`h-3 w-3 ${
isLight
}`}
title="Delete"
>
<X
className={`h-3 w-3 ${isLight
? "text-slate-400"
: "text-slate-400"
}`}
/>
</button>
}`}
/>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
)}
))}
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
)
}

View File

@ -108,17 +108,8 @@ export default function OrderDetailsModal({ isOpen, onClose, orderId, orderName
if (!isOpen) return null
return (
<div
className="fixed inset-0 z-[9999] bg-black/70 backdrop-blur-md"
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
overflowY: 'auto',
overflowX: 'hidden'
}}
<div
className="fixed inset-0 z-[9999] bg-black/70 backdrop-blur-md overflow-y-auto overflow-x-hidden"
onClick={(e) => {
if (e.target === e.currentTarget) {
onClose()
@ -126,12 +117,8 @@ export default function OrderDetailsModal({ isOpen, onClose, orderId, orderName
}}
>
<div className="min-h-screen flex items-center justify-center p-4 py-8">
<div
className="bg-slate-900 rounded-2xl border border-white/10 w-full max-w-5xl max-h-[90vh] overflow-hidden flex flex-col shadow-2xl mx-auto"
style={{
maxWidth: 'min(90vw, 80rem)',
width: '100%'
}}
<div
className="bg-slate-900 rounded-2xl border border-white/10 w-full max-w-5xl max-h-[90vh] overflow-hidden flex flex-col shadow-2xl mx-auto"
>
{/* Header */}
<div className="flex items-center justify-between p-4 md:p-6 border-b border-white/10 flex-shrink-0 bg-slate-900/95 backdrop-blur-sm">
@ -154,201 +141,199 @@ export default function OrderDetailsModal({ isOpen, onClose, orderId, orderName
{/* Content */}
<div className="flex-1 overflow-y-auto overflow-x-hidden p-4 md:p-6 min-h-0 bg-slate-900 max-w-full" style={{ maxHeight: 'calc(90vh - 140px)' }}>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-slate-400">Loading order details...</div>
</div>
) : error ? (
<div className="flex items-center justify-center h-64">
<div className="text-red-400">{error}</div>
</div>
) : order ? (
<div className="space-y-6 max-w-full overflow-hidden">
{/* Order Summary - Top Section */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white/5 rounded-xl p-5 border border-white/10">
<div className="flex items-center gap-2 text-slate-400 mb-3">
<Calendar className="w-4 h-4" />
<span className="text-xs font-bold uppercase tracking-wider">Order Date</span>
</div>
<p className="text-white font-bold text-lg">{new Date(order.created_at).toLocaleString()}</p>
</div>
<div className="bg-white/5 rounded-xl p-5 border border-white/10">
<div className="flex items-center gap-2 text-slate-400 mb-3">
<DollarSign className="w-4 h-4" />
<span className="text-xs font-bold uppercase tracking-wider">Total Amount</span>
</div>
<p className="text-white font-bold text-2xl">{order.total_price} {order.currency}</p>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-slate-400">Loading order details...</div>
</div>
{/* Status Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white/5 rounded-xl p-5 border border-white/10">
<span className="text-xs font-bold uppercase text-slate-400 tracking-wider block mb-3">Payment Status</span>
<div>
<span className={`px-3 py-1.5 rounded-full text-xs font-black uppercase tracking-wider inline-flex items-center gap-1.5 ${
order.financial_status === "paid"
? "bg-emerald-500/20 text-emerald-400 border border-emerald-500/30"
: "bg-red-500/20 text-red-400 border border-red-500/30"
}`}>
{order.financial_status === "paid" && <CheckCircle2 className="w-3 h-3" />}
{order.financial_status}
</span>
</div>
</div>
<div className="bg-white/5 rounded-xl p-5 border border-white/10">
<span className="text-xs font-bold uppercase text-slate-400 tracking-wider block mb-3">Fulfillment Status</span>
<div>
<span className={`px-3 py-1.5 rounded-full text-xs font-black uppercase tracking-wider inline-flex items-center gap-1.5 ${
order.fulfillment_status === "fulfilled"
? "bg-emerald-500/20 text-emerald-400 border border-emerald-500/30"
: "bg-blue-500/20 text-blue-400 border border-blue-500/30"
}`}>
{order.fulfillment_status === "fulfilled" ? <CheckCircle2 className="w-3 h-3" /> : <Clock className="w-3 h-3" />}
{order.fulfillment_status || "Unfulfilled"}
</span>
</div>
</div>
) : error ? (
<div className="flex items-center justify-center h-64">
<div className="text-red-400">{error}</div>
</div>
{/* Main Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 min-w-0 max-w-full">
{/* Left Column - Customer & Shipping */}
<div className="lg:col-span-1 space-y-6 min-w-0 max-w-full">
{/* Customer Info */}
{order.customer && (
<div className="bg-white/5 rounded-xl p-5 border border-white/10">
<div className="flex items-center gap-2 text-slate-400 mb-4">
<User className="w-4 h-4" />
<span className="text-xs font-bold uppercase tracking-wider">Customer</span>
</div>
<div className="space-y-3">
<p className="text-white font-bold text-base">
{order.customer.first_name} {order.customer.last_name}
</p>
<div className="flex items-center gap-2 text-slate-300 text-sm min-w-0">
<Mail className="w-3.5 h-3.5 text-slate-500 flex-shrink-0" />
<span className="break-all min-w-0">{order.customer.email}</span>
</div>
{order.customer.phone && (
<div className="flex items-center gap-2 text-slate-300 text-sm">
<Phone className="w-3.5 h-3.5 text-slate-500" />
<span>{order.customer.phone}</span>
</div>
)}
</div>
) : order ? (
<div className="space-y-6 max-w-full overflow-hidden">
{/* Order Summary - Top Section */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white/5 rounded-xl p-5 border border-white/10">
<div className="flex items-center gap-2 text-slate-400 mb-3">
<Calendar className="w-4 h-4" />
<span className="text-xs font-bold uppercase tracking-wider">Order Date</span>
</div>
)}
<p className="text-white font-bold text-lg">{new Date(order.created_at).toLocaleString()}</p>
</div>
<div className="bg-white/5 rounded-xl p-5 border border-white/10">
<div className="flex items-center gap-2 text-slate-400 mb-3">
<DollarSign className="w-4 h-4" />
<span className="text-xs font-bold uppercase tracking-wider">Total Amount</span>
</div>
<p className="text-white font-bold text-2xl">{order.total_price} {order.currency}</p>
</div>
</div>
{/* Shipping Address */}
{order.shipping_address && (
<div className="bg-white/5 rounded-xl p-5 border border-white/10">
<div className="flex items-center gap-2 text-slate-400 mb-4">
<MapPin className="w-4 h-4" />
<span className="text-xs font-bold uppercase tracking-wider">Shipping Address</span>
</div>
<div className="text-slate-300 text-sm space-y-1.5 break-words">
<p className="font-medium text-white break-words">
{order.shipping_address.first_name} {order.shipping_address.last_name}
</p>
<p className="break-words">{order.shipping_address.address1}</p>
{order.shipping_address.address2 && <p className="break-words">{order.shipping_address.address2}</p>}
<p className="break-words">
{order.shipping_address.city}, {order.shipping_address.province} {order.shipping_address.zip}
</p>
<p className="break-words">{order.shipping_address.country}</p>
{order.shipping_address.phone && (
<p className="mt-2 flex items-center gap-2 pt-2 border-t border-white/5">
<Phone className="w-3.5 h-3.5 text-slate-500" />
{order.shipping_address.phone}
{/* Status Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white/5 rounded-xl p-5 border border-white/10">
<span className="text-xs font-bold uppercase text-slate-400 tracking-wider block mb-3">Payment Status</span>
<div>
<span className={`px-3 py-1.5 rounded-full text-xs font-black uppercase tracking-wider inline-flex items-center gap-1.5 ${order.financial_status === "paid"
? "bg-emerald-500/20 text-emerald-400 border border-emerald-500/30"
: "bg-red-500/20 text-red-400 border border-red-500/30"
}`}>
{order.financial_status === "paid" && <CheckCircle2 className="w-3 h-3" />}
{order.financial_status}
</span>
</div>
</div>
<div className="bg-white/5 rounded-xl p-5 border border-white/10">
<span className="text-xs font-bold uppercase text-slate-400 tracking-wider block mb-3">Fulfillment Status</span>
<div>
<span className={`px-3 py-1.5 rounded-full text-xs font-black uppercase tracking-wider inline-flex items-center gap-1.5 ${order.fulfillment_status === "fulfilled"
? "bg-emerald-500/20 text-emerald-400 border border-emerald-500/30"
: "bg-blue-500/20 text-blue-400 border border-blue-500/30"
}`}>
{order.fulfillment_status === "fulfilled" ? <CheckCircle2 className="w-3 h-3" /> : <Clock className="w-3 h-3" />}
{order.fulfillment_status || "Unfulfilled"}
</span>
</div>
</div>
</div>
{/* Main Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 min-w-0 max-w-full">
{/* Left Column - Customer & Shipping */}
<div className="lg:col-span-1 space-y-6 min-w-0 max-w-full">
{/* Customer Info */}
{order.customer && (
<div className="bg-white/5 rounded-xl p-5 border border-white/10">
<div className="flex items-center gap-2 text-slate-400 mb-4">
<User className="w-4 h-4" />
<span className="text-xs font-bold uppercase tracking-wider">Customer</span>
</div>
<div className="space-y-3">
<p className="text-white font-bold text-base">
{order.customer.first_name} {order.customer.last_name}
</p>
)}
</div>
</div>
)}
</div>
{/* Right Column - Order Items */}
<div className="lg:col-span-2 min-w-0 max-w-full overflow-hidden">
<div className="bg-white/5 rounded-xl p-5 border border-white/10 min-w-0 max-w-full overflow-hidden">
<div className="flex items-center gap-2 text-slate-400 mb-5">
<Package className="w-4 h-4" />
<span className="text-xs font-bold uppercase tracking-wider">Order Items</span>
</div>
<div className="space-y-4 overflow-x-auto">
{order.line_items.map((item) => (
<div key={item.id} className="bg-white/5 rounded-lg p-4 border border-white/5 min-w-0">
<div className="flex items-start justify-between gap-4 min-w-0">
<div className="flex-1 min-w-0 overflow-hidden">
<p className="text-white font-bold text-base mb-1 break-words">{item.title}</p>
{item.variant_title && (
<p className="text-sm text-slate-400 mb-2 break-words">{item.variant_title}</p>
)}
<div className="flex items-center gap-4 flex-wrap">
<span className="text-sm text-slate-400">
<span className="font-medium text-slate-300">Qty:</span> {item.quantity}
</span>
{item.sku && (
<span className="text-xs text-slate-500 break-all">
<span className="font-medium">SKU:</span> {item.sku}
</span>
)}
</div>
</div>
<div className="text-right flex-shrink-0">
<p className="text-white font-bold text-lg whitespace-nowrap">
{(parseFloat(item.price) * item.quantity).toFixed(2)} {order.currency}
</p>
<p className="text-xs text-slate-400 mt-1 whitespace-nowrap">
{parseFloat(item.price).toFixed(2)} {order.currency} each
</p>
</div>
<div className="flex items-center gap-2 text-slate-300 text-sm min-w-0">
<Mail className="w-3.5 h-3.5 text-slate-500 flex-shrink-0" />
<span className="break-all min-w-0">{order.customer.email}</span>
</div>
{order.customer.phone && (
<div className="flex items-center gap-2 text-slate-300 text-sm">
<Phone className="w-3.5 h-3.5 text-slate-500" />
<span>{order.customer.phone}</span>
</div>
)}
</div>
))}
</div>
{/* Order Summary */}
{(order.subtotal_price || order.total_tax || (order.total_discounts && parseFloat(order.total_discounts) > 0)) && (
<div className="mt-6 pt-6 border-t border-white/10 space-y-3">
{order.subtotal_price && (
<div className="flex justify-between text-sm">
<span className="text-slate-400">Subtotal</span>
<span className="text-white font-medium">{order.subtotal_price} {order.currency}</span>
</div>
)}
{order.total_tax && (
<div className="flex justify-between text-sm">
<span className="text-slate-400">Tax</span>
<span className="text-white font-medium">{order.total_tax} {order.currency}</span>
</div>
)}
{order.total_discounts && parseFloat(order.total_discounts) > 0 && (
<div className="flex justify-between text-sm">
<span className="text-slate-400">Discount</span>
<span className="text-emerald-400 font-medium">-{order.total_discounts} {order.currency}</span>
</div>
)}
<div className="flex justify-between text-lg font-bold pt-3 border-t border-white/10">
<span className="text-white">Total</span>
<span className="text-white">{order.total_price} {order.currency}</span>
</div>
)}
{/* Shipping Address */}
{order.shipping_address && (
<div className="bg-white/5 rounded-xl p-5 border border-white/10">
<div className="flex items-center gap-2 text-slate-400 mb-4">
<MapPin className="w-4 h-4" />
<span className="text-xs font-bold uppercase tracking-wider">Shipping Address</span>
</div>
<div className="text-slate-300 text-sm space-y-1.5 break-words">
<p className="font-medium text-white break-words">
{order.shipping_address.first_name} {order.shipping_address.last_name}
</p>
<p className="break-words">{order.shipping_address.address1}</p>
{order.shipping_address.address2 && <p className="break-words">{order.shipping_address.address2}</p>}
<p className="break-words">
{order.shipping_address.city}, {order.shipping_address.province} {order.shipping_address.zip}
</p>
<p className="break-words">{order.shipping_address.country}</p>
{order.shipping_address.phone && (
<p className="mt-2 flex items-center gap-2 pt-2 border-t border-white/5">
<Phone className="w-3.5 h-3.5 text-slate-500" />
{order.shipping_address.phone}
</p>
)}
</div>
</div>
)}
</div>
{/* Order Notes */}
{order.note && (
<div className="bg-white/5 rounded-xl p-5 border border-white/10 mt-6">
<span className="text-xs font-bold uppercase text-slate-400 tracking-wider block mb-3">Order Notes</span>
<p className="text-slate-300 text-sm leading-relaxed">{order.note}</p>
{/* Right Column - Order Items */}
<div className="lg:col-span-2 min-w-0 max-w-full overflow-hidden">
<div className="bg-white/5 rounded-xl p-5 border border-white/10 min-w-0 max-w-full overflow-hidden">
<div className="flex items-center gap-2 text-slate-400 mb-5">
<Package className="w-4 h-4" />
<span className="text-xs font-bold uppercase tracking-wider">Order Items</span>
</div>
<div className="space-y-4 overflow-x-auto">
{order.line_items.map((item) => (
<div key={item.id} className="bg-white/5 rounded-lg p-4 border border-white/5 min-w-0">
<div className="flex items-start justify-between gap-4 min-w-0">
<div className="flex-1 min-w-0 overflow-hidden">
<p className="text-white font-bold text-base mb-1 break-words">{item.title}</p>
{item.variant_title && (
<p className="text-sm text-slate-400 mb-2 break-words">{item.variant_title}</p>
)}
<div className="flex items-center gap-4 flex-wrap">
<span className="text-sm text-slate-400">
<span className="font-medium text-slate-300">Qty:</span> {item.quantity}
</span>
{item.sku && (
<span className="text-xs text-slate-500 break-all">
<span className="font-medium">SKU:</span> {item.sku}
</span>
)}
</div>
</div>
<div className="text-right flex-shrink-0">
<p className="text-white font-bold text-lg whitespace-nowrap">
{(parseFloat(item.price) * item.quantity).toFixed(2)} {order.currency}
</p>
<p className="text-xs text-slate-400 mt-1 whitespace-nowrap">
{parseFloat(item.price).toFixed(2)} {order.currency} each
</p>
</div>
</div>
</div>
))}
</div>
{/* Order Summary */}
{(order.subtotal_price || order.total_tax || (order.total_discounts && parseFloat(order.total_discounts) > 0)) && (
<div className="mt-6 pt-6 border-t border-white/10 space-y-3">
{order.subtotal_price && (
<div className="flex justify-between text-sm">
<span className="text-slate-400">Subtotal</span>
<span className="text-white font-medium">{order.subtotal_price} {order.currency}</span>
</div>
)}
{order.total_tax && (
<div className="flex justify-between text-sm">
<span className="text-slate-400">Tax</span>
<span className="text-white font-medium">{order.total_tax} {order.currency}</span>
</div>
)}
{order.total_discounts && parseFloat(order.total_discounts) > 0 && (
<div className="flex justify-between text-sm">
<span className="text-slate-400">Discount</span>
<span className="text-emerald-400 font-medium">-{order.total_discounts} {order.currency}</span>
</div>
)}
<div className="flex justify-between text-lg font-bold pt-3 border-t border-white/10">
<span className="text-white">Total</span>
<span className="text-white">{order.total_price} {order.currency}</span>
</div>
</div>
)}
</div>
)}
{/* Order Notes */}
{order.note && (
<div className="bg-white/5 rounded-xl p-5 border border-white/10 mt-6">
<span className="text-xs font-bold uppercase text-slate-400 tracking-wider block mb-3">Order Notes</span>
<p className="text-slate-300 text-sm leading-relaxed">{order.note}</p>
</div>
)}
</div>
</div>
</div>
</div>
) : null}
) : null}
</div>
{/* Footer */}

View File

@ -24,14 +24,14 @@ export default function StoreSwitcher() {
useEffect(() => {
fetchStores()
// Listen for store updates (when stores are added/deleted)
const handleStoreUpdate = () => {
fetchStores()
}
window.addEventListener("storeUpdated", handleStoreUpdate)
return () => {
window.removeEventListener("storeUpdated", handleStoreUpdate)
}
@ -95,37 +95,32 @@ export default function StoreSwitcher() {
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className={`flex max-w-full items-center gap-1.5 rounded-xl border px-2 py-2 text-left backdrop-blur-sm transition-all duration-200 ease-out md:gap-2 md:px-4 ${
isLight
className={`flex max-w-full items-center gap-1.5 rounded-xl border px-2 py-2 text-left backdrop-blur-sm transition-all duration-200 ease-out md:gap-2 md:px-4 ${isLight
? "bg-white border-slate-200 hover:bg-slate-50"
: "bg-white/10 border-white/10 hover:bg-white/15"
}`}
}`}
>
<Store
className={`h-3.5 w-3.5 shrink-0 md:h-4 md:w-4 ${
isLight ? "text-slate-600" : "text-slate-300"
}`}
className={`h-3.5 w-3.5 shrink-0 md:h-4 md:w-4 ${isLight ? "text-slate-600" : "text-slate-300"
}`}
/>
<div className="flex min-w-0 flex-1 flex-col items-start">
<span
className={`hidden text-xs font-medium md:block ${
isLight ? "text-slate-500" : "text-slate-400"
}`}
className={`hidden text-xs font-medium md:block ${isLight ? "text-slate-500" : "text-slate-400"
}`}
>
Store
</span>
<span
className={`truncate text-xs font-bold md:text-sm md:max-w-none max-w-[100px] ${
isLight ? "text-slate-900" : "text-white"
}`}
className={`truncate text-xs font-bold md:text-sm md:max-w-none max-w-[80px] xs:max-w-[120px] ${isLight ? "text-slate-900" : "text-white"
}`}
>
{activeStore?.businessName || "Select Store"}
</span>
</div>
<ChevronDown
className={`h-3.5 w-3.5 shrink-0 transition-transform duration-200 ease-out md:h-4 md:w-4 ${
isLight ? "text-slate-500" : "text-slate-400"
} ${isOpen ? "rotate-180" : ""}`}
className={`h-3.5 w-3.5 shrink-0 transition-transform duration-200 ease-out md:h-4 md:w-4 ${isLight ? "text-slate-500" : "text-slate-400"
} ${isOpen ? "rotate-180" : ""}`}
/>
</button>
@ -145,21 +140,18 @@ export default function StoreSwitcher() {
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -8, scale: 0.95 }}
transition={{ duration: 0.2, ease: [0.4, 0, 0.2, 1] }}
className={`absolute left-0 z-[110] mt-2 max-w-[320px] overflow-hidden rounded-2xl border shadow-2xl backdrop-blur-xl md:left-auto md:right-0 w-[calc(100vw-2rem)] md:w-72 ${
isLight
className={`absolute left-0 z-[110] mt-2 max-w-[320px] overflow-hidden rounded-2xl border shadow-2xl backdrop-blur-xl md:left-auto md:right-0 w-[calc(100vw-2rem)] md:w-72 ${isLight
? "bg-white border-slate-200"
: "bg-slate-900/90 border-white/10"
}`}
}`}
>
<div
className={`p-3 border-b ${
isLight ? "bg-slate-50 border-slate-200" : "bg-white/5 border-white/10"
}`}
className={`p-3 border-b ${isLight ? "bg-slate-50 border-slate-200" : "bg-white/5 border-white/10"
}`}
>
<p
className={`text-xs font-bold uppercase tracking-wider ${
isLight ? "text-slate-500" : "text-slate-400"
}`}
className={`text-xs font-bold uppercase tracking-wider ${isLight ? "text-slate-500" : "text-slate-400"
}`}
>
Your Stores
</p>
@ -175,58 +167,52 @@ export default function StoreSwitcher() {
setIsOpen(false)
}}
disabled={loading}
className={`flex w-full items-center justify-between px-4 py-3 transition-all ${
isLight
className={`flex w-full items-center justify-between px-4 py-3 transition-all ${isLight
? "hover:bg-slate-50"
: "hover:bg-white/10"
} ${
store.isActive
} ${store.isActive
? isLight
? "bg-indigo-50"
: "bg-indigo-500/20"
: ""
}`}
}`}
>
<div className="flex items-center gap-3">
<div
className={`flex h-8 w-8 items-center justify-center rounded-lg ${
store.isActive
className={`flex h-8 w-8 items-center justify-center rounded-lg ${store.isActive
? isLight
? "bg-indigo-100 border border-indigo-300"
: "bg-indigo-500/50 border border-indigo-400/30"
: isLight
? "bg-slate-100"
: "bg-white/10"
}`}
? "bg-slate-100"
: "bg-white/10"
}`}
>
<Store
className={`h-4 w-4 ${
store.isActive
className={`h-4 w-4 ${store.isActive
? isLight
? "text-indigo-600"
: "text-indigo-300"
: isLight
? "text-slate-500"
: "text-slate-400"
}`}
? "text-slate-500"
: "text-slate-400"
}`}
/>
</div>
<div className="flex flex-col items-start">
<span
className={`text-sm font-bold ${
isLight
className={`text-sm font-bold ${isLight
? "text-slate-900"
: "text-white"
}`}
}`}
>
{store.businessName}
</span>
<span
className={`text-xs ${
isLight
className={`text-xs ${isLight
? "text-slate-500"
: "text-slate-400"
}`}
}`}
>
{store.shopifyDomain}
</span>
@ -234,31 +220,28 @@ export default function StoreSwitcher() {
</div>
{store.isActive && (
<Check
className={`h-4 w-4 ${
isLight
className={`h-4 w-4 ${isLight
? "text-indigo-600"
: "text-indigo-400"
}`}
}`}
/>
)}
</button>
))}
</div>
<div
className={`p-3 border-t ${
isLight ? "bg-slate-50 border-slate-200" : "bg-white/5 border-white/10"
}`}
className={`p-3 border-t ${isLight ? "bg-slate-50 border-slate-200" : "bg-white/5 border-white/10"
}`}
>
<button
onClick={() => {
setIsOpen(false)
router.push("/dashboard/stores")
}}
className={`flex h-9 w-full items-center justify-center gap-2 rounded-lg text-sm font-bold transition-all border ${
isLight
className={`flex h-9 w-full items-center justify-center gap-2 rounded-lg text-sm font-bold transition-all border ${isLight
? "bg-indigo-600 text-white border-indigo-500 hover:bg-indigo-500"
: "bg-white/10 text-white border-white/10 hover:bg-white/15"
}`}
}`}
>
<Settings className="h-4 w-4" />
Manage Stores

View File

@ -58,7 +58,7 @@ export function SupplierSidebar() {
</div>
<h1 className="text-xl font-bold tracking-tight">JUNOHUB</h1>
</div>
<div className="flex-1 px-3 space-y-1">
<div className="flex-1 px-3 space-y-1 overflow-y-auto">
{supplierRoutes.map((route) => (
<Link
key={route.href}

View File

@ -13,37 +13,49 @@ export default function ThemeToggle() {
<button
type="button"
onClick={toggleTheme}
className="inline-flex items-center gap-1.5 rounded-full border border-white/15 bg-slate-900/40 px-2.5 py-1 text-[11px] font-medium text-slate-200 hover:bg-white/10"
className={`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-[11px] font-medium ${isLight
? "border-slate-300 bg-white/80 text-slate-700 hover:bg-slate-100"
: "border-white/15 bg-slate-900/40 text-slate-200 hover:bg-white/10"
}`}
aria-label="Toggle theme"
>
{isLight ? (
<Sun className="h-3.5 w-3.5 text-amber-300" />
<Sun className="h-3.5 w-3.5 text-amber-500" />
) : (
<Moon className="h-3.5 w-3.5 text-sky-300" />
)}
<span>{isLight ? "Light mode" : "Default mode"}</span>
<span className="hidden xs:inline">{isLight ? "Light" : "Default"}</span>
</button>
<div className="hidden md:flex rounded-full border border-white/10 bg-slate-900/50 text-[11px] font-semibold text-slate-300 overflow-hidden">
<div className={`hidden md:flex rounded-full border text-[11px] font-semibold overflow-hidden ${isLight
? "border-slate-300 bg-slate-100 text-slate-600"
: "border-white/10 bg-slate-900/50 text-slate-300"
}`}>
<button
type="button"
onClick={() => setTheme("default")}
className={`px-2.5 py-1 ${
theme === "default"
? "bg-white text-slate-900"
className={`px-2.5 py-1 ${theme === "default"
? isLight
? "bg-slate-700 text-white"
: "bg-white text-slate-900"
: isLight
? "bg-transparent hover:bg-slate-200"
: "bg-transparent hover:bg-white/5"
}`}
}`}
>
Default
</button>
<button
type="button"
onClick={() => setTheme("light")}
className={`px-2.5 py-1 ${
theme === "light"
? "bg-white text-slate-900"
className={`px-2.5 py-1 ${theme === "light"
? isLight
? "bg-slate-700 text-white"
: "bg-white text-slate-900"
: isLight
? "bg-transparent hover:bg-slate-200"
: "bg-transparent hover:bg-white/5"
}`}
}`}
>
Light
</button>

View File

@ -4,12 +4,16 @@ import { useState, useEffect, useRef } from "react"
import { useRouter } from "next/navigation"
import { signOut, useSession } from "next-auth/react"
import { Settings, LogOut, ChevronDown } from "lucide-react"
import { motion, AnimatePresence } from "framer-motion"
import { useTheme } from "@/components/ThemeProvider"
export default function UserAccountMenu() {
const { data: session } = useSession()
const router = useRouter()
const [isOpen, setIsOpen] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)
const { theme } = useTheme()
const isLight = theme === "light"
// Close menu when clicking outside
useEffect(() => {
@ -49,25 +53,42 @@ export default function UserAccountMenu() {
<ChevronDown className={`w-4 h-4 text-slate-400 transition-transform ${isOpen ? "rotate-180" : ""}`} />
</button>
{isOpen && (
<div className="absolute right-0 top-full mt-2 bg-white/95 backdrop-blur-xl border border-white/20 rounded-xl shadow-xl overflow-hidden z-50 min-w-[180px]">
<button
onClick={handleSettings}
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"
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, scale: 0.95, y: -4 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: -4 }}
transition={{ duration: 0.15, ease: "easeOut" }}
className={`absolute right-0 top-full mt-2 backdrop-blur-xl border rounded-xl shadow-xl overflow-hidden z-50 min-w-[180px] ${isLight
? "bg-white/95 border-slate-200"
: "bg-slate-800/95 border-white/10"
}`}
>
<Settings className="w-4 h-4" />
Settings
</button>
<div className="border-t border-white/10"></div>
<button
onClick={handleLogout}
className="w-full px-4 py-2.5 text-left text-sm font-medium text-red-600 hover:bg-red-500 hover:text-white transition-colors flex items-center gap-2"
>
<LogOut className="w-4 h-4" />
Logout
</button>
</div>
)}
<button
onClick={handleSettings}
className={`w-full px-4 py-2.5 text-left text-sm font-medium transition-colors flex items-center gap-2 ${isLight
? "text-slate-700 hover:bg-indigo-500 hover:text-white"
: "text-slate-200 hover:bg-indigo-500 hover:text-white"
}`}
>
<Settings className="w-4 h-4" />
Settings
</button>
<div className={`border-t ${isLight ? "border-slate-200" : "border-white/10"}`}></div>
<button
onClick={handleLogout}
className={`w-full px-4 py-2.5 text-left text-sm font-medium transition-colors flex items-center gap-2 ${isLight
? "text-red-600 hover:bg-red-500 hover:text-white"
: "text-red-400 hover:bg-red-500 hover:text-white"
}`}
>
<LogOut className="w-4 h-4" />
Logout
</button>
</motion.div>
)}
</AnimatePresence>
</div>
)
}