From cde6b68a393b61fc32fab6caab1b894d682a69c3 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Thu, 4 Jun 2026 18:25:06 +0330 Subject: [PATCH] [Admin] Redesign Settings as sidebar tabs + style password/toggle fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split the long settings page into 7 sidebar tabs (publish+AI, sources, channels, SMS, push, map, demo) with a single form so one Save persists everything; seed/clear/test are submit buttons targeting their handlers via asp-page-handler. Boolean settings now render as clean .toggle-row cards. CSS fix: the form input rule omitted input[type=password] (and url/email/search), so API-key/VAPID/token fields were unstyled — added them, plus accent-color + sizing for checkboxes/radios. Active tab persists across handler posts via sessionStorage; layout collapses to a horizontal tab strip on mobile. Co-Authored-By: Claude Opus 4.8 --- .../Pages/Admin/Settings.cshtml | 419 +++++++++--------- src/JobsMedical.Web/wwwroot/css/site.css | 43 +- 2 files changed, 254 insertions(+), 208 deletions(-) diff --git a/src/JobsMedical.Web/Pages/Admin/Settings.cshtml b/src/JobsMedical.Web/Pages/Admin/Settings.cshtml index 17610b3..bf25a02 100644 --- a/src/JobsMedical.Web/Pages/Admin/Settings.cshtml +++ b/src/JobsMedical.Web/Pages/Admin/Settings.cshtml @@ -1,233 +1,238 @@ @page @model JobsMedical.Web.Pages.Admin.SettingsModel @{ - ViewData["Title"] = "تنظیمات جمع‌آوری و هوش مصنوعی"; + ViewData["Title"] = "تنظیمات سامانه"; }
-

تنظیمات جمع‌آوری و هوش مصنوعی

+

تنظیمات سامانه

← بازگشت به صف

-
+
+ @if (Model.Saved is not null) {
✓ @Model.Saved
} @if (Model.DemoMsg is not null) {
@Model.DemoMsg
} -
-

حالت نمایشی (Demo)

-

داده‌های نمونه‌ی تهران را برای نمایش/دمو روی سایت قرار بده یا حذف کن. (تیک «حالت نمایشی» پایین را هم بزن تا پس از هر استقرار دوباره ساخته شود.)

-
-
-
-
-
@if (Model.SmsTest is not null) {
@Model.SmsTest
} -
-
- - -
- -
- @if (Model.Saved is not null) - { -
✓ @Model.Saved
- } -
-

کانال‌های اعلان (فعال / غیرفعال)

-

روشن/خاموش‌کردن هر کانال ارسال اعلان به کاربران. کلیدها و تنظیمات هر کانال در بخش‌های پایین‌تر.

-
- -

از طریق سرور خودمان ارسال می‌شود؛ نیازی به سرویس‌های گوگل ندارد و در ایران کار می‌کند.

-
-
- -

برای کد ورود و اعلان‌های مهم. کلید و تمپلیت را در بخش «پیامک ورود» پایین وارد کن.

-
-
- -

برای اعلان هنگام بسته‌بودن برنامه؛ ولی از سرویس مرورگر (گوگل) عبور می‌کند که در ایران اغلب فیلتر است.

-
-
+ +
-

حالت انتشار

-
- - -
-
- - -

در حالت خودکار و بدون AI، آگهی‌هایی با اطمینان بالاتر از این مقدار خودکار منتشر می‌شوند.

-
+ + -
+ +
+ +
+

حالت انتشار

+
+ + +
+
+ + +

در حالت خودکار و بدون AI، آگهی‌هایی با اطمینان بالاتر از این مقدار خودکار منتشر می‌شوند.

+
-

لایه هوش مصنوعی (اختیاری)

-
- -

در صورت فعال بودن، هر آگهی پیش از انتشار توسط مدل بررسی و تأیید/رد/ساختارمند می‌شود.

-
-
- - -

می‌تواند یک مدل self-hosted یا سرویس داخلی باشد (OpenAI/Anthropic در ایران مسدودند).

-
-
-
-
-
-
- - -

به مدل بگو چطور تأیید/رد کند و چه فیلدهایی را استخراج کند. خروجی باید JSON باشد.

-
-
- -
+
+

لایه هوش مصنوعی (اختیاری)

+ +
+ + +

می‌تواند یک مدل self-hosted یا سرویس داخلی باشد (OpenAI/Anthropic در ایران مسدودند).

+
+
+
+
+
+
+ + +

به مدل بگو چطور تأیید/رد کند و چه فیلدهایی را استخراج کند. خروجی باید JSON باشد.

+
+ +
-
+ +
+

منابع جمع‌آوری

+ +
+ + +
-

منابع جمع‌آوری (اسکرپ کانال‌ها)

-
- -
-
- - -
+ +
+ + +
-
- - - -
+ +
-
- - - -
+ +
+
+
+
-
- -
-
-
+ +
+ + +
+ + +
+ +
+ +
+ + +

یک کلاینت Xray/V2Ray کانفیگ vmess/vless/trojan تو را به یک پروکسی محلی تبدیل می‌کند (socks5:// یا socks4:// یا http://).

+
+
+ + +
+

کانال‌های اعلان (فعال / غیرفعال)

+

روشن/خاموش‌کردن هر کانال ارسال اعلان به کاربران. کلیدها در تب‌های «پیامک» و «پوش مرورگری».

+ + + +
+ + +
+

پیامک ورود (OTP) — کاوه‌نگار

+

روشن/خاموش‌کردن این کانال در تب «کانال‌های اعلان». (در صورت خاموش بودن، کد ورود روی صفحه نمایش داده می‌شود.)

+
+
+
+
+
+

روش پیشنهادی: تمپلیت verify/lookup با متغیر %token. اگر تمپلیت خالی باشد، پیامک ساده با خط ارسال فرستاده می‌شود.

+
+
+
+ +
+
+ + +
+

اعلان‌ها (Web Push / PWA)

+

روشن/خاموش‌کردن این کانال در تب «کانال‌های اعلان». اینجا فقط کلیدهای VAPID را وارد کن.

+
+
+
+

جفت‌کلید VAPID را یک‌بار بساز (web-push). بدون آن، اعلان محلی روی دستگاه کار می‌کند ولی ارسال از سرور نیاز به کلید دارد.

+
+ + +
+

نقشه (نشان)

+
+ + +

برای انتخاب موقعیت روی نقشه در فرم ثبت مرکز. بدون کلید، فقط دکمه «موقعیت فعلی من» نمایش داده می‌شود.

+
+
+ + +
+

حالت نمایشی (Demo)

+ +

ساخت/حذف فوری داده‌های نمونه‌ی تهران:

+
+ + +
+
+ +
+ +
- -
- - - -

آگهی‌های تکراری به‌صورت خودکار رد می‌شوند؛ هر اجرا فقط آگهی‌های جدید را می‌آورد.

-
- -
- - - -

موتور هر آدرس را می‌خواند و متن آگهی را استخراج می‌کند (عنوان og + بدنه محتوا). برای هر صفحه شغلی، آرشیو کانال یا آگهی طبقه‌بندی.

-
- -
- - - -

یک کلاینت Xray/V2Ray (سرویس جانبی) کانفیگ vmess/vless/trojan تو را به یک پروکسی محلی SOCKS تبدیل می‌کند؛ آدرس همان را اینجا بگذار (socks5:// یا socks4:// یا http://).

-
- -
- -

حالت نمایشی (Demo)

-
- -

برای ساخت/حذف فوری داده‌های نمونه از کارت بالای همین صفحه استفاده کن.

-
- -
- -

پیامک ورود (OTP) — کاوه‌نگار

-

روشن/خاموش‌کردن این کانال در بخش «کانال‌های اعلان» بالا. (در صورت خاموش بودن، کد ورود روی صفحه نمایش داده می‌شود.)

-
- - -
-
-
-
-
-

روش پیشنهادی: تمپلیت verify/lookup با متغیر %token. اگر تمپلیت خالی باشد، پیامک ساده با خط ارسال فرستاده می‌شود.

- -
-

نقشه (نشان)

-
- - -

برای انتخاب موقعیت روی نقشه در فرم ثبت مرکز. بدون کلید، فقط دکمه «موقعیت فعلی من» نمایش داده می‌شود.

-
- -
-

اعلان‌ها (Web Push / PWA)

-

روشن/خاموش‌کردن این کانال در بخش «کانال‌های اعلان» بالا. اینجا فقط کلیدهای VAPID را وارد کن.

-
- - -
-
- - -
-
- - -
-

جفت‌کلید VAPID را یک‌بار بساز (web-push). بدون آن، اعلان محلی روی دستگاه کار می‌کند ولی ارسال از سرور نیاز به کلید دارد.

- -
+ +@section Scripts { + +} diff --git a/src/JobsMedical.Web/wwwroot/css/site.css b/src/JobsMedical.Web/wwwroot/css/site.css index 72a8630..155386a 100644 --- a/src/JobsMedical.Web/wwwroot/css/site.css +++ b/src/JobsMedical.Web/wwwroot/css/site.css @@ -143,10 +143,13 @@ button, input, select, textarea, optgroup { font-family: inherit; } .search-card label { font-size: 12px; font-weight: 700; color: var(--muted); } /* ---------- Forms ---------- */ -select, input[type="text"], input[type="tel"], input[type="number"], textarea { +select, textarea, +input[type="text"], input[type="tel"], input[type="number"], input[type="password"], +input[type="email"], input[type="url"], input[type="search"] { width: 100%; padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px; font-family: inherit; font-size: 14px; background: #fff; color: var(--ink); } +input[type="checkbox"], input[type="radio"] { accent-color: var(--primary); width: 17px; height: 17px; } select:focus, input:focus, textarea:focus { outline: none; border-color: var(--primary); } label { font-size: 13px; } @@ -253,6 +256,44 @@ label { font-size: 13px; } .tour-count { font-size: 12px; color: var(--muted); } .tour-btns { display: flex; gap: 6px; } +/* ---------- Admin settings: sidebar tabs ---------- */ +.settings-layout { display: grid; grid-template-columns: 220px 1fr; gap: 18px; align-items: start; } +.settings-tabs { + position: sticky; top: 80px; display: flex; flex-direction: column; gap: 4px; + background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); + box-shadow: var(--shadow); padding: 8px; +} +.settings-tabs button { + display: flex; align-items: center; gap: 8px; width: 100%; text-align: start; + padding: 11px 12px; border: none; background: transparent; border-radius: 10px; + font-family: inherit; font-size: 14px; font-weight: 600; color: var(--muted); cursor: pointer; +} +.settings-tabs button:hover { background: var(--primary-soft); color: var(--primary-dark); } +.settings-tabs button.active { background: var(--primary); color: #fff; } +.settings-tabs .tab-ico { font-size: 16px; } + +.settings-panel { display: none; } +.settings-panel.active { display: block; animation: fadeIn .15s ease; } +@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } } +.settings-panel h3:first-child { margin-top: 0; } + +/* Toggle rows — give each boolean field a clean, card-like row. */ +.toggle-row { + display: flex; align-items: flex-start; gap: 10px; font-weight: 700; + padding: 12px 14px; border: 1px solid var(--line); border-radius: 12px; background: var(--bg); + cursor: pointer; margin-bottom: 4px; +} +.toggle-row input[type="checkbox"] { margin-top: 2px; flex: 0 0 auto; } +.toggle-row .t-body { display: flex; flex-direction: column; gap: 3px; } +.toggle-row .t-hint { font-size: 12px; font-weight: 500; color: var(--muted); } +.settings-save { position: sticky; bottom: 0; padding-top: 12px; background: linear-gradient(transparent, var(--bg) 40%); } + +@media (max-width: 760px) { + .settings-layout { grid-template-columns: 1fr; } + .settings-tabs { position: static; flex-direction: row; overflow-x: auto; } + .settings-tabs button { white-space: nowrap; } +} + /* Legal/policy pages (privacy, rules, terms) — comfortable long-form reading. */ .legal { line-height: 2; } .legal h2 { font-size: 17px; margin: 22px 0 8px; color: var(--primary-dark); }