-
-
-
-
-
-
-
-
-
{{ hint }}
-
{{ err }}
-
-
![]()
-
+
+
+
diff --git a/apps/adm/src/components/SchemaFields.vue b/apps/adm/src/components/SchemaFields.vue
new file mode 100644
index 0000000..8535d5a
--- /dev/null
+++ b/apps/adm/src/components/SchemaFields.vue
@@ -0,0 +1,163 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ افزودن {{ field.itemLabel }}
+
+
+
+
+
+
{{ field.label }}
+
{{ field.hint }}
+
+
+ {{ field.itemLabel }} {{ toFa(i + 1) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ افزودن {{ field.itemLabel }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/adm/src/composables/useLeadsCount.ts b/apps/adm/src/composables/useLeadsCount.ts
new file mode 100644
index 0000000..78e405c
--- /dev/null
+++ b/apps/adm/src/composables/useLeadsCount.ts
@@ -0,0 +1,16 @@
+import { ref } from "vue";
+import { api } from "../lib/api";
+
+const newCount = ref(0);
+
+export function useLeadsCount() {
+ async function refresh() {
+ try {
+ const { data } = await api.get<{ newCount: number }>("/admin/leads", { params: { pageSize: 1 } });
+ newCount.value = data.newCount ?? 0;
+ } catch {
+ /* the badge is non-critical */
+ }
+ }
+ return { newCount, refresh };
+}
diff --git a/apps/adm/src/fonts.css b/apps/adm/src/fonts.css
deleted file mode 100644
index 405b3c7..0000000
--- a/apps/adm/src/fonts.css
+++ /dev/null
@@ -1,65 +0,0 @@
-/* IRANSans from landing assets (shared path in monorepo) */
-@font-face {
- font-family: "IRANSans";
- src: url("../../landing/assets/fonts/iranSans/woff2/IRANSansWeb(FaNum)_UltraLight.woff2") format("woff2"),
- url("../../landing/assets/fonts/iranSans/woff/IRANSansWeb(FaNum)_UltraLight.woff") format("woff");
- font-weight: 200;
- font-style: normal;
- font-display: swap;
-}
-@font-face {
- font-family: "IRANSans";
- src: url("../../landing/assets/fonts/iranSans/woff2/IRANSansWeb(FaNum)_Light.woff2") format("woff2"),
- url("../../landing/assets/fonts/iranSans/woff/IRANSansWeb(FaNum)_Light.woff") format("woff");
- font-weight: 300;
- font-style: normal;
- font-display: swap;
-}
-@font-face {
- font-family: "IRANSans";
- src: url("../../landing/assets/fonts/iranSans/woff2/IRANSansWeb(FaNum).woff2") format("woff2"),
- url("../../landing/assets/fonts/iranSans/woff/IRANSansWeb(FaNum).woff") format("woff");
- font-weight: 400;
- font-style: normal;
- font-display: swap;
-}
-@font-face {
- font-family: "IRANSans";
- src: url("../../landing/assets/fonts/iranSans/woff2/IRANSansWeb(FaNum)_Medium.woff2") format("woff2"),
- url("../../landing/assets/fonts/iranSans/woff/IRANSansWeb(FaNum)_Medium.woff") format("woff");
- font-weight: 500;
- font-style: normal;
- font-display: swap;
-}
-@font-face {
- font-family: "IRANSans";
- src: url("../../landing/assets/fonts/iranSans/woff2/IRANSansWeb(FaNum)_Medium.woff2") format("woff2"),
- url("../../landing/assets/fonts/iranSans/woff/IRANSansWeb(FaNum)_Medium.woff") format("woff");
- font-weight: 600;
- font-style: normal;
- font-display: swap;
-}
-@font-face {
- font-family: "IRANSans";
- src: url("../../landing/assets/fonts/iranSans/woff2/IRANSansWeb(FaNum)_Bold.woff2") format("woff2"),
- url("../../landing/assets/fonts/iranSans/woff/IRANSansWeb(FaNum)_Bold.woff") format("woff");
- font-weight: 700;
- font-style: normal;
- font-display: swap;
-}
-@font-face {
- font-family: "IRANSans";
- src: url("../../landing/assets/fonts/iranSans/woff2/IRANSansWeb(FaNum)_Black.woff2") format("woff2"),
- url("../../landing/assets/fonts/iranSans/woff/IRANSansWeb(FaNum)_Black.woff") format("woff");
- font-weight: 800;
- font-style: normal;
- font-display: swap;
-}
-@font-face {
- font-family: "IRANSans";
- src: url("../../landing/assets/fonts/iranSans/woff2/IRANSansWeb(FaNum)_Black.woff2") format("woff2"),
- url("../../landing/assets/fonts/iranSans/woff/IRANSansWeb(FaNum)_Black.woff") format("woff");
- font-weight: 900;
- font-style: normal;
- font-display: swap;
-}
diff --git a/apps/adm/src/layouts/AdminLayout.vue b/apps/adm/src/layouts/AdminLayout.vue
index 202dd73..a244727 100644
--- a/apps/adm/src/layouts/AdminLayout.vue
+++ b/apps/adm/src/layouts/AdminLayout.vue
@@ -1,67 +1,163 @@
-
-
-
-
-
-
+
+
+
+
+ منتک پنل مدیریت
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/adm/src/lib/api.ts b/apps/adm/src/lib/api.ts
index 30df757..81b63ec 100644
--- a/apps/adm/src/lib/api.ts
+++ b/apps/adm/src/lib/api.ts
@@ -1,4 +1,4 @@
-import axios from "axios";
+import axios, { isAxiosError } from "axios";
const TOKEN_KEY = "montech_adm_token";
@@ -12,6 +12,18 @@ api.interceptors.request.use((config) => {
return config;
});
+api.interceptors.response.use(
+ (res) => res,
+ (error) => {
+ if (isAxiosError(error) && error.response?.status === 401 && !error.config?.url?.startsWith("/auth/login")) {
+ setToken(null);
+ const redirect = encodeURIComponent(window.location.pathname.replace(/^\/adm/, "") || "/");
+ window.location.assign(`/adm/login?redirect=${redirect}`);
+ }
+ return Promise.reject(error);
+ },
+);
+
export function setToken(token: string | null) {
if (token) localStorage.setItem(TOKEN_KEY, token);
else localStorage.removeItem(TOKEN_KEY);
@@ -20,3 +32,10 @@ export function setToken(token: string | null) {
export function getToken() {
return localStorage.getItem(TOKEN_KEY);
}
+
+export function apiErrorMessage(error: unknown, fallback = "خطا در ارتباط با سرور.") {
+ if (!isAxiosError(error)) return fallback;
+ const detail = (error.response?.data as { message?: string | string[] } | undefined)?.message;
+ if (Array.isArray(detail)) return detail.join("، ");
+ return detail || fallback;
+}
diff --git a/apps/adm/src/main.ts b/apps/adm/src/main.ts
index 8dd6185..e92d814 100644
--- a/apps/adm/src/main.ts
+++ b/apps/adm/src/main.ts
@@ -1,5 +1,9 @@
import { createApp } from "vue";
import { createPinia } from "pinia";
+import "@fontsource/vazirmatn/400.css";
+import "@fontsource/vazirmatn/500.css";
+import "@fontsource/vazirmatn/700.css";
+import "ant-design-vue/dist/reset.css";
import App from "./App.vue";
import router from "./router";
import "./styles.css";
diff --git a/apps/adm/src/router/index.ts b/apps/adm/src/router/index.ts
index 813243d..b2fae79 100644
--- a/apps/adm/src/router/index.ts
+++ b/apps/adm/src/router/index.ts
@@ -10,6 +10,7 @@ const router = createRouter({
component: () => import("../layouts/AdminLayout.vue"),
children: [
{ path: "", name: "dashboard", component: () => import("../views/DashboardView.vue") },
+ { path: "leads", name: "leads", component: () => import("../views/LeadsView.vue") },
{ path: "settings", name: "settings", component: () => import("../views/SettingsView.vue") },
{ path: "sections", name: "sections", component: () => import("../views/SectionsView.vue") },
{ path: "sections/:key", name: "section-edit", component: () => import("../views/SectionEditView.vue") },
@@ -21,6 +22,7 @@ const router = createRouter({
{ path: "media", name: "media", component: () => import("../views/MediaView.vue") },
],
},
+ { path: "/:pathMatch(.*)*", redirect: "/" },
],
});
diff --git a/apps/adm/src/sections/schema.ts b/apps/adm/src/sections/schema.ts
new file mode 100644
index 0000000..68f581e
--- /dev/null
+++ b/apps/adm/src/sections/schema.ts
@@ -0,0 +1,260 @@
+import {
+ METRIC_ICONS,
+ SOLUTION_ACCENTS,
+ SOLUTION_ICONS,
+ type SectionKey,
+} from "@montech/shared";
+
+export interface Option {
+ value: string;
+ label: string;
+}
+
+interface BaseField {
+ key: string;
+ label: string;
+ hint?: string;
+ /** Grid span out of 24 on desktop. */
+ span?: number;
+}
+
+export type Field =
+ | (BaseField & { kind: "text" | "textarea" | "image"; ltr?: boolean; required?: boolean })
+ | (BaseField & { kind: "number"; min?: number; max?: number })
+ | (BaseField & { kind: "select"; options: Option[] })
+ | (BaseField & { kind: "strings"; itemLabel: string; item?: "text" | "image"; fixed?: number; max?: number })
+ | (BaseField & { kind: "list"; itemLabel: string; fields: Field[]; newItem: () => Record
; max?: number })
+ | (BaseField & { kind: "group"; fields: Field[] });
+
+export interface SectionSchema {
+ description: string;
+ fields: Field[];
+}
+
+const linkFields: Field[] = [
+ { kind: "text", key: "label", label: "متن", span: 12, required: true },
+ { kind: "text", key: "href", label: "لینک", span: 12, ltr: true, hint: "مثلاً #solutions یا /blog" },
+];
+
+const newLink = () => ({ label: "", href: "#" });
+
+const opt = (values: readonly string[], labels: Record): Option[] =>
+ values.map((v) => ({ value: v, label: labels[v] ?? v }));
+
+export const SECTION_SCHEMAS: Record = {
+ header: {
+ description: "لوگو، منوی بالای صفحه و دکمه مشاوره.",
+ fields: [
+ { kind: "text", key: "brandTitle", label: "نام برند", span: 12, required: true },
+ { kind: "text", key: "brandSubtitle", label: "زیرعنوان برند", span: 12, ltr: true },
+ { kind: "text", key: "ctaLabel", label: "متن دکمه مشاوره", span: 12 },
+ {
+ kind: "list",
+ key: "nav",
+ label: "آیتمهای منو",
+ hint: "به ترتیب خواندن از راست به چپ.",
+ itemLabel: "آیتم",
+ fields: linkFields,
+ newItem: newLink,
+ max: 8,
+ },
+ ],
+ },
+ hero: {
+ description: "تیتر اصلی، جستجوی هوشمند، ماکاپ لپتاپ/موبایل و پنجره پیشنهاد هوشمند.",
+ fields: [
+ { kind: "text", key: "eyebrow", label: "برچسب بالای تیتر", span: 12 },
+ { kind: "text", key: "titleLine1", label: "خط اول تیتر", span: 12, required: true },
+ { kind: "text", key: "titleHighlight", label: "خط رنگی تیتر", span: 12, required: true },
+ { kind: "textarea", key: "subtitle", label: "توضیح زیر تیتر" },
+ { kind: "text", key: "searchPlaceholder", label: "متن راهنمای جستجو", span: 12 },
+ { kind: "text", key: "searchDefaultQuery", label: "پرسوجوی پیشفرض (جستجوی خالی)", span: 12 },
+ { kind: "text", key: "suggestionsLabel", label: "عنوان پیشنهادها", span: 12 },
+ {
+ kind: "list",
+ key: "suggestions",
+ label: "پیشنهادهای پرطرفدار",
+ itemLabel: "پیشنهاد",
+ max: 6,
+ fields: [
+ { kind: "text", key: "label", label: "متن دکمه", span: 8, required: true },
+ { kind: "text", key: "query", label: "پرسوجوی ارسالی", span: 16 },
+ ],
+ newItem: () => ({ label: "", query: "" }),
+ },
+ {
+ kind: "group",
+ key: "mockup",
+ label: "ماکاپ لپتاپ و موبایل",
+ fields: [
+ { kind: "text", key: "domain", label: "دامنه نوار آدرس", span: 12, ltr: true },
+ { kind: "text", key: "version", label: "نسخه", span: 12 },
+ { kind: "text", key: "salesValue", label: "مبلغ فروش", span: 12 },
+ { kind: "text", key: "salesGrowth", label: "رشد فروش", span: 12 },
+ { kind: "text", key: "customersValue", label: "تعداد مشتریان", span: 12 },
+ { kind: "text", key: "satisfaction", label: "شاخص رضایت", span: 12 },
+ { kind: "text", key: "chartDefaultTooltip", label: "برچسب پیشفرض نمودار", span: 12 },
+ { kind: "number", key: "goldBarCount", label: "موجودی شمش (موبایل)", span: 12, min: 0, max: 9999 },
+ { kind: "strings", key: "chartTooltips", label: "برچسب نقاط نمودار", itemLabel: "نقطه", fixed: 3 },
+ ],
+ },
+ {
+ kind: "group",
+ key: "ai",
+ label: "پنجره پیشنهاد هوشمند",
+ fields: [
+ { kind: "text", key: "title", label: "عنوان پنجره", span: 12 },
+ { kind: "text", key: "queryLabel", label: "برچسب پرسوجو", span: 12 },
+ { kind: "text", key: "fallbackQuery", label: "پرسوجوی جایگزین", span: 12 },
+ { kind: "text", key: "packageLabel", label: "برچسب بسته", span: 12 },
+ { kind: "text", key: "packageTitle", label: "عنوان بسته", span: 12 },
+ { kind: "text", key: "matchBadge", label: "نشان تطابق", span: 12 },
+ { kind: "strings", key: "items", label: "موارد بسته", itemLabel: "مورد", max: 6 },
+ { kind: "text", key: "primaryCta", label: "دکمه ثبت مشاوره", span: 12 },
+ { kind: "text", key: "closeLabel", label: "دکمه بستن", span: 12 },
+ ],
+ },
+ ],
+ },
+ metrics: {
+ description: "نوار سهتایی آمار زیر هیرو.",
+ fields: [
+ {
+ kind: "list",
+ key: "items",
+ label: "آمار",
+ itemLabel: "آمار",
+ max: 3,
+ fields: [
+ { kind: "select", key: "icon", label: "آیکون", span: 6, options: opt(METRIC_ICONS, { gear: "چرخدنده", users: "کاربران", box: "جعبه" }) },
+ { kind: "text", key: "value", label: "مقدار", span: 6, required: true },
+ { kind: "text", key: "label", label: "برچسب", span: 6 },
+ { kind: "select", key: "emphasis", label: "نمایش", span: 6, options: [{ value: "number", label: "عدد بزرگ" }, { value: "text", label: "متن پررنگ" }] },
+ ],
+ newItem: () => ({ icon: "users", value: "", label: "", emphasis: "number" }),
+ },
+ ],
+ },
+ features: {
+ description: "بخش «از نسخه پایه تا راهکار اختصاصی» با کارتهای سهبعدی.",
+ fields: [
+ { kind: "text", key: "eyebrow", label: "برچسب", span: 12 },
+ { kind: "text", key: "titleLine1", label: "خط اول تیتر", span: 12 },
+ { kind: "text", key: "titleLine2", label: "خط دوم تیتر (رنگی)", span: 12 },
+ { kind: "textarea", key: "body", label: "توضیح" },
+ { kind: "text", key: "ctaLabel", label: "متن دکمه", span: 12 },
+ { kind: "text", key: "ctaHref", label: "لینک دکمه", span: 12, ltr: true },
+ { kind: "text", key: "baseCardLabel", label: "برچسب کارت پایه", span: 12 },
+ { kind: "text", key: "customCardTitle", label: "عنوان کارت اختصاصی", span: 12 },
+ { kind: "text", key: "customCardBadge", label: "نشان کارت اختصاصی", span: 12 },
+ { kind: "text", key: "salesLabel", label: "برچسب فروش", span: 12 },
+ { kind: "text", key: "salesValue", label: "مبلغ فروش", span: 12 },
+ { kind: "text", key: "linkNote", label: "یادداشت اتصال", span: 12 },
+ { kind: "text", key: "aiLabel", label: "برچسب هوش مصنوعی", span: 8 },
+ { kind: "text", key: "aiStatus", label: "وضعیت", span: 8 },
+ { kind: "text", key: "aiNote", label: "توضیح", span: 8 },
+ ],
+ },
+ solutions: {
+ description: "کارتهای محصولات؛ کلیک روی هر کارت فرم مشاوره را با زمینه انتخابشده باز میکند.",
+ fields: [
+ { kind: "text", key: "eyebrow", label: "برچسب", span: 8 },
+ { kind: "text", key: "title", label: "عنوان", span: 8 },
+ { kind: "text", key: "subtitle", label: "زیرعنوان", span: 8 },
+ {
+ kind: "list",
+ key: "items",
+ label: "کارتها",
+ itemLabel: "کارت",
+ max: 10,
+ fields: [
+ { kind: "text", key: "title", label: "عنوان", span: 12, required: true },
+ { kind: "text", key: "subtitle", label: "زیرعنوان", span: 12 },
+ { kind: "text", key: "consultType", label: "زمینه در فرم مشاوره", span: 8, hint: "باید با یکی از گزینههای فرم مشاوره یکسان باشد." },
+ {
+ kind: "select",
+ key: "accent",
+ label: "رنگ",
+ span: 8,
+ options: opt(SOLUTION_ACCENTS, { brand: "آبی برند", sky: "آسمانی", indigo: "نیلی", cyan: "فیروزهای", amber: "طلایی" }),
+ },
+ {
+ kind: "select",
+ key: "icon",
+ label: "آیکون",
+ span: 8,
+ options: opt(SOLUTION_ICONS, { cube: "مکعب", bars: "نمودار", avatars: "کاربران", cart: "سبد خرید", gold: "شمش طلا" }),
+ },
+ ],
+ newItem: () => ({ title: "", subtitle: "", consultType: "سایر", accent: "brand", icon: "cube" }),
+ },
+ ],
+ },
+ testimonials: {
+ description: "بخش اعتماد کسبوکارها و کارت نظر مشتری.",
+ fields: [
+ { kind: "text", key: "eyebrow", label: "برچسب", span: 12 },
+ { kind: "text", key: "titleLine1", label: "خط اول تیتر", span: 12 },
+ { kind: "text", key: "titleLine2", label: "خط دوم تیتر (رنگی)", span: 12 },
+ { kind: "textarea", key: "body", label: "توضیح" },
+ { kind: "text", key: "ctaLabel", label: "متن دکمه", span: 12 },
+ { kind: "text", key: "badge", label: "نشان کارت", span: 12 },
+ { kind: "textarea", key: "quote", label: "متن نظر" },
+ { kind: "text", key: "name", label: "نام مشتری", span: 12 },
+ { kind: "text", key: "role", label: "سمت", span: 12 },
+ { kind: "image", key: "avatarUrl", label: "تصویر مشتری" },
+ { kind: "strings", key: "decorAvatars", label: "آواتارهای تزئینی", itemLabel: "تصویر", item: "image", fixed: 2 },
+ ],
+ },
+ cta: {
+ description: "بنر آبی دعوت به مشاوره رایگان.",
+ fields: [
+ { kind: "text", key: "titleLine1", label: "خط اول تیتر", span: 12 },
+ { kind: "text", key: "titleLine2", label: "خط دوم تیتر", span: 12 },
+ { kind: "textarea", key: "body", label: "توضیح" },
+ { kind: "text", key: "ctaLabel", label: "متن دکمه", span: 12 },
+ ],
+ },
+ footer: {
+ description: "ستونهای فوتر. تلفن، ایمیل، آدرس و شبکههای اجتماعی از «تنظیمات سایت» خوانده میشوند.",
+ fields: [
+ { kind: "text", key: "contactTitle", label: "عنوان ستون تماس", span: 8 },
+ { kind: "text", key: "productsTitle", label: "عنوان ستون محصولات", span: 8 },
+ { kind: "text", key: "quickLinksTitle", label: "عنوان دسترسی سریع", span: 8 },
+ { kind: "list", key: "products", label: "لینکهای محصولات", itemLabel: "لینک", fields: linkFields, newItem: newLink, max: 10 },
+ { kind: "list", key: "quickLinks", label: "لینکهای دسترسی سریع", itemLabel: "لینک", fields: linkFields, newItem: newLink, max: 10 },
+ { kind: "text", key: "brandTitle", label: "نام برند", span: 12 },
+ { kind: "text", key: "brandSubtitle", label: "زیرعنوان برند", span: 12, ltr: true },
+ { kind: "strings", key: "descriptionLines", label: "خطوط توضیح برند", itemLabel: "خط", max: 3 },
+ { kind: "text", key: "copyright", label: "متن کپیرایت" },
+ { kind: "list", key: "legalLinks", label: "لینکهای حقوقی", itemLabel: "لینک", fields: linkFields, newItem: newLink, max: 4 },
+ ],
+ },
+ consult: {
+ description: "پنجره درخواست مشاوره رایگان؛ درخواستها در بخش «درخواستهای مشاوره» ثبت میشوند.",
+ fields: [
+ { kind: "text", key: "title", label: "عنوان پنجره" },
+ { kind: "text", key: "nameLabel", label: "برچسب نام", span: 12 },
+ { kind: "text", key: "namePlaceholder", label: "راهنمای نام", span: 12 },
+ { kind: "text", key: "phoneLabel", label: "برچسب تلفن", span: 12 },
+ { kind: "text", key: "phonePlaceholder", label: "راهنمای تلفن", span: 12, ltr: true },
+ { kind: "text", key: "typeLabel", label: "برچسب زمینه کسبوکار", span: 12 },
+ {
+ kind: "list",
+ key: "types",
+ label: "گزینههای زمینه کسبوکار",
+ hint: "«مقدار» در درخواست ثبت میشود و باید با «زمینه در فرم مشاوره» کارتهای راهکار یکسان باشد.",
+ itemLabel: "گزینه",
+ max: 12,
+ fields: [
+ { kind: "text", key: "value", label: "مقدار", span: 12, required: true },
+ { kind: "text", key: "label", label: "متن نمایشی", span: 12, required: true },
+ ],
+ newItem: () => ({ value: "", label: "" }),
+ },
+ { kind: "textarea", key: "successMessage", label: "پیام موفقیت" },
+ { kind: "textarea", key: "errorMessage", label: "پیام خطا" },
+ { kind: "text", key: "submitLabel", label: "متن دکمه ارسال", span: 12 },
+ ],
+ },
+};
diff --git a/apps/adm/src/styles.css b/apps/adm/src/styles.css
index 324ce8e..eec0be1 100644
--- a/apps/adm/src/styles.css
+++ b/apps/adm/src/styles.css
@@ -1,70 +1,46 @@
-@import "./fonts.css";
-
-:root {
- --bg: #0f1115;
- --panel: #171a21;
- --border: #2a2f3a;
- --text: #e8e9ed;
- --muted: #9aa0ab;
- --accent: #c8ff2e;
- --danger: #ff6b6b;
- --radius: 10px;
- font-family: "IRANSans", Tahoma, sans-serif;
+html,
+body,
+#app {
+ min-height: 100%;
}
-* { box-sizing: border-box; }
body {
margin: 0;
- background: var(--bg);
- color: var(--text);
- font-family: "IRANSans", Tahoma, sans-serif;
+ background: #f5f7fb;
+ font-family: Vazirmatn, Tahoma, sans-serif;
}
-a { color: inherit; text-decoration: none; }
-button, input, textarea, select {
- font: inherit;
+
+.page-header {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 20px;
}
-.btn {
- border: 0;
+
+.page-header h1 {
+ margin: 0;
+ font-size: 20px;
+ font-weight: 700;
+}
+
+.page-header p {
+ margin: 4px 0 0;
+ color: rgba(0, 0, 0, 0.45);
+}
+
+.ltr,
+.ltr input {
+ direction: ltr;
+ text-align: left;
+}
+
+.thumb {
+ width: 48px;
+ height: 48px;
+ object-fit: cover;
border-radius: 8px;
- padding: 0.55rem 1rem;
- background: var(--accent);
- color: #111;
- font-weight: 600;
- cursor: pointer;
-}
-.btn.ghost {
- background: transparent;
- color: var(--text);
- border: 1px solid var(--border);
-}
-.btn.danger { background: var(--danger); color: #fff; }
-.input, .textarea {
- width: 100%;
- background: #0c0e12;
- border: 1px solid var(--border);
- border-radius: 8px;
- color: var(--text);
- padding: 0.6rem 0.75rem;
-}
-.textarea { min-height: 140px; resize: vertical; }
-.card {
- background: var(--panel);
- border: 1px solid var(--border);
- border-radius: var(--radius);
- padding: 1rem;
-}
-.label { display: block; margin-bottom: 0.35rem; color: var(--muted); font-size: 0.85rem; }
-.field { margin-bottom: 0.9rem; }
-.table { width: 100%; border-collapse: collapse; }
-.table th, .table td {
- text-align: right;
- padding: 0.65rem 0.4rem;
- border-bottom: 1px solid var(--border);
-}
-.muted { color: var(--muted); }
-.row { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }
-.grid-2 {
- display: grid;
- gap: 1rem;
- grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
+ border: 1px solid #f0f0f0;
+ background: #fafafa;
}
diff --git a/apps/adm/src/views/BlogEditView.vue b/apps/adm/src/views/BlogEditView.vue
index 4b0ecf5..cae6fdc 100644
--- a/apps/adm/src/views/BlogEditView.vue
+++ b/apps/adm/src/views/BlogEditView.vue
@@ -1,63 +1,86 @@
-
-
{{ isNew ? "پست جدید" : "ویرایش پست" }}
-
بازگشت
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ذخیره
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/adm/src/views/BlogListView.vue b/apps/adm/src/views/BlogListView.vue
index 2b67b2a..741d979 100644
--- a/apps/adm/src/views/BlogListView.vue
+++ b/apps/adm/src/views/BlogListView.vue
@@ -1,42 +1,88 @@
-
-
بلاگ
-
پست جدید
+
-
-
- | عنوان | اسلاگ | وضعیت | |
-
-
-
- | {{ p.title }} |
- {{ p.slug }} |
- {{ p.published ? "منتشر" : "پیشنویس" }} |
-
- ویرایش
-
- |
-
-
-
+
+
+
+
+
+ {{ record.title }}
+
+
+ {{ record.slug }}
+
+
+ {{ record.published ? "منتشر" : "پیشنویس" }}
+
+
+
+ ویرایش
+
+ حذف
+
+
+
+
+
+
diff --git a/apps/adm/src/views/DashboardView.vue b/apps/adm/src/views/DashboardView.vue
index ebeb6fd..8c6a1ce 100644
--- a/apps/adm/src/views/DashboardView.vue
+++ b/apps/adm/src/views/DashboardView.vue
@@ -1,17 +1,80 @@
-
داشبورد
-
سلام {{ auth.admin?.name || "ادمین" }} — مدیریت محتوای montech.ir
-
-
ویرایش بخشهای لندینگ
-
مدیریت بلاگ
-
گالری
-
سئو صفحات
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ویرایش هیرو
+ راهکارها
+ اطلاعات تماس
+ پست جدید
+ سئو صفحات
+
+
diff --git a/apps/adm/src/views/GalleryView.vue b/apps/adm/src/views/GalleryView.vue
index 8d73a57..bf4879e 100644
--- a/apps/adm/src/views/GalleryView.vue
+++ b/apps/adm/src/views/GalleryView.vue
@@ -1,70 +1,128 @@
-
گالری
-