feat: complete UI/UX improvements, vuesax icons, media server, and section management

This commit is contained in:
s.esmaeili
2026-10-23 21:06:15 +03:30
parent 49f30a96b1
commit fa61a76444
64 changed files with 4527 additions and 819 deletions
+16
View File
@@ -143,6 +143,18 @@ export interface Paginated<T> {
pageSize: number;
}
export interface DashboardCategoryStat {
id: number;
name: string;
count: number;
}
export interface DashboardTrendDay {
date: string;
dayName: string;
messages: number;
}
export interface DashboardDto {
categories: number;
products: number;
@@ -151,4 +163,8 @@ export interface DashboardDto {
media: number;
recentMessages: ContactMessageDto[];
recentActivity: { id: number; action: string; entity: string; summary: string; user: string | null; createdAt: string }[];
categoryStats: DashboardCategoryStat[];
messageBreakdown: { new: number; read: number; archived: number; total: number };
sectionsSummary: { total: number; visible: number; hidden: number };
trend: DashboardTrendDay[];
}
+1 -1
View File
@@ -15,7 +15,7 @@ export const slugSchema = z
.max(80)
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "فقط حروف کوچک انگلیسی، عدد و خط تیره (مثلاً safety-shoes)");
export const iconNameSchema = z.enum(ICON_NAMES);
export const iconNameSchema = z.string().trim().min(1).max(80);
/** Image stored by URL inside section content */
export const imageRefSchema = z.object({
+152 -52
View File
@@ -1,56 +1,156 @@
/**
* Lucide icon names that content editors may pick in the admin panel.
* The web app keeps a matching registry (DynamicIcon) so only these icons end up in the bundle.
* Vuesax linear icon aliases and recommended picker list.
*/
export const ICON_NAMES = [
"ShieldCheck",
"ShieldPlus",
"ShieldAlert",
"BadgeCheck",
"Award",
"Medal",
"PackageCheck",
"Package",
"Truck",
"Warehouse",
"Globe",
"Users",
"Handshake",
"Headset",
"ClipboardCheck",
"FileCheck",
"FileSearch",
"GraduationCap",
"Gauge",
"Microscope",
"Wrench",
"HardHat",
"Shirt",
"Footprints",
"Hand",
"Glasses",
"Ear",
"Wind",
"TriangleAlert",
"Siren",
"TrafficCone",
"Construction",
"Factory",
"Building2",
"Fuel",
"Pickaxe",
"Flame",
"Zap",
"Leaf",
"HeartPulse",
"Clock",
"Phone",
"Mail",
"MapPin",
"Star",
"ThumbsUp",
"Target",
"Lock",
export const ICON_ALIASES: Record<string, string> = {
// Navigation & Actions
ArrowLeft: "arrow-left",
ArrowRight: "arrow-right",
ArrowUp: "arrow-up",
ArrowDown: "arrow-down",
ChevronDown: "arrow-down-1",
ChevronUp: "arrow-up-1",
ChevronLeft: "arrow-left-1",
ChevronRight: "arrow-right-1",
Menu: "menu",
X: "close-circle",
Plus: "add",
Trash: "trash",
Edit: "edit-2",
Copy: "copy",
Check: "tick-circle",
Search: "search-normal",
Filter: "filter",
Eye: "eye",
EyeOff: "eye-slash",
ImageOff: "gallery-slash",
Quote: "quote-up",
// Contact & Social
Phone: "call",
Mail: "sms",
MapPin: "location",
Clock: "clock",
Globe: "global",
Send: "send-2",
Instagram: "instagram",
Linkedin: "link",
MessageCircle: "messages-2",
Youtube: "video-play",
AtSign: "sms",
// Equipment & Safety Categories
HardHat: "security-safe",
Glasses: "glass",
Shirt: "tag",
Footprints: "routing-2",
Hand: "shield-tick",
Ear: "volume-high",
Wind: "wind-2",
TriangleAlert: "danger",
Siren: "lamp-on",
TrafficCone: "danger",
Construction: "setting-2",
Factory: "building-4",
Building2: "buildings-2",
Fuel: "gas-station",
Pickaxe: "hierarchy",
Flame: "flash",
Zap: "flash-circle",
Leaf: "tree",
HeartPulse: "heart-tick",
Star: "star",
ThumbsUp: "like-1",
Target: "radar-2",
Lock: "lock",
// Trust, Certifications & Standards
ShieldCheck: "shield-tick",
ShieldPlus: "shield-security",
ShieldAlert: "shield-cross",
BadgeCheck: "verify",
Award: "award",
Medal: "medal-star",
PackageCheck: "box-tick",
Package: "box",
Truck: "truck-time",
Warehouse: "buildings",
Users: "people",
Handshake: "user-tag",
Headset: "24-support",
ClipboardCheck: "clipboard-tick",
FileCheck: "document-text",
FileSearch: "document-filter",
GraduationCap: "teacher",
Gauge: "status",
Microscope: "search-status",
Wrench: "setting-4",
};
export function resolveVuesaxIcon(name: string): string {
if (!name) return "shield-tick";
if (name in ICON_ALIASES) return ICON_ALIASES[name];
return name.replace(/^vuesax-linear:/, "");
}
/**
* Recommended icon names displayed in pickers and dropdowns
*/
export const RECOMMENDED_ICONS = [
"shield-tick",
"shield-security",
"shield-cross",
"security-safe",
"security-user",
"verify",
"award",
"medal-star",
"box-tick",
"box",
"truck-time",
"buildings",
"building-4",
"buildings-2",
"global",
"people",
"user-tag",
"24-support",
"call",
"sms",
"location",
"clock",
"clipboard-tick",
"document-text",
"document-filter",
"teacher",
"status",
"search-status",
"setting-2",
"setting-4",
"glass",
"tag",
"routing-2",
"volume-high",
"wind-2",
"danger",
"lamp-on",
"gas-station",
"hierarchy",
"flash",
"flash-circle",
"tree",
"heart-tick",
"star",
"like-1",
"radar-2",
"lock",
"eye",
"gallery",
"video-play",
] as const;
export type IconName = (typeof ICON_NAMES)[number];
export const ICON_NAMES = [
...RECOMMENDED_ICONS,
...Object.keys(ICON_ALIASES),
] as const;
export type IconName = string;
+11
View File
@@ -77,6 +77,7 @@ export const settingsSectionSchema = z.object({
});
export const heroSectionSchema = z.object({
enabled: z.boolean().default(true),
badge: req(120),
title: req(80),
titleHighlight: req(80),
@@ -91,10 +92,12 @@ export const heroSectionSchema = z.object({
});
export const tapeSectionSchema = z.object({
enabled: z.boolean().default(true),
items: z.array(req(80)).min(1).max(20),
});
export const statsSectionSchema = z.object({
enabled: z.boolean().default(true),
items: z
.array(
z.object({
@@ -108,12 +111,14 @@ export const statsSectionSchema = z.object({
});
export const productsIntroSectionSchema = z.object({
enabled: z.boolean().default(true),
head: sectionHeadSchema,
ctaLabel: req(60),
trust: z.array(iconCard).max(6),
});
export const aboutSectionSchema = z.object({
enabled: z.boolean().default(true),
head: sectionHeadSchema,
body: req(2000),
image: imageRefSchema,
@@ -123,6 +128,7 @@ export const aboutSectionSchema = z.object({
});
export const servicesSectionSchema = z.object({
enabled: z.boolean().default(true),
head: sectionHeadSchema,
items: z
.array(
@@ -137,6 +143,7 @@ export const servicesSectionSchema = z.object({
});
export const standardsSectionSchema = z.object({
enabled: z.boolean().default(true),
head: sectionHeadSchema,
note: text(500),
ctaLabel: req(80),
@@ -144,6 +151,7 @@ export const standardsSectionSchema = z.object({
});
export const industriesSectionSchema = z.object({
enabled: z.boolean().default(true),
head: sectionHeadSchema,
items: z
.array(
@@ -158,11 +166,13 @@ export const industriesSectionSchema = z.object({
});
export const testimonialsSectionSchema = z.object({
enabled: z.boolean().default(true),
head: sectionHeadSchema,
items: z.array(z.object({ text: req(600), name: req(80), role: text(160) })).max(12),
});
export const contactSectionSchema = z.object({
enabled: z.boolean().default(true),
head: sectionHeadSchema,
emergencyTitle: req(80),
emergencyText: req(400),
@@ -173,6 +183,7 @@ export const contactSectionSchema = z.object({
});
export const footerSectionSchema = z.object({
enabled: z.boolean().default(true),
about: req(500),
copyright: req(160),
certLine: text(160),