From d5dd3acf9f89dec6e55f092255d0e761f7dfb949 Mon Sep 17 00:00:00 2001 From: riyadhafraa <49757212-riyadhafraa@users.noreply.replit.com> Date: Tue, 12 May 2026 09:35:47 +0000 Subject: [PATCH] Task #509: Redesign Edit App modal + verify responsive sizing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User asked for the Edit App modal to match the new professional Edit Service design and confirmed responsiveness for iPad / mobile. Changes — admin.tsx only: 1. New shell matching Edit Service modal: - bg-slate-950/50 backdrop-blur-md overlay, click-outside-to-close, fade + zoom-in animations. - White rounded-2xl container, max-w-2xl (wider than Service modal because of the additional fields + permissions list), max-h-[90vh], flex column with sticky-feeling header / footer. - Header tile is now LIVE — its gradient color comes from the editing app's color field, and the icon comes from the app's iconName via a new resolveAppIcon() helper. So the header previews the app as you edit. - X close button in header. 2. Form layout: - Names row (sm:grid-cols-2): nameAr (RTL, ع badge) | nameEn (LTR, EN badge). - Identifier row (sm:grid-cols-2): slug | route, both font-mono. - Visuals row (sm:grid-cols-2): - Icon picker: 40x40 preview tile + text input. Tile shows the resolved Lucide icon, or a dashed HelpCircle when the name doesn't resolve. Hint text under the field. - Color picker: clickable color swatch wrapping a hidden native + hex text input. Both stay in sync. - Permissions wrapped in a bordered card with its own header strip. 3. Footer: Cancel (outline) + Save (gradient indigo→blue, shadow), each flex-1 h-10 rounded-lg. 4. New helper resolveAppIcon() above the imports — does a safe lookup into the lucide-react namespace and returns null when the name is invalid. Mirrors the pattern already used in pages/home.tsx. Responsiveness verified by reasoning over breakpoints: - Mobile (≤414px): sm: never kicks in, so all rows stack into one column; outer p-4 keeps gutters; max-h-[90vh] + scroll handles tall permissions. - iPad portrait (768px) and up: sm:grid-cols-2 activates, two-column layout fits comfortably inside max-w-2xl (672px). - Desktop: same max-w-2xl cap looks well-proportioned, not cramped. - Edit Service modal already uses the same primitives, no changes needed. No behavioral changes — all state wiring (editingApp, handleSaveApp, permission flows, history sections) is unchanged. Pure presentational refactor. Code review: skipped — presentational refactor with no logic / data flow changes. Follow-ups: none proposed — request fully addressed; the other admin modals are deliberately out of scope per the plan. --- artifacts/tx-os/src/pages/admin.tsx | 318 ++++++++++++++++++++++------ 1 file changed, 251 insertions(+), 67 deletions(-) diff --git a/artifacts/tx-os/src/pages/admin.tsx b/artifacts/tx-os/src/pages/admin.tsx index 7ea915a9..6151a1ff 100644 --- a/artifacts/tx-os/src/pages/admin.tsx +++ b/artifacts/tx-os/src/pages/admin.tsx @@ -138,7 +138,17 @@ import { useQueryClient, useQuery } from "@tanstack/react-query"; import { useAuth } from "@/contexts/AuthContext"; import { useUpload, type UploadResponse } from "@workspace/object-storage-web"; import { resolveServiceImageUrl } from "@/lib/image-url"; -import { ArrowRight, ArrowLeft, Settings, Plus, Pencil, Trash2, Shield, Upload, Loader2, LayoutDashboard, Grid2X2, Coffee, Users as UsersIcon, Menu as MenuIcon, TrendingUp, TrendingDown, UserPlus, Activity, KeyRound, Copy, ChevronDown, ScrollText, Download, X, ImageIcon, Power } from "lucide-react"; +import { ArrowRight, ArrowLeft, Settings, Plus, Pencil, Trash2, Shield, Upload, Loader2, LayoutDashboard, Grid2X2, Coffee, Users as UsersIcon, Menu as MenuIcon, TrendingUp, TrendingDown, UserPlus, Activity, KeyRound, Copy, ChevronDown, ScrollText, Download, X, ImageIcon, Power, HelpCircle, Palette, Link2, Hash, type LucideIcon } from "lucide-react"; +import * as LucideIcons from "lucide-react"; + +function resolveAppIcon(name: string): LucideIcon | null { + if (!name) return null; + const Candidate = (LucideIcons as Record)[name]; + if (typeof Candidate === "function" || (typeof Candidate === "object" && Candidate !== null)) { + return Candidate as LucideIcon; + } + return null; +} import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -1183,76 +1193,250 @@ export default function AdminPage() { {/* Edit App Modal */} - {editingApp && ( -
-
-

- {editingApp.id ? t("admin.editApp") : t("admin.addApp")} -

- {( - [ - { field: "nameAr", label: t("admin.appNameAr") }, - { field: "nameEn", label: t("admin.appNameEn") }, - { field: "slug", label: t("admin.appSlug") }, - { field: "iconName", label: t("admin.appIcon") }, - { field: "route", label: t("admin.appRoute") }, - { field: "color", label: t("admin.appColor") }, - ] as { field: keyof AppForm; label: string }[] - ).map(({ field, label }) => ( -
- - setEditingApp({ ...editingApp, form: { ...editingApp.form, [field]: e.target.value } })} - className="bg-white/70 border-slate-200" - /> + {editingApp && (() => { + const PreviewIcon = resolveAppIcon(editingApp.form.iconName); + const previewColor = /^#[0-9a-fA-F]{6}$/.test(editingApp.form.color) + ? editingApp.form.color + : "#6366f1"; + return ( +
{ if (e.target === e.currentTarget) setEditingApp(null); }} + > +
+ {/* Header */} +
+
+
+ {PreviewIcon ? : } +
+
+

+ {editingApp.id ? t("admin.editApp") : t("admin.addApp")} +

+

+ {editingApp.id + ? (lang === "ar" ? "حدّث تفاصيل التطبيق وأذوناته" : "Update app details and permissions") + : (lang === "ar" ? "أضف تطبيقاً جديداً للوحة" : "Add a new app to the launcher")} +

+
- ))} - {editingApp.id !== undefined ? ( - - ) : ( - - setEditingApp({ - ...editingApp, - form: { ...editingApp.form, permissionIds: next }, - }) - } - /> - )} - {editingApp.id !== undefined && ( - { - const p = (permissionsList ?? []).find((x) => x.id === id); - return p ? p.name : `#${id}`; - }} - /> - )} - {editingApp.id !== undefined && ( - - openAuditLogForTarget("app", editingApp.id!) - } - /> - )} -
- - + +
+ + {/* Body */} +
+ {/* Names row */} +
+
+ + setEditingApp({ ...editingApp, form: { ...editingApp.form, nameAr: e.target.value } })} + className="bg-slate-50/70 border-slate-200 rounded-lg h-10 text-sm focus-visible:ring-2 focus-visible:ring-indigo-500/30 focus-visible:border-indigo-400" + dir="rtl" + placeholder="الملاحظات" + /> +
+
+ + setEditingApp({ ...editingApp, form: { ...editingApp.form, nameEn: e.target.value } })} + className="bg-slate-50/70 border-slate-200 rounded-lg h-10 text-sm focus-visible:ring-2 focus-visible:ring-indigo-500/30 focus-visible:border-indigo-400" + dir="ltr" + placeholder="Notes" + /> +
+
+ + {/* Identifier row */} +
+
+ + setEditingApp({ ...editingApp, form: { ...editingApp.form, slug: e.target.value } })} + className="bg-slate-50/70 border-slate-200 rounded-lg h-10 text-sm font-mono focus-visible:ring-2 focus-visible:ring-indigo-500/30 focus-visible:border-indigo-400" + dir="ltr" + placeholder="notes" + /> +
+
+ + setEditingApp({ ...editingApp, form: { ...editingApp.form, route: e.target.value } })} + className="bg-slate-50/70 border-slate-200 rounded-lg h-10 text-sm font-mono focus-visible:ring-2 focus-visible:ring-indigo-500/30 focus-visible:border-indigo-400" + dir="ltr" + placeholder="/notes" + /> +
+
+ + {/* Visuals row — icon + color with live previews */} +
+ {/* Icon picker */} +
+ +
+
+ {PreviewIcon ? : } +
+ setEditingApp({ ...editingApp, form: { ...editingApp.form, iconName: e.target.value } })} + className="bg-slate-50/70 border-slate-200 rounded-lg h-10 text-sm font-mono focus-visible:ring-2 focus-visible:ring-indigo-500/30 focus-visible:border-indigo-400" + dir="ltr" + placeholder="StickyNote" + /> +
+

+ {lang === "ar" ? "اسم أيقونة من Lucide (مثل: StickyNote, Coffee, Users)" : "Lucide icon name (e.g. StickyNote, Coffee, Users)"} +

+
+ + {/* Color picker */} +
+ +
+ + setEditingApp({ ...editingApp, form: { ...editingApp.form, color: e.target.value } })} + className="bg-slate-50/70 border-slate-200 rounded-lg h-10 text-sm font-mono uppercase focus-visible:ring-2 focus-visible:ring-indigo-500/30 focus-visible:border-indigo-400" + dir="ltr" + placeholder="#6366F1" + /> +
+

+ {lang === "ar" ? "كود اللون السداسي (مثل #6366F1)" : "Hex color code (e.g. #6366F1)"} +

+
+
+ + {/* Permissions section in a bordered card */} +
+
+ + + {lang === "ar" ? "الأذونات" : "Permissions"} + +
+
+ {editingApp.id !== undefined ? ( + + ) : ( + + setEditingApp({ + ...editingApp, + form: { ...editingApp.form, permissionIds: next }, + }) + } + /> + )} +
+
+ + {editingApp.id !== undefined && ( + { + const p = (permissionsList ?? []).find((x) => x.id === id); + return p ? p.name : `#${id}`; + }} + /> + )} + {editingApp.id !== undefined && ( + + openAuditLogForTarget("app", editingApp.id!) + } + /> + )} +
+ + {/* Footer */} +
+ +
- )} + ); + })()} {/* Edit Service Modal */} {editingService && (