Autosave: 20260328-071848
This commit is contained in:
parent
de5aa451c1
commit
86813df53b
9
.bolt/extra-store.log
Normal file
9
.bolt/extra-store.log
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
> @workspace/extra-store@0.0.0 dev /home/ubuntu/executor/workspace/artifacts/extra-store
|
||||
> vite --config vite.config.ts --host 0.0.0.0
|
||||
|
||||
|
||||
VITE v7.3.1 ready in 737 ms
|
||||
|
||||
➜ Local: http://localhost:3001/
|
||||
➜ Network: http://10.128.0.201:3001/
|
||||
1
.bolt/extra-store.pid
Normal file
1
.bolt/extra-store.pid
Normal file
@ -0,0 +1 @@
|
||||
47713
|
||||
@ -1357,9 +1357,12 @@ function Header() {
|
||||
});
|
||||
};
|
||||
|
||||
const extraCats =
|
||||
allCats?.filter((c) => !c.source || c.source === "extra") ?? [];
|
||||
const sheinTree = tree?.filter((n) => n.source === "shein") ?? [];
|
||||
const extraCats = Array.isArray(allCats)
|
||||
? allCats.filter((c) => !c.source || c.source === "extra")
|
||||
: [];
|
||||
const sheinTree = Array.isArray(tree)
|
||||
? tree.filter((n) => n.source === "shein")
|
||||
: [];
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 bg-[#0a0a0a] border-b border-white/10">
|
||||
@ -1762,6 +1765,14 @@ function Footer() {
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
<li>
|
||||
<Link
|
||||
href="/admin"
|
||||
className="hover:text-orange-400 transition-colors"
|
||||
>
|
||||
{lang === "en" ? "Admin Panel" : "لوحة المسؤول"}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
@ -5088,6 +5099,7 @@ function Router() {
|
||||
if (isAdmin)
|
||||
return (
|
||||
<Switch>
|
||||
<Route path="/admin/:tab" component={AdminPage} />
|
||||
<Route path="/admin" component={AdminPage} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
@ -80,6 +80,23 @@ function getBaseApiPath() {
|
||||
}
|
||||
}
|
||||
|
||||
function buildCategoryTree(categories: any[]) {
|
||||
const sorted = [...categories].sort(
|
||||
(a, b) =>
|
||||
Number(a?.sort_order || 0) - Number(b?.sort_order || 0) ||
|
||||
Number(a?.id || 0) - Number(b?.id || 0),
|
||||
);
|
||||
|
||||
return sorted
|
||||
.filter((category) => !category?.parent_id)
|
||||
.map((category) => ({
|
||||
...category,
|
||||
children: sorted.filter(
|
||||
(child) => Number(child?.parent_id || 0) === Number(category.id),
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
function seedStoreSettings() {
|
||||
return {
|
||||
store_name_ar: "اكسترا السعودية",
|
||||
@ -1207,6 +1224,8 @@ function handlePreviewApi(url: URL, init?: RequestInit) {
|
||||
return json({ ok: true });
|
||||
}
|
||||
|
||||
if (path === "/categories/tree" && method === "GET")
|
||||
return json(buildCategoryTree(db.categories));
|
||||
if (path === "/categories" && method === "GET") return json(db.categories);
|
||||
if (path === "/categories" && method === "POST") {
|
||||
const id = db.nextIds.categories++;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { useLocation } from "wouter";
|
||||
import { API } from "../lib/api";
|
||||
import { isJsonResponse, loginPreviewAdmin } from "../lib/mock-auth";
|
||||
import { installPreviewAdminApi } from "../lib/admin-preview-api";
|
||||
@ -337,19 +338,50 @@ const TABS = [
|
||||
{ id: "settings", name: "الإعدادات", icon: Settings },
|
||||
];
|
||||
|
||||
type AdminTabId = (typeof TABS)[number]["id"];
|
||||
const ADMIN_TABS = new Set<AdminTabId>(TABS.map((tab) => tab.id));
|
||||
|
||||
function getAdminTabFromPath(pathname: string): AdminTabId {
|
||||
const parts = pathname.split("/").filter(Boolean);
|
||||
const candidate = (parts[1] || "dashboard") as AdminTabId;
|
||||
return ADMIN_TABS.has(candidate) ? candidate : "dashboard";
|
||||
}
|
||||
|
||||
// ─── Login ──────────────────────────────────────────────
|
||||
export default function AdminPage() {
|
||||
const [token, setToken] = useState(localStorage.getItem("admin_token"));
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [token, setToken] = useState(() =>
|
||||
typeof window === "undefined" ? null : localStorage.getItem("admin_token"),
|
||||
);
|
||||
const [username, setUsername] = useState("admin");
|
||||
const [password, setPassword] = useState("admin123");
|
||||
const [showPass, setShowPass] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [remember, setRemember] = useState(false);
|
||||
const [remember, setRemember] = useState(() =>
|
||||
typeof window !== "undefined" && localStorage.getItem("admin_remember") === "1",
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
installPreviewAdminApi();
|
||||
if (typeof window === "undefined") return;
|
||||
if (!localStorage.getItem("admin_token") && localStorage.getItem("admin_remember") === "1") {
|
||||
try {
|
||||
const session = loginPreviewAdmin({ username: "admin", password: "admin123" });
|
||||
localStorage.setItem("admin_token", session.token);
|
||||
setToken(session.token);
|
||||
} catch (_) {}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleQuickPreviewLogin = () => {
|
||||
unlockAudio();
|
||||
const session = loginPreviewAdmin({ username: "admin", password: "admin123" });
|
||||
localStorage.setItem("admin_token", session.token);
|
||||
if (remember) localStorage.setItem("admin_remember", "1");
|
||||
else localStorage.removeItem("admin_remember");
|
||||
setToken(session.token);
|
||||
adminToast("تم فتح لوحة التحكم الكاملة بوضع المعاينة", "ok");
|
||||
};
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
unlockAudio(); // unlock browser audio on this user gesture
|
||||
@ -379,6 +411,7 @@ export default function AdminPage() {
|
||||
|
||||
localStorage.setItem("admin_token", data.token);
|
||||
if (remember) localStorage.setItem("admin_remember", "1");
|
||||
else localStorage.removeItem("admin_remember");
|
||||
setToken(data.token);
|
||||
} catch (error) {
|
||||
alert(error instanceof Error ? error.message : "بيانات الدخول غير صحيحة");
|
||||
@ -466,10 +499,20 @@ export default function AdminPage() {
|
||||
{loading && <Loader2 className="w-4 h-4 animate-spin" />}
|
||||
دخول
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleQuickPreviewLogin}
|
||||
className="w-full border border-[#D4AF37]/30 text-[#D4AF37] font-bold py-3 rounded-xl hover:bg-[#D4AF37]/10 transition-colors"
|
||||
>
|
||||
فتح لوحة التحكم الكاملة مباشرة
|
||||
</button>
|
||||
</form>
|
||||
<p className="text-center text-xs text-gray-700 mt-6">
|
||||
admin / admin123
|
||||
</p>
|
||||
<div className="mt-6 space-y-2 text-center">
|
||||
<p className="text-xs text-gray-700">admin / admin123</p>
|
||||
<a href="/" className="text-xs text-gray-500 hover:text-white transition-colors">
|
||||
العودة إلى المتجر
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -561,7 +604,10 @@ function notifToneClasses(tone: string) {
|
||||
}
|
||||
|
||||
function AdminDashboard({ onLogout }: { onLogout: () => void }) {
|
||||
const [activeTab, setActiveTab] = useState("dashboard");
|
||||
const [location, setLocation] = useLocation();
|
||||
const [activeTab, setActiveTab] = useState<AdminTabId>(() =>
|
||||
typeof window === "undefined" ? "dashboard" : getAdminTabFromPath(window.location.pathname),
|
||||
);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [checkoutNotifs, setCheckoutNotifs] = useState<CheckoutNotif[]>([]);
|
||||
const [showNotifPanel, setShowNotifPanel] = useState(false);
|
||||
@ -571,6 +617,19 @@ function AdminDashboard({ onLogout }: { onLogout: () => void }) {
|
||||
const lastEventId = useRef<number>(0);
|
||||
const eventsInitialized = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const nextTab = getAdminTabFromPath(location);
|
||||
setActiveTab((current) => (current === nextTab ? current : nextTab));
|
||||
}, [location]);
|
||||
|
||||
const goToTab = useCallback(
|
||||
(tabId: AdminTabId) => {
|
||||
setActiveTab(tabId);
|
||||
setLocation(tabId === "dashboard" ? "/admin" : `/admin/${tabId}`);
|
||||
},
|
||||
[setLocation],
|
||||
);
|
||||
|
||||
const pollOrders = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch(`${API}/orders?limit=1`);
|
||||
@ -790,7 +849,7 @@ function AdminDashboard({ onLogout }: { onLogout: () => void }) {
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => {
|
||||
setActiveTab(tab.id);
|
||||
goToTab(tab.id);
|
||||
setSidebarOpen(false);
|
||||
}}
|
||||
className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium transition-all mb-1
|
||||
|
||||
@ -13,6 +13,7 @@ if (Number.isNaN(port) || port <= 0) {
|
||||
}
|
||||
|
||||
const basePath = process.env.BASE_PATH ?? "/";
|
||||
const apiProxyTarget = process.env.API_PROXY_TARGET ?? "http://127.0.0.1:3002";
|
||||
|
||||
export default defineConfig({
|
||||
base: basePath,
|
||||
@ -50,6 +51,12 @@ export default defineConfig({
|
||||
port,
|
||||
host: "0.0.0.0",
|
||||
allowedHosts: true,
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: apiProxyTarget,
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
fs: {
|
||||
strict: true,
|
||||
deny: ["**/.*"],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user