[Profile] Editable profile (avatar + resume) + role-based profile dropdown menu
Every user gets a full editable profile at /Me/Profile: name, role, city, specialty/title, license, years, bio + avatar image upload + resume upload (PDF/image). Avatar/resume stored in-DB on User (migration, 5 nullable columns). Endpoints: /avatar/{id} (public) and /resume/{id} (owner, admin, or an employer who received that user's application). Nav: replaced the scattered action links with an avatar button + dropdown listing all of the user's pages by role (profile, کارجو panel, alerts, preferences, notifications; employer panel; admin panel + settings; logout) — shows the avatar image or initials; collapses into the burger menu on mobile; closes on outside-click.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,22 @@
|
||||
@using System.Security.Claims
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@inject JobsMedical.Web.Services.NotificationService Notifications
|
||||
@inject JobsMedical.Web.Data.AppDbContext Db
|
||||
@{
|
||||
var title = ViewData["Title"] as string;
|
||||
int unreadCount = 0;
|
||||
if (User.Identity?.IsAuthenticated == true && int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out var _uid))
|
||||
int meId = 0;
|
||||
string? meName = null;
|
||||
bool meHasAvatar = false;
|
||||
if (User.Identity?.IsAuthenticated == true && int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out meId))
|
||||
{
|
||||
unreadCount = await Notifications.UnreadCountAsync(_uid);
|
||||
unreadCount = await Notifications.UnreadCountAsync(meId);
|
||||
var info = await Db.Users.Where(u => u.Id == meId)
|
||||
.Select(u => new { u.FullName, u.Phone, HasAvatar = u.Avatar != null }).FirstOrDefaultAsync();
|
||||
meName = string.IsNullOrWhiteSpace(info?.FullName) ? info?.Phone : info!.FullName;
|
||||
meHasAvatar = info?.HasAvatar ?? false;
|
||||
}
|
||||
var meInitial = string.IsNullOrWhiteSpace(meName) ? "؟" : meName!.Trim().Substring(0, 1);
|
||||
}
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
@@ -62,20 +72,44 @@
|
||||
<div class="header-actions">
|
||||
@if (User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
@if (User.IsInRole("Admin"))
|
||||
{
|
||||
<a class="nav-action" asp-page="/Admin/Overview">پنل مدیریت</a>
|
||||
<a class="nav-action" asp-page="/Admin/Settings">تنظیمات</a>
|
||||
}
|
||||
@if (User.IsInRole("FacilityAdmin"))
|
||||
{
|
||||
<a class="nav-action" asp-page="/Employer/Index">پنل کارفرما</a>
|
||||
}
|
||||
<a class="nav-action bell-inline js-bell" asp-page="/Me/Notifications" title="اعلانها" data-tour="bell"><span class="bell-ico">🔔</span><span class="bell-label">اعلانها</span>@if (unreadCount > 0) {<span class="bell-badge">@JalaliDate.ToPersianDigits(unreadCount > 99 ? "99+" : unreadCount.ToString())</span>}</a>
|
||||
<a class="nav-action" asp-page="/Me/Index" data-tour="panel">پنل کارجو</a>
|
||||
<form method="post" asp-page="/Account/Logout" style="display:contents;">
|
||||
<button type="submit" class="btn btn-outline btn-sm">خروج</button>
|
||||
</form>
|
||||
|
||||
<div class="profile-menu">
|
||||
<input type="checkbox" id="profile-toggle" class="profile-toggle" hidden />
|
||||
<label for="profile-toggle" class="avatar-btn" data-tour="profile" aria-label="منوی کاربر">
|
||||
@if (meHasAvatar)
|
||||
{
|
||||
<img class="avatar-img" src="/avatar/@meId" alt="پروفایل" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="avatar-fallback">@meInitial</span>
|
||||
}
|
||||
<span class="avatar-caret">▾</span>
|
||||
</label>
|
||||
<nav class="profile-dropdown">
|
||||
<div class="pd-head">@meName</div>
|
||||
<a asp-page="/Me/Profile">👤 ویرایش پروفایل</a>
|
||||
<a asp-page="/Me/Index" data-tour="panel">🗂️ پنل کارجو</a>
|
||||
<a asp-page="/Me/Alerts">🔎 هشدارهای شغلی</a>
|
||||
<a asp-page="/Preferences/Index">⭐ علاقهمندیها</a>
|
||||
<a asp-page="/Me/Notifications">🔔 اعلانها@if (unreadCount > 0) {<span class="bell-badge" style="position:static; margin-inline-start:6px;">@JalaliDate.ToPersianDigits(unreadCount > 99 ? "99+" : unreadCount.ToString())</span>}</a>
|
||||
@if (User.IsInRole("FacilityAdmin"))
|
||||
{
|
||||
<a asp-page="/Employer/Index">🏥 پنل کارفرما</a>
|
||||
}
|
||||
@if (User.IsInRole("Admin"))
|
||||
{
|
||||
<div class="pd-sep"></div>
|
||||
<a asp-page="/Admin/Overview">🛠️ پنل مدیریت</a>
|
||||
<a asp-page="/Admin/Settings">⚙️ تنظیمات</a>
|
||||
}
|
||||
<div class="pd-sep"></div>
|
||||
<form method="post" asp-page="/Account/Logout">
|
||||
<button type="submit" class="pd-logout">🚪 خروج</button>
|
||||
</form>
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -122,6 +156,14 @@
|
||||
@* Self-hosted guided app tour (no CDN). Auto-runs once for new visitors; re-runnable from /Help. *@
|
||||
<script src="~/js/tour.js" asp-append-version="true" defer></script>
|
||||
|
||||
@* Close the profile dropdown when clicking outside it. *@
|
||||
<script>
|
||||
document.addEventListener('click', function (e) {
|
||||
var t = document.getElementById('profile-toggle');
|
||||
if (t && t.checked && !e.target.closest('.profile-menu')) t.checked = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
@* Live in-app notifications over SSE (our own origin — works in Iran, no Google push).
|
||||
Updates the bell badge, shows a toast, and fires a local OS notification when allowed. *@
|
||||
@if (User.Identity?.IsAuthenticated == true)
|
||||
|
||||
Reference in New Issue
Block a user