diff --git a/GanjooRazor/Areas/User/Pages/Notifications.cshtml b/GanjooRazor/Areas/User/Pages/Notifications.cshtml
index 4f10b9ef..ca6379c5 100644
--- a/GanjooRazor/Areas/User/Pages/Notifications.cshtml
+++ b/GanjooRazor/Areas/User/Pages/Notifications.cshtml
@@ -1,170 +1,129 @@
-@page
+@page
@model GanjooRazor.Areas.User.Pages.NotificationsModel
@using DNTPersianUtils.Core
@{
Layout = "_UserPanelLayout";
ViewData["Title"] = "اعلانهای من";
await GanjooRazor.Utils.GanjoorSessionChecker.ApplyPermissionsToViewData(Request, Response, ViewData);
- string color = "gray";
}
-
@ViewData["Title"]
+
@if (Model.LastError != "")
{
- @Model.LastError
+ @Model.LastError
}
else
{
-
- @foreach (var paginationLink in Model.PaginationLinks)
- {
- if (!string.IsNullOrEmpty(paginationLink.Url))
- {
- @paginationLink.Name
- }
- else
- {
- @paginationLink.Name
- }
- }
-
+
@if (Model.Notifications.Count == 0)
{
-
- در حال حاضر اعلانی وجود ندارد. در صورتی که کاربری با زدن دکمهٔ «پاسخگویی» برای حاشیهٔ شما پاسخی بنویسد یا شما با پیشنهاد آهنگ مرتبط با شعر یا روش دیگری در گنجور مشارکت کنید و مشارکت شما بررسی شود در قسمت «اعلانهای من» با دریافت یک اعلان از این رویدادها مطلع می شوید.
-
+
}
else
{
-
- همه را خواندم
-
-
- حذف همهٔ خوانده شدهها
-
- }
-
- @foreach (var notification in Model.Notifications)
- {
- if (color == "gray")
- color = "lightsteelblue";
- else
- color = "gray";
-
-
-
-
- @notification.Subject در @notification.DateTime.ToFriendlyPersianDateTextify()
-
-
- @Html.Raw(notification.HtmlText)
-
-
-
-
- @if (notification.Status == RSecurityBackend.Models.Notification.NotificationStatus.Unread)
- {
-
- }
-
+
+
+ @foreach (var notification in Model.Notifications)
+ {
+ bool unread = notification.Status == RSecurityBackend.Models.Notification.NotificationStatus.Unread;
+
+
+ @notification.Subject
+ @notification.DateTime.ToFriendlyPersianDateTextify()
+
+
+ @Html.Raw(notification.HtmlText)
+
+
+
+
+
+ @if (unread)
+ {
+
+
+
+ }
+
+
+ }
+
-
+
}
-
-
- @foreach (var paginationLink in Model.PaginationLinks)
- {
- if (!string.IsNullOrEmpty(paginationLink.Url))
- {
- @paginationLink.Name
- }
- else
- {
- @paginationLink.Name
- }
- }
-
-
-
}
diff --git a/GanjooRazor/Areas/User/Pages/Shared/_ConfirmModal.cshtml b/GanjooRazor/Areas/User/Pages/Shared/_ConfirmModal.cshtml
new file mode 100644
index 00000000..f34f9d49
--- /dev/null
+++ b/GanjooRazor/Areas/User/Pages/Shared/_ConfirmModal.cshtml
@@ -0,0 +1,11 @@
+@* Styled replacement for window.confirm(). Included once by
+ _UserPanelLayout.cshtml. Trigger with JS: await upConfirm('پیام شما') *@
+
diff --git a/GanjooRazor/Areas/User/Pages/Shared/_EmptyState.cshtml b/GanjooRazor/Areas/User/Pages/Shared/_EmptyState.cshtml
new file mode 100644
index 00000000..9b30576c
--- /dev/null
+++ b/GanjooRazor/Areas/User/Pages/Shared/_EmptyState.cshtml
@@ -0,0 +1,12 @@
+@*
+ A consistent "nothing here yet" state for list pages, instead of a bare
+
. Pass the explanatory text as the model:
+
+
+*@
+@model string
+
+
diff --git a/GanjooRazor/Areas/User/Pages/Shared/_Pagination.cshtml b/GanjooRazor/Areas/User/Pages/Shared/_Pagination.cshtml
new file mode 100644
index 00000000..1c79363a
--- /dev/null
+++ b/GanjooRazor/Areas/User/Pages/Shared/_Pagination.cshtml
@@ -0,0 +1,29 @@
+@*
+ Renders the pagination links used throughout the User area.
+ Every list page already builds a List
called
+ PaginationLinks (Name = label, Url = link, or Url == "" for the
+ current/disabled page) — pass that model straight in:
+
+
+
+ Replaces the hand-written @foreach loop that was copy-pasted at the
+ top and bottom of every list page.
+*@
+@model System.Collections.Generic.List
+
+@if (Model != null && Model.Count > 0)
+{
+
+}
diff --git a/GanjooRazor/Areas/User/Pages/Shared/_Toasts.cshtml b/GanjooRazor/Areas/User/Pages/Shared/_Toasts.cshtml
new file mode 100644
index 00000000..6fdbd919
--- /dev/null
+++ b/GanjooRazor/Areas/User/Pages/Shared/_Toasts.cshtml
@@ -0,0 +1,3 @@
+@* Toast notification host. Included once by _UserPanelLayout.cshtml.
+ Populate with JS: upToast('پیام شما', 'success' | 'error' | 'info') *@
+
diff --git a/GanjooRazor/Pages/Shared/_UserPanelLayout.cshtml b/GanjooRazor/Pages/Shared/_UserPanelLayout.cshtml
index fb8efd4f..f04f497b 100644
--- a/GanjooRazor/Pages/Shared/_UserPanelLayout.cshtml
+++ b/GanjooRazor/Pages/Shared/_UserPanelLayout.cshtml
@@ -10,6 +10,7 @@
+
@@ -180,16 +181,18 @@
-
+
@RenderBody()
-
+
+
+
@await RenderSectionAsync("Scripts", required: false)
diff --git a/GanjooRazor/wwwroot/css/user-panel.css b/GanjooRazor/wwwroot/css/user-panel.css
new file mode 100644
index 00000000..c68a120e
--- /dev/null
+++ b/GanjooRazor/wwwroot/css/user-panel.css
@@ -0,0 +1,451 @@
+/*
+ User area (پیشخان کاربر) design foundation.
+ Scoped, additive layer on top of p8.css/site.css — nothing here overrides
+ the public site's look. Tokens are pulled from p8.css's existing palette
+ (brand maroon #300, success/danger greens & reds, 5px radius, existing
+ box-shadow recipe) so the panel still feels like Ganjoor, just tidier.
+
+ Class prefix "up-" (user-panel) is used throughout to avoid collisions
+ with the large, unscoped p8.css class list.
+*/
+
+:root {
+ --up-primary: #300;
+ --up-primary-hover: #4d1010;
+ --up-on-primary: #fff;
+
+ --up-success: #0a0;
+ --up-success-dark: darkgreen;
+ --up-danger: #f00;
+ --up-danger-dark: darkred;
+ --up-gray: slategray;
+
+ --up-bg: #f5f7fb;
+ --up-surface: #fff;
+ --up-surface-alt: #f4f1ec;
+ --up-border: #e0e0e0;
+ --up-text: #222;
+ --up-text-muted: #666;
+
+ --up-radius-sm: 5px;
+ --up-radius: 10px;
+ --up-radius-lg: 16px;
+ --up-shadow: 0 5px 0.3rem rgba(0, 0, 0, 0.3);
+ --up-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.12);
+
+ --up-space-1: 4px;
+ --up-space-2: 8px;
+ --up-space-3: 16px;
+ --up-space-4: 24px;
+ --up-space-5: 32px;
+
+ --up-font: 'Vazirmatn', sans-serif;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --up-bg: #121212;
+ --up-surface: #1c1c1c;
+ --up-surface-alt: #232323;
+ --up-border: #3a3a3a;
+ --up-text: bisque;
+ --up-text-muted: #c9b8a8;
+ --up-primary: #a54a4a;
+ --up-primary-hover: #c25e5e;
+ --up-on-primary: #fff;
+ }
+}
+
+/* ---------- layout shell ---------- */
+
+.up-shell {
+ font-family: var(--up-font);
+ color: var(--up-text);
+ text-align: right;
+}
+
+.up-page-header {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: var(--up-space-2);
+ margin-bottom: var(--up-space-3);
+ flex-wrap: wrap;
+}
+
+.up-page-header h1 {
+ margin: 0;
+ font-size: 1.5rem;
+}
+
+.up-page-subtitle {
+ color: var(--up-text-muted);
+ font-size: 0.9rem;
+ margin-top: var(--up-space-1);
+}
+
+/* ---------- card ---------- */
+
+.up-card {
+ background: var(--up-surface);
+ border: 1px solid var(--up-border);
+ border-radius: var(--up-radius-lg);
+ box-shadow: var(--up-shadow-sm);
+ padding: var(--up-space-4);
+ margin-bottom: var(--up-space-3);
+}
+
+.up-card + .up-card {
+ margin-top: var(--up-space-3);
+}
+
+.up-card__title {
+ margin: 0 0 var(--up-space-3);
+ font-size: 1.1rem;
+ border-bottom: 1px solid var(--up-border);
+ padding-bottom: var(--up-space-2);
+}
+
+/* ---------- buttons ---------- */
+/* Compatible in spirit with the existing .pagebutton/.editor-button look
+ (radius, shadow) so mixed usage during the migration doesn't feel jarring. */
+
+.up-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--up-space-1);
+ cursor: pointer;
+ font-family: var(--up-font);
+ font-size: 0.9rem;
+ padding: 8px 16px;
+ border-radius: var(--up-radius-sm);
+ border: 1px solid transparent;
+ box-shadow: var(--up-shadow);
+ color: var(--up-on-primary);
+ background-color: var(--up-primary);
+ text-decoration: none;
+ transition: background-color 0.15s ease, opacity 0.15s ease, transform 0.05s ease;
+}
+
+.up-btn:hover {
+ background-color: var(--up-primary-hover);
+ color: var(--up-on-primary);
+ text-decoration: none;
+}
+
+.up-btn:active {
+ transform: translateY(1px);
+}
+
+.up-btn:disabled,
+.up-btn[aria-disabled="true"] {
+ opacity: 0.55;
+ cursor: not-allowed;
+ transform: none;
+}
+
+.up-btn--success {
+ background-color: var(--up-success-dark);
+}
+
+.up-btn--success:hover {
+ background-color: var(--up-success);
+}
+
+.up-btn--danger {
+ background-color: var(--up-danger-dark);
+}
+
+.up-btn--danger:hover {
+ background-color: var(--up-danger);
+}
+
+.up-btn--ghost {
+ background-color: transparent;
+ color: var(--up-text);
+ border-color: var(--up-border);
+ box-shadow: none;
+}
+
+.up-btn--ghost:hover {
+ background-color: var(--up-surface-alt);
+ color: var(--up-text);
+}
+
+.up-btn--sm {
+ padding: 4px 10px;
+ font-size: 0.8rem;
+}
+
+.up-btn--block {
+ display: flex;
+ width: 100%;
+}
+
+.up-btn-group {
+ display: flex;
+ gap: var(--up-space-2);
+ flex-wrap: wrap;
+}
+
+/* icon-only action buttons (delete/mark-read/etc.) */
+.up-icon-btn {
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ border: 1px solid var(--up-border);
+ background: var(--up-surface);
+ color: var(--up-text);
+}
+
+.up-icon-btn:hover {
+ background: var(--up-surface-alt);
+}
+
+/* ---------- alerts / notices ---------- */
+
+.up-alert {
+ border-radius: var(--up-radius);
+ padding: var(--up-space-3);
+ margin-bottom: var(--up-space-3);
+ border: 1px solid transparent;
+ font-size: 0.9rem;
+}
+
+.up-alert--info {
+ background: #eef3fb;
+ border-color: #cfe0f5;
+ color: #1c3a5e;
+}
+
+.up-alert--success {
+ background: #eaf7ea;
+ border-color: #c9e8c9;
+ color: #1e4620;
+}
+
+.up-alert--danger {
+ background: #fbeaea;
+ border-color: #f0caca;
+ color: #5e1c1c;
+}
+
+.up-alert--warning {
+ background: #fff6e5;
+ border-color: #f3ddaa;
+ color: #5e4a1c;
+}
+
+@media (prefers-color-scheme: dark) {
+ .up-alert--info { background: #172633; border-color: #24405c; color: bisque; }
+ .up-alert--success { background: #142a16; border-color: #1f4a22; color: bisque; }
+ .up-alert--danger { background: #331616; border-color: #5c2424; color: bisque; }
+ .up-alert--warning { background: #332a14; border-color: #5c4a1f; color: bisque; }
+}
+
+/* ---------- badges ---------- */
+
+.up-badge {
+ display: inline-block;
+ padding: 2px 10px;
+ border-radius: 999px;
+ font-size: 0.75rem;
+ line-height: 1.6;
+ background: var(--up-surface-alt);
+ color: var(--up-text-muted);
+ border: 1px solid var(--up-border);
+}
+
+.up-badge--primary { background: var(--up-primary); color: var(--up-on-primary); border-color: transparent; }
+.up-badge--success { background: var(--up-success-dark); color: #fff; border-color: transparent; }
+.up-badge--danger { background: var(--up-danger-dark); color: #fff; border-color: transparent; }
+
+/* ---------- empty state ---------- */
+
+.up-empty {
+ text-align: center;
+ padding: var(--up-space-5) var(--up-space-3);
+ color: var(--up-text-muted);
+}
+
+.up-empty__icon {
+ font-size: 2rem;
+ margin-bottom: var(--up-space-2);
+ opacity: 0.6;
+}
+
+/* ---------- pagination ---------- */
+
+.up-pagination {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: var(--up-space-1);
+ margin: var(--up-space-3) 0;
+}
+
+.up-pagination a,
+.up-pagination span {
+ display: inline-flex;
+ min-width: 32px;
+ height: 32px;
+ padding: 0 8px;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--up-radius-sm);
+ font-size: 0.85rem;
+ text-decoration: none;
+ color: var(--up-text);
+}
+
+.up-pagination a {
+ border: 1px solid var(--up-border);
+ background: var(--up-surface);
+}
+
+.up-pagination a:hover {
+ background: var(--up-surface-alt);
+ text-decoration: none;
+}
+
+.up-pagination span.up-pagination__current {
+ background: var(--up-primary);
+ color: var(--up-on-primary);
+ font-weight: 600;
+}
+
+/* ---------- list items (notifications, comments, edits...) ---------- */
+
+.up-list {
+ display: flex;
+ flex-direction: column;
+ gap: var(--up-space-2);
+}
+
+.up-list-item {
+ background: var(--up-surface);
+ border: 1px solid var(--up-border);
+ border-radius: var(--up-radius);
+ padding: var(--up-space-3);
+ position: relative;
+}
+
+.up-list-item--unread {
+ border-inline-start: 4px solid var(--up-primary);
+}
+
+.up-list-item__meta {
+ color: var(--up-text-muted);
+ font-size: 0.8rem;
+ margin-inline-start: var(--up-space-2);
+}
+
+.up-list-item__actions {
+ display: flex;
+ gap: var(--up-space-2);
+ margin-top: var(--up-space-2);
+}
+
+/* ---------- toasts ---------- */
+
+.up-toast-container {
+ position: fixed;
+ bottom: var(--up-space-4);
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 1080;
+ display: flex;
+ flex-direction: column;
+ gap: var(--up-space-2);
+ align-items: center;
+ pointer-events: none;
+ width: 100%;
+ padding: 0 var(--up-space-3);
+}
+
+.up-toast {
+ pointer-events: auto;
+ width: max-content;
+ max-width: min(420px, 90vw);
+ padding: 10px 20px;
+ border-radius: var(--up-radius);
+ box-shadow: var(--up-shadow-sm);
+ color: #fff;
+ background: var(--up-gray);
+ font-size: 0.85rem;
+ opacity: 0;
+ transform: translateY(8px);
+ transition: opacity 0.2s ease, transform 0.2s ease;
+}
+
+.up-toast--visible {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+.up-toast--success {
+ background: var(--up-success-dark);
+}
+
+.up-toast--error {
+ background: var(--up-danger-dark);
+}
+
+/* ---------- confirm modal (replaces window.confirm) ---------- */
+
+.up-modal-backdrop {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1090;
+ padding: var(--up-space-3);
+}
+
+.up-modal-backdrop[hidden] {
+ display: none;
+}
+
+.up-modal {
+ background: var(--up-surface);
+ border-radius: var(--up-radius-lg);
+ box-shadow: var(--up-shadow-sm);
+ padding: var(--up-space-4);
+ max-width: 400px;
+ width: 100%;
+ text-align: center;
+}
+
+.up-modal__message {
+ margin: 0 0 var(--up-space-4);
+ font-size: 0.95rem;
+ line-height: 1.6;
+}
+
+.up-modal__actions {
+ display: flex;
+ gap: var(--up-space-2);
+ justify-content: center;
+}
+
+.up-modal__actions .up-btn {
+ flex: 1;
+}
+
+body.up-modal-open {
+ overflow: hidden;
+}
+
+/* ---------- small utilities ---------- */
+
+.up-mt-0 { margin-top: 0; }
+.up-text-muted { color: var(--up-text-muted); }
+.up-text-center { text-align: center; }
diff --git a/GanjooRazor/wwwroot/js/user-panel.js b/GanjooRazor/wwwroot/js/user-panel.js
new file mode 100644
index 00000000..65e2e651
--- /dev/null
+++ b/GanjooRazor/wwwroot/js/user-panel.js
@@ -0,0 +1,90 @@
+// User area (پیشخان کاربر) shared UI helpers.
+// Provides upConfirm() and upToast() as drop-in, styled replacements for
+// window.confirm()/window.alert(), used across the panel's AJAX actions.
+// Requires the markup from Areas/User/Pages/Shared/_ConfirmModal.cshtml and
+// _Toasts.cshtml to be present on the page (included once by _UserPanelLayout).
+
+/**
+ * Shows a styled confirmation dialog and resolves to true/false.
+ * Falls back to window.confirm() if the modal markup isn't on the page.
+ * @param {string} message
+ * @param {{okText?: string, cancelText?: string, danger?: boolean}} [options]
+ * @returns {Promise
}
+ */
+function upConfirm(message, options) {
+ options = options || {};
+ return new Promise(function (resolve) {
+ var backdrop = document.getElementById('upConfirmBackdrop');
+ var titleEl = document.getElementById('upConfirmTitle');
+ var okBtn = document.getElementById('upConfirmOk');
+ var cancelBtn = document.getElementById('upConfirmCancel');
+
+ if (!backdrop || !titleEl || !okBtn || !cancelBtn) {
+ resolve(window.confirm(message));
+ return;
+ }
+
+ titleEl.textContent = message;
+ okBtn.textContent = options.okText || 'تأیید';
+ cancelBtn.textContent = options.cancelText || 'انصراف';
+ okBtn.className = 'up-btn ' + (options.danger === false ? 'up-btn--success' : 'up-btn--danger');
+
+ backdrop.hidden = false;
+ document.body.classList.add('up-modal-open');
+
+ function cleanup(result) {
+ backdrop.hidden = true;
+ document.body.classList.remove('up-modal-open');
+ okBtn.removeEventListener('click', onOk);
+ cancelBtn.removeEventListener('click', onCancel);
+ backdrop.removeEventListener('click', onBackdropClick);
+ document.removeEventListener('keydown', onKeyDown);
+ resolve(result);
+ }
+
+ function onOk() { cleanup(true); }
+ function onCancel() { cleanup(false); }
+ function onBackdropClick(e) { if (e.target === backdrop) cleanup(false); }
+ function onKeyDown(e) { if (e.key === 'Escape') cleanup(false); }
+
+ okBtn.addEventListener('click', onOk);
+ cancelBtn.addEventListener('click', onCancel);
+ backdrop.addEventListener('click', onBackdropClick);
+ document.addEventListener('keydown', onKeyDown);
+
+ okBtn.focus();
+ });
+}
+
+/**
+ * Shows a transient toast notification.
+ * Falls back to window.alert() if the toast container isn't on the page.
+ * @param {string} message
+ * @param {'success'|'error'|'info'} [type]
+ */
+function upToast(message, type) {
+ type = type || 'success';
+ var container = document.getElementById('upToastContainer');
+
+ if (!container) {
+ alert(message);
+ return;
+ }
+
+ var toast = document.createElement('div');
+ toast.className = 'up-toast up-toast--' + type;
+ toast.setAttribute('role', type === 'error' ? 'alert' : 'status');
+ toast.textContent = message;
+ container.appendChild(toast);
+
+ requestAnimationFrame(function () {
+ toast.classList.add('up-toast--visible');
+ });
+
+ setTimeout(function () {
+ toast.classList.remove('up-toast--visible');
+ setTimeout(function () {
+ toast.remove();
+ }, 250);
+ }, 4000);
+}