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 =
|
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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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++;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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: ["**/.*"],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user