| Version 1 (modified by , 2 days ago) ( diff ) |
|---|
ОAuth автентикација
Корисникот кликнува на "Sign in with Google" или "Sign in with Facebook" копчето на Login страната, што активира OnPost() метод со provider параметар (Google/Facebook).
_signInManager.ConfigureExternalAuthenticationProperties() креира AuthenticationProperties објект со redirectUrl до "./ExternalLogin" Callback handler-от и враќа ChallengeResult(provider, properties).
OnGetCallbackAsync() го повикува _signInManager.GetExternalLoginInfoAsync() за да ги добие ClaimTypes.Email и ClaimTypes.Name од OAuth provider-от преку FindFirstValue().
- Ако корисникот веќе има поврзан external login, _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey) го најавува директно и прави LocalRedirect(returnUrl).
- Ако корисникот постои со истиот email но без поврзан login, _userManager.AddLoginAsync(existingUser, info) го поврзува OAuth налогот со постоечкиот User запис.
- Ако корисникот не постои, CreateUser() креира нов User instance, email.Split('@')[0] го генерира почетниот username, а while loop со _userManager.FindByNameAsync() проверува уникатност зголемувајќи counter додека не најде слободен username.
Нотификации
Системот креира нотификација преку CreateNotificationAsync(userId, type, message, recipeId).
_userManager.FindByIdAsync(userId) го наоѓа User објектот, а потоа проверува со type switch { "RecipeRated" => user.NotifyRecipeRated, "RecipeAccepted" => user.NotifyRecipeAccepted... } дали корисникот има овозможено нотификации од тој тип.
Ако shouldNotify е false, методот враќа false без да креира нотификација. Ако е true, се креира нов Notification објект со UserId, Type, Message, RecipeId, IsRead = false и CreatedAt = DateTime.UtcNow.
_context.Notifications.Add(notification) го додава записот, а await _context.SaveChangesAsync() го зачувува во базата.
/Notifications/Stream endpoint користи Response.Headers.Add("Content-Type", "text/event-stream") за Server-Sent Events (SSE) за real-time updates. while (HttpContext.RequestAborted.IsCancellationRequested) непрекинато врти, _context.Notifications.Count(n => n.UserId == userId && !n.IsRead) го пресметува unreadCount секои 3000ms преку await Task.Delay(3000).
Кога unreadCount != lastUnreadCount, .OrderByDescending(n => n.CreatedAt).FirstOrDefault() ја зема најновата нотификација, System.Text.Json.JsonSerializer.Serialize(payload) ја конвертира во JSON. await Response.WriteAsync($"data: {json}\n\n") го испраќа SSE форматираниот одговор, а await Response.Body.FlushAsync() осигурува дека податоците се веднаш испратени до клиентот.
GetNotifications() ги враќа последните нотификации со .Take() и unreadCount преку Json(new { notifications, unreadCount }) response.
MarkAsRead(notificationId) го сетира notification.IsRead = true и зачувува со _context.SaveChanges(), додека MarkAllAsRead() користи .Where(n => n.UserId == userId && !n.IsRead).ToList() и foreach loop за bulk update.
Delete(notificationId) користи _context.Notifications.Remove(notification) за бришење, а DeleteAll() користи .RemoveRange(userNotifications) за да ги избриши сите нотификации на корисникот.
NotificationPanel() враќа PartialView("_NotificationPanel", notifications) кој динамички генерира HTML со @foreach (var notification in Model) loop, каде GetTimeAgo(notification.CreatedAt) ја пресметува релативната временска разлика со timeSpan.TotalMinutes, timeSpan.TotalHours и timeSpan.TotalDays. JavaScript код повикува fetch('/Notifications/GetNotifications') за да ги вчита нотификациите, додека EventSource('/Notifications/Stream') креира SSE connection за real-time updates со onmessage event handler.
CRUD за Restaurant Meal
Администраторот кликнува на "Manage Restaurant Meals" копчето што го отвора modal-от со id="restaurantMealsModal", каде loadRestaurantMeals() прави GET барање до /Restaurants/Index.
restaurantSelect.addEventListener("change") го детектира избраниот ресторан, а loadRestaurantMeals(restaurantId) прави fetch(/Restaurants/GetRestaurantMeals/${restaurantId}) за да ги добие сите оброци.
showAddMealForm() го прикажува addMealFormSection div-от со style.display = "block", каде администраторот пополнува input полиња за mealItemName, mealItemDescription, calories, protein, carbs, fat . addRestaurantMeal() валидира дека document.querySelectorAll('input[name="mealType"]:checked').length > 0, креира RestaurantMeal објект со selectedTypes = Array.from(checkedBoxes).map(cb => cb.value). fetch('/Restaurants/AddRestaurantMeal', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(meal) }) го испраќа POST барањето.
Контролерот AddRestaurantMeal([FromBody] RestaurantMeal meal) валидира IsNullOrWhiteSpace(meal.ItemName), проверува meal.RestaurantId != null && meal.RestaurantId != 0, и користи await _context.Restaurants.FindAsync(meal.RestaurantId) за да го најде ресторанот. meal.RestaurantName = restaurant.Name го сетира името, _context.RestaurantMeals.Add(meal) го додава записот, а await _context.SaveChangesAsync() го зачувува.
CreateMealNotifications(meal, restaurant) креира нотификации за сите followers на ресторанот преку _context.RestaurantFollowings.Include(f => f.User).Where(f => f.RestaurantId == meal.RestaurantId && f.User.NotifyRestaurantNewMeal).
editRestaurantMeal() прикажува populated form со постоечки вредности, а EditRestaurantMeal([FromBody] RestaurantMeal meal) ги ажурира existing.ItemName, existing.Type, existing.Calories преку _context.RestaurantMeals.FindAsync(meal.Id).
deleteRestaurantMeal(mealId) прикажува confirmation dialog, а DeleteRestaurantMeal(int id) користи await _mealPlanService.HandleDeletedRestaurantMealAsync(id) за да ги отстрани reference-ите од meal plans пред да направи _context.RestaurantMeals.Remove(meal).
CRUD за Restaurant
Администраторот кликнува "Add New Restaurant" што го активира showAddRestaurantForm() кој сетира addRestaurantFormSection.style.display = "block" и restaurantFormTitle.textContent = "Add New Restaurant".
previewImage() користи FileReader() API со reader.readAsDataURL(file) за да креира preview на избраната слика, а reader.onload = (e) => { previewImg.src = e.target.result } ја прикажува сликата во imagePreview div-от.
addRestaurant() валидира дека restaurantName.value.trim() !== "" и restaurantImage.files.length > 0, креира FormData објект со formData.append('name', name), formData.append('description', description), formData.append('image', imageFile). fetch('/Restaurants/AddRestaurant', { method: 'POST', body: formData }) го испраќа multipart/form-data барањето.
AddRestaurant([FromForm] string name, [FromForm] string description, [FromForm] IFormFile image) ги bind-ува параметрите, Path.GetExtension(image.FileName).ToLowerInvariant() го проверува extension-от. await _context.Users.Where(u => u.NotifyNewRestaurant).ToListAsync() ги добива сите корисници со овозможени нотификации за нови ресторани. foreach (var user in users) loop креира Notification со Type = "NewRestaurant", Message = "New restaurant added: " + restaurant.Name, RecipeId = restaurant.Id.
EditRestaurant([FromForm] int id, [FromForm] IFormFile image) проверува дали image != null && image.Length > 0, и ако е така, System.IO.File.Exists(oldImagePath) проверува за стара слика, System.IO.File.Delete(oldImagePath) ја брише старата слика пред да ја зачува новата.
showEditRestaurantForm() ја популира формата со restaurant.Name, restaurant.Description и currentImage.src = restaurant.ImageUrl, а editRestaurantId.value = restaurant.Id го сетира hidden input-от за идентификација.
DeleteRestaurant(int id) користи .Include(r => r.RestaurantMeals).Include(r => r.Followers) за eager loading, _context.RestaurantFollowings.RemoveRange(restaurant.Followers) ги брише followers.
foreach (var meal in restaurant.RestaurantMeals) await _mealPlanService.HandleDeletedRestaurantMealAsync(meal.Id) ги отстранува meal references од сите meal plans.
_context.RestaurantMeals.RemoveRange(restaurant.RestaurantMeals) ги брише сите оброци, System.IO.File.Delete(imagePath) ја брише сликата од wwwroot/images, а _context.Restaurants.Remove(restaurant) го брише ресторанот.
CRUD за Meal Keyword
Администраторот го отвора "Manage Meal Tags" modal-от со id="mealTagsModal", каде loadMealKeywords() прави fetch('/Admin/GetMealKeywords') за да ги добие сите постоечки keywords.
response.json() го парсира одговорот, а keywords.forEach(keyword => { ... }) динамички креира div.keyword-card елементи со innerHTML што содржи keyword.Name, keyword.Tag badge и delete копче.
newKeywordName input field и newKeywordTag select dropdown овозможуваат внесување на нов keyword со тип (breakfast, main, snack).
addMealKeyword() креира објект { Name: name, Tag: tag }, и прави fetch('/Admin/AddMealKeyword', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(keyword) }).
Контролерот AddMealKeyword([FromBody] MealKeyword keyword) проверува дали keyword.Tag е валиден тип (breakfast, main, snack). _context.MealKeywords.Add(keyword) го додава записот, await _context.SaveChangesAsync() го зачувува, а return Json(new { success = true, keyword }) го враќа креираниот keyword со неговиот ID.
JavaScript then(response => response.json()) го добива одговорот, createKeywordCard(data.keyword) динамички креира нов card element, mealKeywordsList.insertBefore(card, mealKeywordsList.firstChild) го додава на врвот на листата. newKeywordName.value = "" и newKeywordTag.selectedIndex = 0 ги ресетираат input полињата.
deleteMealKeyword(keywordId) прикажува confirmation dialog со confirm('Are you sure you want to delete this keyword?'), а fetch(/Admin/DeleteMealKeyword/${keywordId}, { method: 'DELETE' }) го испраќа DELETE барањето.
DeleteMealKeyword(int id) користи await _context.MealKeywords.FindAsync(id) за да го најде keyword-от, _context.MealKeywords.Remove(keyword) го брише записот.
JavaScript then() handler користи document.querySelector([data-keyword-id="${keywordId}"]) за да го најде card-от, card.remove() го отстранува од DOM.
loadMealKeywords() се повикува при отварање на modal-от преку $('#mealTagsModal').on('show.bs.modal', loadMealKeywords) jQuery event listener.
