Admin: replace top tabs with sidebar menu (RTL on right)

Task #9 — the admin page used a horizontal Tabs bar across the top.
User wanted an OS-style admin shell with a vertical side menu that
sits on the right in Arabic and on the left in English.

Changes (artifacts/teaboy-os/src/pages/admin.tsx):
- Removed the Tabs/TabsList/TabsTrigger/TabsContent layout
- Added local section state ("dashboard" | "apps" | "services" |
  "users" | "settings"), defaulting to dashboard
- Desktop (>=md): vertical sidebar (240px wide) with icon + label
  for each section, glass-style highlighted active item. Sidebar is
  the first DOM child of the flex row, so under dir="rtl" it
  naturally appears on the right; under dir="ltr" on the left.
- Mobile (<md): sidebar hidden; hamburger button added in the header
  opens a Sheet drawer (right side in RTL, left in LTR) with the
  same nav items. Selecting an item closes the drawer.
- Added a placeholder Dashboard panel ("Dashboard widgets coming
  soon.") so the new default section has content.
- Added bilingual locale keys: admin.nav.{dashboard,apps,services,
  users,settings,menu} and admin.dashboardSoon in en.json/ar.json.
- All existing CRUD (apps/services/users) and Site Settings panel
  preserved untouched inside their new section blocks.

Verified: pnpm --filter @workspace/teaboy-os run typecheck passes.
This commit is contained in:
Riyadh
2026-04-20 11:41:01 +00:00
parent 174fa10a36
commit ac96f86e29
3 changed files with 114 additions and 28 deletions
+10 -1
View File
@@ -96,7 +96,16 @@
"siteNameAr": "اسم الموقع (عربي)",
"siteNameEn": "اسم الموقع (إنجليزي)",
"registrationOpen": "السماح بإنشاء حسابات",
"registrationOpenHint": "عند الإيقاف، الأدمن فقط يقدر ينشئ مستخدمين جدد."
"registrationOpenHint": "عند الإيقاف، الأدمن فقط يقدر ينشئ مستخدمين جدد.",
"nav": {
"dashboard": "الرئيسية",
"apps": "التطبيقات",
"services": "الخدمات",
"users": "المستخدمين",
"settings": "الإعدادات",
"menu": "القائمة"
},
"dashboardSoon": "إحصائيات اللوحة قريباً."
},
"notFound": {
"title": "404 — الصفحة غير موجودة",
+10 -1
View File
@@ -96,7 +96,16 @@
"siteNameAr": "Site Name (Arabic)",
"siteNameEn": "Site Name (English)",
"registrationOpen": "Allow public registration",
"registrationOpenHint": "When off, only admins can create new users."
"registrationOpenHint": "When off, only admins can create new users.",
"nav": {
"dashboard": "Dashboard",
"apps": "Apps",
"services": "Services",
"users": "Users",
"settings": "Settings",
"menu": "Menu"
},
"dashboardSoon": "Dashboard widgets coming soon."
},
"notFound": {
"title": "404 — Page Not Found",
+94 -26
View File
@@ -24,13 +24,16 @@ import {
import { useQueryClient } from "@tanstack/react-query";
import { useAuth } from "@/contexts/AuthContext";
import { useUpload, type UploadResponse } from "@workspace/object-storage-web";
import { ArrowRight, ArrowLeft, Settings, Plus, Pencil, Trash2, Shield, Upload, Loader2 } from "lucide-react";
import { ArrowRight, ArrowLeft, Settings, Plus, Pencil, Trash2, Shield, Upload, Loader2, LayoutDashboard, Grid2X2, Coffee, Users as UsersIcon, Menu as MenuIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import { useToast } from "@/hooks/use-toast";
import { cn } from "@/lib/utils";
type AdminSection = "dashboard" | "apps" | "services" | "users" | "settings";
type AppForm = {
nameAr: string;
@@ -87,6 +90,42 @@ export default function AdminPage() {
const [editingApp, setEditingApp] = useState<{ id?: number; form: AppForm } | null>(null);
const [editingService, setEditingService] = useState<{ id?: number; form: ServiceForm } | null>(null);
const [newUserForm, setNewUserForm] = useState<{ username: string; email: string; password: string } | null>(null);
const [section, setSection] = useState<AdminSection>("dashboard");
const [mobileNavOpen, setMobileNavOpen] = useState(false);
const navItems: { key: AdminSection; label: string; Icon: typeof LayoutDashboard }[] = [
{ key: "dashboard", label: t("admin.nav.dashboard"), Icon: LayoutDashboard },
{ key: "apps", label: t("admin.nav.apps"), Icon: Grid2X2 },
{ key: "services", label: t("admin.nav.services"), Icon: Coffee },
{ key: "users", label: t("admin.nav.users"), Icon: UsersIcon },
{ key: "settings", label: t("admin.nav.settings"), Icon: Settings },
];
const renderNavList = (onSelect?: () => void) => (
<nav className="flex flex-col gap-1">
{navItems.map(({ key, label, Icon }) => {
const active = section === key;
return (
<button
key={key}
onClick={() => {
setSection(key);
onSelect?.();
}}
className={cn(
"flex items-center gap-3 w-full px-3 py-2.5 rounded-xl text-sm transition-colors text-start",
active
? "bg-primary/15 text-primary font-medium shadow-sm"
: "text-muted-foreground hover:bg-slate-100 hover:text-foreground",
)}
>
<Icon size={18} className="shrink-0" />
<span className="truncate">{label}</span>
</button>
);
})}
</nav>
);
const emptyAppForm: AppForm = { nameAr: "", nameEn: "", slug: "", iconName: "Grid2X2", route: "/", color: "#6366f1", sortOrder: 0 };
const emptyServiceForm: ServiceForm = { nameAr: "", nameEn: "", descriptionAr: "", descriptionEn: "", price: "0.00", imageUrl: "", isAvailable: true };
@@ -171,10 +210,27 @@ export default function AdminPage() {
>
<BackIcon size={20} />
</button>
<div className="flex items-center gap-2">
<Settings size={20} className="text-red-400" />
<h1 className="text-lg font-semibold text-foreground">{t("admin.title")}</h1>
<div className="flex items-center gap-2 flex-1 min-w-0">
<Settings size={20} className="text-red-400 shrink-0" />
<h1 className="text-lg font-semibold text-foreground truncate">{t("admin.title")}</h1>
</div>
<Sheet open={mobileNavOpen} onOpenChange={setMobileNavOpen}>
<SheetTrigger asChild>
<button
className="md:hidden p-2 rounded-xl hover:bg-slate-100 text-muted-foreground hover:text-foreground transition-colors"
aria-label={t("admin.nav.menu")}
>
<MenuIcon size={20} />
</button>
</SheetTrigger>
<SheetContent side={isRtl ? "right" : "left"} className="w-72 p-4">
<div className="flex items-center gap-2 mb-4 mt-2">
<Settings size={18} className="text-red-400" />
<h2 className="font-semibold text-foreground">{t("admin.title")}</h2>
</div>
{renderNavList(() => setMobileNavOpen(false))}
</SheetContent>
</Sheet>
</div>
{/* Edit App Modal */}
@@ -303,17 +359,24 @@ export default function AdminPage() {
</div>
)}
<div className="flex-1 p-4 pb-8">
<Tabs defaultValue="apps">
<TabsList className="glass-panel w-full mb-4">
<TabsTrigger value="apps" className="flex-1">{t("admin.manageApps")}</TabsTrigger>
<TabsTrigger value="services" className="flex-1">{t("admin.manageServices")}</TabsTrigger>
<TabsTrigger value="users" className="flex-1">{t("admin.manageUsers")}</TabsTrigger>
<TabsTrigger value="settings" className="flex-1">{t("admin.siteSettings")}</TabsTrigger>
</TabsList>
<div className="flex-1 flex w-full">
{/* Sidebar (desktop) */}
<aside className="hidden md:block w-60 shrink-0 border-e border-slate-200/70 bg-white/40 backdrop-blur-sm p-3">
<div className="sticky top-20">
{renderNavList()}
</div>
</aside>
{/* Apps Tab */}
<TabsContent value="apps" className="space-y-3">
{/* Main content */}
<div className="flex-1 min-w-0 p-4 pb-8">
{section === "dashboard" && (
<div className="glass-panel rounded-2xl p-8 text-center text-muted-foreground">
{t("admin.dashboardSoon")}
</div>
)}
{section === "apps" && (
<div className="space-y-3">
<div className="flex justify-end">
<Button size="sm" onClick={() => setEditingApp({ form: emptyAppForm })}>
<Plus size={14} className="me-1" />
@@ -377,10 +440,11 @@ export default function AdminPage() {
</div>
</div>
))}
</TabsContent>
</div>
)}
{/* Services Tab */}
<TabsContent value="services" className="space-y-3">
{section === "services" && (
<div className="space-y-3">
<div className="flex justify-end">
<Button size="sm" onClick={() => setEditingService({ form: emptyServiceForm })}>
<Plus size={14} className="me-1" />
@@ -438,10 +502,11 @@ export default function AdminPage() {
</div>
</div>
))}
</TabsContent>
</div>
)}
{/* Users Tab */}
<TabsContent value="users" className="space-y-3">
{section === "users" && (
<div className="space-y-3">
<Button
size="sm"
className="w-full"
@@ -491,12 +556,15 @@ export default function AdminPage() {
</div>
</div>
))}
</TabsContent>
</div>
)}
<TabsContent value="settings" className="space-y-3">
<SiteSettingsPanel />
</TabsContent>
</Tabs>
{section === "settings" && (
<div className="space-y-3">
<SiteSettingsPanel />
</div>
)}
</div>
</div>
</div>
);