Autosave: 20260328-071848

This commit is contained in:
Flatlogic Bot 2026-03-28 07:18:48 +00:00
parent de5aa451c1
commit 86813df53b
6 changed files with 119 additions and 12 deletions

9
.bolt/extra-store.log Normal file
View 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
View File

@ -0,0 +1 @@
47713

View File

@ -1357,9 +1357,12 @@ function Header() {
}); });
}; };
const extraCats = const extraCats = Array.isArray(allCats)
allCats?.filter((c) => !c.source || c.source === "extra") ?? []; ? allCats.filter((c) => !c.source || c.source === "extra")
const sheinTree = tree?.filter((n) => n.source === "shein") ?? []; : [];
const sheinTree = Array.isArray(tree)
? tree.filter((n) => n.source === "shein")
: [];
return ( return (
<header className="sticky top-0 z-50 bg-[#0a0a0a] border-b border-white/10"> <header className="sticky top-0 z-50 bg-[#0a0a0a] border-b border-white/10">
@ -1762,6 +1765,14 @@ function Footer() {
</a> </a>
</li> </li>
))} ))}
<li>
<Link
href="/admin"
className="hover:text-orange-400 transition-colors"
>
{lang === "en" ? "Admin Panel" : "لوحة المسؤول"}
</Link>
</li>
</ul> </ul>
</div> </div>
<div> <div>
@ -5088,6 +5099,7 @@ function Router() {
if (isAdmin) if (isAdmin)
return ( return (
<Switch> <Switch>
<Route path="/admin/:tab" component={AdminPage} />
<Route path="/admin" component={AdminPage} /> <Route path="/admin" component={AdminPage} />
</Switch> </Switch>
); );

View File

@ -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() { function seedStoreSettings() {
return { return {
store_name_ar: "اكسترا السعودية", store_name_ar: "اكسترا السعودية",
@ -1207,6 +1224,8 @@ function handlePreviewApi(url: URL, init?: RequestInit) {
return json({ ok: true }); 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 === "GET") return json(db.categories);
if (path === "/categories" && method === "POST") { if (path === "/categories" && method === "POST") {
const id = db.nextIds.categories++; const id = db.nextIds.categories++;

View File

@ -1,4 +1,5 @@
import { useState, useEffect, useRef, useCallback } from "react"; import { useState, useEffect, useRef, useCallback } from "react";
import { useLocation } from "wouter";
import { API } from "../lib/api"; import { API } from "../lib/api";
import { isJsonResponse, loginPreviewAdmin } from "../lib/mock-auth"; import { isJsonResponse, loginPreviewAdmin } from "../lib/mock-auth";
import { installPreviewAdminApi } from "../lib/admin-preview-api"; import { installPreviewAdminApi } from "../lib/admin-preview-api";
@ -337,19 +338,50 @@ const TABS = [
{ id: "settings", name: "الإعدادات", icon: Settings }, { 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 ────────────────────────────────────────────── // ─── Login ──────────────────────────────────────────────
export default function AdminPage() { export default function AdminPage() {
const [token, setToken] = useState(localStorage.getItem("admin_token")); const [token, setToken] = useState(() =>
const [username, setUsername] = useState(""); typeof window === "undefined" ? null : localStorage.getItem("admin_token"),
const [password, setPassword] = useState(""); );
const [username, setUsername] = useState("admin");
const [password, setPassword] = useState("admin123");
const [showPass, setShowPass] = useState(false); const [showPass, setShowPass] = useState(false);
const [loading, setLoading] = 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(() => { useEffect(() => {
installPreviewAdminApi(); 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) => { const handleLogin = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
unlockAudio(); // unlock browser audio on this user gesture unlockAudio(); // unlock browser audio on this user gesture
@ -379,6 +411,7 @@ export default function AdminPage() {
localStorage.setItem("admin_token", data.token); localStorage.setItem("admin_token", data.token);
if (remember) localStorage.setItem("admin_remember", "1"); if (remember) localStorage.setItem("admin_remember", "1");
else localStorage.removeItem("admin_remember");
setToken(data.token); setToken(data.token);
} catch (error) { } catch (error) {
alert(error instanceof Error ? error.message : "بيانات الدخول غير صحيحة"); alert(error instanceof Error ? error.message : "بيانات الدخول غير صحيحة");
@ -466,10 +499,20 @@ export default function AdminPage() {
{loading && <Loader2 className="w-4 h-4 animate-spin" />} {loading && <Loader2 className="w-4 h-4 animate-spin" />}
دخول دخول
</button> </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> </form>
<p className="text-center text-xs text-gray-700 mt-6"> <div className="mt-6 space-y-2 text-center">
admin / admin123 <p className="text-xs text-gray-700">admin / admin123</p>
</p> <a href="/" className="text-xs text-gray-500 hover:text-white transition-colors">
العودة إلى المتجر
</a>
</div>
</div> </div>
</div> </div>
); );
@ -561,7 +604,10 @@ function notifToneClasses(tone: string) {
} }
function AdminDashboard({ onLogout }: { onLogout: () => void }) { 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 [sidebarOpen, setSidebarOpen] = useState(false);
const [checkoutNotifs, setCheckoutNotifs] = useState<CheckoutNotif[]>([]); const [checkoutNotifs, setCheckoutNotifs] = useState<CheckoutNotif[]>([]);
const [showNotifPanel, setShowNotifPanel] = useState(false); const [showNotifPanel, setShowNotifPanel] = useState(false);
@ -571,6 +617,19 @@ function AdminDashboard({ onLogout }: { onLogout: () => void }) {
const lastEventId = useRef<number>(0); const lastEventId = useRef<number>(0);
const eventsInitialized = useRef(false); 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 () => { const pollOrders = useCallback(async () => {
try { try {
const res = await fetch(`${API}/orders?limit=1`); const res = await fetch(`${API}/orders?limit=1`);
@ -790,7 +849,7 @@ function AdminDashboard({ onLogout }: { onLogout: () => void }) {
<button <button
key={tab.id} key={tab.id}
onClick={() => { onClick={() => {
setActiveTab(tab.id); goToTab(tab.id);
setSidebarOpen(false); 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 className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium transition-all mb-1

View File

@ -13,6 +13,7 @@ if (Number.isNaN(port) || port <= 0) {
} }
const basePath = process.env.BASE_PATH ?? "/"; const basePath = process.env.BASE_PATH ?? "/";
const apiProxyTarget = process.env.API_PROXY_TARGET ?? "http://127.0.0.1:3002";
export default defineConfig({ export default defineConfig({
base: basePath, base: basePath,
@ -50,6 +51,12 @@ export default defineConfig({
port, port,
host: "0.0.0.0", host: "0.0.0.0",
allowedHosts: true, allowedHosts: true,
proxy: {
"/api": {
target: apiProxyTarget,
changeOrigin: true,
},
},
fs: { fs: {
strict: true, strict: true,
deny: ["**/.*"], deny: ["**/.*"],