fixed UI and MFA
This commit is contained in:
parent
9d2ca7f592
commit
59d90ed807
@ -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
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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"}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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 ? (
|
||||
<>
|
||||
|
||||
@ -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 |
@ -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
4
src/app/icon.svg
Normal 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 |
@ -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 = {
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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'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'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
|
||||
)
|
||||
}
|
||||
|
||||
@ -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'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>
|
||||
|
||||
72
src/components/MobileAdminSidebar.tsx
Normal file
72
src/components/MobileAdminSidebar.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
</>
|
||||
|
||||
@ -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"
|
||||
>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 */}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user