User panel: Phase 0 shared UI foundations
- Add design-token stylesheet (wwwroot/css/user-panel.css): CSS variables derived from the existing p8.css palette (brand maroon, success/danger colors, radius, shadow), plus reusable component classes (card, button variants, alert, badge, empty state, pagination, list item, toast, confirm modal). Includes a prefers-color-scheme dark block compatible with the existing toggleColorScheme() mechanism in bk.js. - Add wwwroot/js/user-panel.js: upConfirm() and upToast() helpers as styled, promise-based replacements for window.confirm()/alert(), falling back to the native dialogs if the modal/toast markup isn't present on a page. - Add Areas/User/Pages/Shared/ partials: _Pagination (wraps the List<NameIdUrlImage> pattern already used by every list page), _EmptyState, _Toasts and _ConfirmModal (hosts, included once by the layout). - Wire the new stylesheet/script and the toast/modal hosts into _UserPanelLayout.cshtml. - Refactor Notifications.cshtml as a first demonstration: replaces the hand-rolled pagination loop, manual gray/lightsteelblue row alternation, and confirm()/alert() calls with the new components. No backend/API changes. Rest of the User area is untouched pending Phase 1+ (nav/dashboard, forms, remaining list pages, moderation queues, editors).
This commit is contained in:
parent
f350bb9fc3
commit
f09b9de84a
@ -1,69 +1,65 @@
|
||||
@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";
|
||||
}
|
||||
<div class="up-page-header">
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
</div>
|
||||
@if (Model.LastError != "")
|
||||
{
|
||||
<h2>@Model.LastError</h2>
|
||||
<div class="up-alert up-alert--danger">@Model.LastError</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<script>
|
||||
function deleteNotification(notificationId) {
|
||||
if (!confirm('آیا از حذف این اعلان اطمینان دارید؟'))
|
||||
return;
|
||||
|
||||
var url = '?handler=Notification';
|
||||
async function deleteNotification(notificationId) {
|
||||
var ok = await upConfirm('آیا از حذف این اعلان اطمینان دارید؟');
|
||||
if (!ok) return;
|
||||
|
||||
$.ajax({
|
||||
type: "DELETE",
|
||||
url: url,
|
||||
data: {
|
||||
id: notificationId
|
||||
},
|
||||
url: '?handler=Notification',
|
||||
data: { id: notificationId },
|
||||
success: function () {
|
||||
var notificationBlockId = '#notification-' + notificationId;
|
||||
$(notificationBlockId).remove();
|
||||
$('#notification-' + notificationId).remove();
|
||||
upToast('اعلان حذف شد.', 'success');
|
||||
},
|
||||
error: function (e) {
|
||||
upToast(e.responseText || 'حذف اعلان با خطا مواجه شد.', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function markReadNotification(notificationId) {
|
||||
var url = '?handler=MarkRead';
|
||||
|
||||
$.ajax({
|
||||
type: "PUT",
|
||||
url: url,
|
||||
data: {
|
||||
id: notificationId
|
||||
},
|
||||
url: '?handler=MarkRead',
|
||||
data: { id: notificationId },
|
||||
success: function () {
|
||||
var notificationBlockId = '#unread-' + notificationId;
|
||||
$(notificationBlockId).remove();
|
||||
var item = document.getElementById('notification-' + notificationId);
|
||||
if (item) item.classList.remove('up-list-item--unread');
|
||||
var btn = document.getElementById('unread-' + notificationId);
|
||||
if (btn) btn.remove();
|
||||
},
|
||||
error: function (e) {
|
||||
upToast(e.responseText || 'ثبت خوانده شدن اعلان با خطا مواجه شد.', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function markAllRead() {
|
||||
if (!confirm('آیا از علامتگذاری همهٔ اعلانها به عنوان خوانده شده اطمینان دارید؟'))
|
||||
return;
|
||||
|
||||
var url = '?handler=MarkAllRead';
|
||||
async function markAllRead() {
|
||||
var ok = await upConfirm('آیا از علامتگذاری همهٔ اعلانها به عنوان خوانده شده اطمینان دارید؟', { danger: false, okText: 'بله، علامتگذاری کن' });
|
||||
if (!ok) return;
|
||||
|
||||
$.ajax({
|
||||
type: "PUT",
|
||||
url: url,
|
||||
url: '?handler=MarkAllRead',
|
||||
error: function (e) {
|
||||
if (e.responseText == null)
|
||||
alert(e);
|
||||
else
|
||||
alert(e.responseText);
|
||||
upToast(e.responseText || 'خطایی رخ داد.', 'error');
|
||||
},
|
||||
success: function () {
|
||||
location.reload();
|
||||
@ -71,20 +67,15 @@ else
|
||||
});
|
||||
}
|
||||
|
||||
function deleteAllRead() {
|
||||
if (!confirm('آیا از حذف همهٔ اعلانهای خوانده شده اطمینان دارید؟'))
|
||||
return;
|
||||
|
||||
var url = '?handler=AllRead';
|
||||
async function deleteAllRead() {
|
||||
var ok = await upConfirm('آیا از حذف همهٔ اعلانهای خوانده شده اطمینان دارید؟');
|
||||
if (!ok) return;
|
||||
|
||||
$.ajax({
|
||||
type: "DELETE",
|
||||
url: url,
|
||||
url: '?handler=AllRead',
|
||||
error: function (e) {
|
||||
if (e.responseText == null)
|
||||
alert(e);
|
||||
else
|
||||
alert(e.responseText);
|
||||
upToast(e.responseText || 'خطایی رخ داد.', 'error');
|
||||
},
|
||||
success: function () {
|
||||
location.reload();
|
||||
@ -93,78 +84,46 @@ else
|
||||
}
|
||||
</script>
|
||||
|
||||
<p style="text-align:center">
|
||||
@foreach (var paginationLink in Model.PaginationLinks)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(paginationLink.Url))
|
||||
{
|
||||
<a href="@paginationLink.Url">@paginationLink.Name</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
@paginationLink.Name
|
||||
}
|
||||
}
|
||||
</p>
|
||||
<partial name="_Pagination" model="Model.PaginationLinks" />
|
||||
|
||||
@if (Model.Notifications.Count == 0)
|
||||
{
|
||||
<p>
|
||||
در حال حاضر اعلانی وجود ندارد. در صورتی که کاربری با زدن دکمهٔ «پاسخگویی» برای حاشیهٔ شما پاسخی بنویسد یا شما با پیشنهاد آهنگ مرتبط با شعر یا روش دیگری در گنجور مشارکت کنید و مشارکت شما بررسی شود در قسمت «اعلانهای من» با دریافت یک اعلان از این رویدادها مطلع می شوید.
|
||||
</p>
|
||||
<partial name="_EmptyState" model="@("در حال حاضر اعلانی وجود ندارد. در صورتی که کاربری با زدن دکمهٔ «پاسخگویی» برای حاشیهٔ شما پاسخی بنویسد یا شما با پیشنهاد آهنگ مرتبط با شعر یا روش دیگری در گنجور مشارکت کنید و مشارکت شما بررسی شود در قسمت «اعلانهای من» با دریافت یک اعلان از این رویدادها مطلع میشوید.")" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>
|
||||
<a role="button" onclick="markAllRead()" class="pagebutton">همه را خواندم</a>
|
||||
</p>
|
||||
<p>
|
||||
<a role="button" onclick="deleteAllRead()" class="pagebutton">حذف همهٔ خوانده شدهها</a>
|
||||
</p>
|
||||
}
|
||||
<div class="up-btn-group up-mt-0" style="margin-bottom: var(--up-space-3);">
|
||||
<a role="button" onclick="markAllRead()" class="up-btn up-btn--ghost">همه را خواندم</a>
|
||||
<a role="button" onclick="deleteAllRead()" class="up-btn up-btn--ghost">حذف همهٔ خوانده شدهها</a>
|
||||
</div>
|
||||
|
||||
<div class="up-list">
|
||||
@foreach (var notification in Model.Notifications)
|
||||
{
|
||||
if (color == "gray")
|
||||
color = "lightsteelblue";
|
||||
else
|
||||
color = "gray";
|
||||
|
||||
<div style="background-color:@color" id="notification-@notification.Id">
|
||||
<blockquote>
|
||||
<p>
|
||||
@notification.Subject <small>در @notification.DateTime.ToFriendlyPersianDateTextify()</small>
|
||||
</p>
|
||||
bool unread = notification.Status == RSecurityBackend.Models.Notification.NotificationStatus.Unread;
|
||||
<div class="up-list-item @(unread ? "up-list-item--unread" : "")" id="notification-@notification.Id">
|
||||
<div>
|
||||
<strong>@notification.Subject</strong>
|
||||
<span class="up-list-item__meta">@notification.DateTime.ToFriendlyPersianDateTextify()</span>
|
||||
</div>
|
||||
<div id="notification-text-@notification.Id">
|
||||
@Html.Raw(notification.HtmlText)
|
||||
</div>
|
||||
</blockquote>
|
||||
<p>
|
||||
<a role="button" onclick="deleteNotification('@notification.Id')" class="actionlink" title="حذف"><i class="noindent-info-button delete-icon"></i></a>
|
||||
@if (notification.Status == RSecurityBackend.Models.Notification.NotificationStatus.Unread)
|
||||
<div class="up-list-item__actions">
|
||||
<a role="button" onclick="deleteNotification('@notification.Id')" class="up-icon-btn" title="حذف" aria-label="حذف این اعلان">
|
||||
<i class="noindent-info-button delete-icon"></i>
|
||||
</a>
|
||||
@if (unread)
|
||||
{
|
||||
<a role="button" onclick="markReadNotification('@notification.Id')" class="actionlink" title="خواندم" id="unread-@notification.Id"><i class="noindent-info-button done-icon"></i></a>
|
||||
<a role="button" onclick="markReadNotification('@notification.Id')" class="up-icon-btn" title="خواندم" aria-label="علامتگذاری به عنوان خوانده شده" id="unread-@notification.Id">
|
||||
<i class="noindent-info-button done-icon"></i>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<hr />
|
||||
}
|
||||
|
||||
<p style="text-align:center">
|
||||
@foreach (var paginationLink in Model.PaginationLinks)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(paginationLink.Url))
|
||||
{
|
||||
<a href="@paginationLink.Url">@paginationLink.Name</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
@paginationLink.Name
|
||||
<partial name="_Pagination" model="Model.PaginationLinks" />
|
||||
}
|
||||
}
|
||||
</p>
|
||||
|
||||
|
||||
}
|
||||
|
||||
11
GanjooRazor/Areas/User/Pages/Shared/_ConfirmModal.cshtml
Normal file
11
GanjooRazor/Areas/User/Pages/Shared/_ConfirmModal.cshtml
Normal file
@ -0,0 +1,11 @@
|
||||
@* Styled replacement for window.confirm(). Included once by
|
||||
_UserPanelLayout.cshtml. Trigger with JS: await upConfirm('پیام شما') *@
|
||||
<div class="up-modal-backdrop" id="upConfirmBackdrop" hidden>
|
||||
<div class="up-modal" role="alertdialog" aria-modal="true" aria-labelledby="upConfirmTitle">
|
||||
<p id="upConfirmTitle" class="up-modal__message"></p>
|
||||
<div class="up-modal__actions">
|
||||
<button type="button" class="up-btn up-btn--ghost" id="upConfirmCancel">انصراف</button>
|
||||
<button type="button" class="up-btn up-btn--danger" id="upConfirmOk">تأیید</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
12
GanjooRazor/Areas/User/Pages/Shared/_EmptyState.cshtml
Normal file
12
GanjooRazor/Areas/User/Pages/Shared/_EmptyState.cshtml
Normal file
@ -0,0 +1,12 @@
|
||||
@*
|
||||
A consistent "nothing here yet" state for list pages, instead of a bare
|
||||
<p>. Pass the explanatory text as the model:
|
||||
|
||||
<partial name="_EmptyState" model="@("در حال حاضر اعلانی وجود ندارد.")" />
|
||||
*@
|
||||
@model string
|
||||
|
||||
<div class="up-empty">
|
||||
<div class="up-empty__icon" aria-hidden="true">●</div>
|
||||
<p>@Model</p>
|
||||
</div>
|
||||
29
GanjooRazor/Areas/User/Pages/Shared/_Pagination.cshtml
Normal file
29
GanjooRazor/Areas/User/Pages/Shared/_Pagination.cshtml
Normal file
@ -0,0 +1,29 @@
|
||||
@*
|
||||
Renders the pagination links used throughout the User area.
|
||||
Every list page already builds a List<NameIdUrlImage> called
|
||||
PaginationLinks (Name = label, Url = link, or Url == "" for the
|
||||
current/disabled page) — pass that model straight in:
|
||||
|
||||
<partial name="_Pagination" model="Model.PaginationLinks" />
|
||||
|
||||
Replaces the hand-written @foreach loop that was copy-pasted at the
|
||||
top and bottom of every list page.
|
||||
*@
|
||||
@model System.Collections.Generic.List<GSpotifyProxy.Models.NameIdUrlImage>
|
||||
|
||||
@if (Model != null && Model.Count > 0)
|
||||
{
|
||||
<nav class="up-pagination" aria-label="صفحهبندی">
|
||||
@foreach (var link in Model)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(link.Url))
|
||||
{
|
||||
<a href="@link.Url">@link.Name</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="up-pagination__current" aria-current="page">@link.Name</span>
|
||||
}
|
||||
}
|
||||
</nav>
|
||||
}
|
||||
3
GanjooRazor/Areas/User/Pages/Shared/_Toasts.cshtml
Normal file
3
GanjooRazor/Areas/User/Pages/Shared/_Toasts.cshtml
Normal file
@ -0,0 +1,3 @@
|
||||
@* Toast notification host. Included once by _UserPanelLayout.cshtml.
|
||||
Populate with JS: upToast('پیام شما', 'success' | 'error' | 'info') *@
|
||||
<div class="up-toast-container" id="upToastContainer" aria-live="polite" aria-atomic="true"></div>
|
||||
@ -10,6 +10,7 @@
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="~/css/site.css" />
|
||||
<link href="/css/p8.css?version=86" rel="stylesheet" type="text/css" />
|
||||
<link href="/css/user-panel.css?version=1" rel="stylesheet" type="text/css" />
|
||||
<link rel="shortcut icon" href="/favicon.ico?version=1" type="image/x-icon" />
|
||||
<link rel="search" type="application/opensearchdescription+xml" title="گنجور" href="https://ganjoor.net/search.xml" />
|
||||
<script src="~/lib/jquery/dist/jquery.min.js"></script>
|
||||
@ -180,16 +181,18 @@
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<div class="container">
|
||||
<div class="container up-shell">
|
||||
<main role="main" class="pb-3">
|
||||
@RenderBody()
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
<partial name="_Toasts" />
|
||||
<partial name="_ConfirmModal" />
|
||||
|
||||
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="~/js/site.js" asp-append-version="true"></script>
|
||||
<script src="/js/user-panel.js?version=1"></script>
|
||||
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
<div id="footer"></div>
|
||||
|
||||
451
GanjooRazor/wwwroot/css/user-panel.css
Normal file
451
GanjooRazor/wwwroot/css/user-panel.css
Normal file
@ -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; }
|
||||
90
GanjooRazor/wwwroot/js/user-panel.js
Normal file
90
GanjooRazor/wwwroot/js/user-panel.js
Normal file
@ -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<boolean>}
|
||||
*/
|
||||
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);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user