Refactor: Streamline product management UI and enhance PWA behavior
**Product List Improvements:** - Move Import/Export to settings menu for cleaner interface - Replace Edit/Variants/Multi-Buys buttons with single Details action - Remove Blazor UI button from product list - Simplify product row actions for better mobile UX **Product Details Enhancements:** - Add Danger Zone section with Delete button at bottom - Improve visual hierarchy and action placement **Navigation Updates:** - Remove hamburger menu toggle (desktop nav always visible) - Rename Settings to Menu in mobile bottom nav - Update settings drawer header and icon **Code Cleanup:** - Remove unused Blazor, Variations, and Variants endpoints (243 lines) - Consolidate variant/multi-buy management within product details - Clean up ProductsController for better maintainability **PWA & Notifications:** - Add proper PWA support detection (only show if browser supports) - Implement session-based notification prompt tracking - Prevent repeated prompts after dismissal in same session - Respect permanent dismissal preferences - Enhance iOS Safari detection and instructions **Technical Details:** - 6 files changed, 96 insertions(+), 286 deletions(-) - Build successful with 0 errors - All features production-ready 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4992b6b839
commit
5adf1b90d5
@ -34,12 +34,6 @@ public class ProductsController : Controller
|
|||||||
return View(products);
|
return View(products);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IActionResult Blazor(Guid? id)
|
|
||||||
{
|
|
||||||
ViewData["ProductId"] = id;
|
|
||||||
return View();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IActionResult> Create()
|
public async Task<IActionResult> Create()
|
||||||
{
|
{
|
||||||
var categories = await _categoryService.GetAllCategoriesAsync();
|
var categories = await _categoryService.GetAllCategoriesAsync();
|
||||||
@ -193,243 +187,6 @@ public class ProductsController : Controller
|
|||||||
return RedirectToAction(nameof(Index));
|
return RedirectToAction(nameof(Index));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Product Variations
|
|
||||||
public async Task<IActionResult> Variations(Guid id)
|
|
||||||
{
|
|
||||||
var product = await _productService.GetProductByIdAsync(id);
|
|
||||||
if (product == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
ViewData["Product"] = product;
|
|
||||||
var variations = await _productService.GetProductMultiBuysAsync(id);
|
|
||||||
return View(variations);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IActionResult> CreateVariation(Guid productId)
|
|
||||||
{
|
|
||||||
var product = await _productService.GetProductByIdAsync(productId);
|
|
||||||
if (product == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
// Force no-cache to ensure updated form loads
|
|
||||||
Response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
||||||
Response.Headers.Add("Pragma", "no-cache");
|
|
||||||
Response.Headers.Add("Expires", "0");
|
|
||||||
|
|
||||||
ViewData["Product"] = product;
|
|
||||||
|
|
||||||
// Get existing quantities to help user avoid duplicates
|
|
||||||
var existingQuantities = await _productService.GetProductMultiBuysAsync(productId);
|
|
||||||
ViewData["ExistingQuantities"] = existingQuantities.Select(v => v.Quantity).ToList();
|
|
||||||
|
|
||||||
return View(new CreateProductMultiBuyDto { ProductId = productId });
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
[ValidateAntiForgeryToken]
|
|
||||||
public async Task<IActionResult> CreateVariation(CreateProductMultiBuyDto model)
|
|
||||||
{
|
|
||||||
// Debug form data
|
|
||||||
Console.WriteLine("=== FORM DEBUG ===");
|
|
||||||
Console.WriteLine($"Received CreateVariation POST: ProductId={model?.ProductId}, Name='{model?.Name}', Quantity={model?.Quantity}, Price={model?.Price}");
|
|
||||||
Console.WriteLine($"ModelState.IsValid: {ModelState.IsValid}");
|
|
||||||
Console.WriteLine("Raw form data:");
|
|
||||||
foreach (var key in Request.Form.Keys)
|
|
||||||
{
|
|
||||||
Console.WriteLine($" {key} = '{Request.Form[key]}'");
|
|
||||||
}
|
|
||||||
Console.WriteLine("================");
|
|
||||||
|
|
||||||
if (!ModelState.IsValid)
|
|
||||||
{
|
|
||||||
foreach (var key in ModelState.Keys)
|
|
||||||
{
|
|
||||||
var errors = ModelState[key]?.Errors;
|
|
||||||
if (errors?.Any() == true)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"ModelState Error - {key}: {string.Join(", ", errors.Select(e => e.ErrorMessage))}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var product = await _productService.GetProductByIdAsync(model.ProductId);
|
|
||||||
ViewData["Product"] = product;
|
|
||||||
|
|
||||||
// Re-populate existing quantities for error display
|
|
||||||
var existingQuantities = await _productService.GetProductMultiBuysAsync(model.ProductId);
|
|
||||||
ViewData["ExistingQuantities"] = existingQuantities.Select(v => v.Quantity).ToList();
|
|
||||||
|
|
||||||
return View(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _productService.CreateProductMultiBuyAsync(model);
|
|
||||||
return RedirectToAction(nameof(Variations), new { id = model.ProductId });
|
|
||||||
}
|
|
||||||
catch (ArgumentException ex)
|
|
||||||
{
|
|
||||||
// Add the error to the appropriate field if it's a quantity conflict
|
|
||||||
if (ex.Message.Contains("quantity") && ex.Message.Contains("already exists"))
|
|
||||||
{
|
|
||||||
ModelState.AddModelError("Quantity", ex.Message);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ModelState.AddModelError("", ex.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
var product = await _productService.GetProductByIdAsync(model.ProductId);
|
|
||||||
ViewData["Product"] = product;
|
|
||||||
|
|
||||||
// Re-populate existing quantities for error display
|
|
||||||
var existingQuantities = await _productService.GetProductMultiBuysAsync(model.ProductId);
|
|
||||||
ViewData["ExistingQuantities"] = existingQuantities.Select(v => v.Quantity).ToList();
|
|
||||||
|
|
||||||
return View(model);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IActionResult> EditVariation(Guid id)
|
|
||||||
{
|
|
||||||
var variation = await _productService.GetProductMultiBuyByIdAsync(id);
|
|
||||||
if (variation == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
var product = await _productService.GetProductByIdAsync(variation.ProductId);
|
|
||||||
ViewData["Product"] = product;
|
|
||||||
|
|
||||||
var model = new UpdateProductMultiBuyDto
|
|
||||||
{
|
|
||||||
Name = variation.Name,
|
|
||||||
Description = variation.Description,
|
|
||||||
Quantity = variation.Quantity,
|
|
||||||
Price = variation.Price,
|
|
||||||
SortOrder = variation.SortOrder,
|
|
||||||
IsActive = variation.IsActive
|
|
||||||
};
|
|
||||||
|
|
||||||
return View(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
[ValidateAntiForgeryToken]
|
|
||||||
public async Task<IActionResult> EditVariation(Guid id, UpdateProductMultiBuyDto model)
|
|
||||||
{
|
|
||||||
if (!ModelState.IsValid)
|
|
||||||
{
|
|
||||||
var variation = await _productService.GetProductMultiBuyByIdAsync(id);
|
|
||||||
var product = await _productService.GetProductByIdAsync(variation!.ProductId);
|
|
||||||
ViewData["Product"] = product;
|
|
||||||
return View(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
var success = await _productService.UpdateProductMultiBuyAsync(id, model);
|
|
||||||
if (!success)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
var variationToRedirect = await _productService.GetProductMultiBuyByIdAsync(id);
|
|
||||||
return RedirectToAction(nameof(Variations), new { id = variationToRedirect!.ProductId });
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
[ValidateAntiForgeryToken]
|
|
||||||
public async Task<IActionResult> DeleteVariation(Guid id)
|
|
||||||
{
|
|
||||||
var variation = await _productService.GetProductMultiBuyByIdAsync(id);
|
|
||||||
if (variation == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
await _productService.DeleteProductMultiBuyAsync(id);
|
|
||||||
return RedirectToAction(nameof(Variations), new { id = variation.ProductId });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Product Variants (Colors/Flavors)
|
|
||||||
public async Task<IActionResult> Variants(Guid id)
|
|
||||||
{
|
|
||||||
var product = await _productService.GetProductByIdAsync(id);
|
|
||||||
if (product == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
ViewData["Product"] = product;
|
|
||||||
var variants = await _productService.GetProductVariantsAsync(id);
|
|
||||||
return View("ProductVariants", variants);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IActionResult> CreateVariant(Guid productId)
|
|
||||||
{
|
|
||||||
var product = await _productService.GetProductByIdAsync(productId);
|
|
||||||
if (product == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
ViewData["Product"] = product;
|
|
||||||
return View("CreateVariant", new CreateProductVariantDto { ProductId = productId });
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
[ValidateAntiForgeryToken]
|
|
||||||
public async Task<IActionResult> CreateVariant(CreateProductVariantDto model)
|
|
||||||
{
|
|
||||||
if (!ModelState.IsValid)
|
|
||||||
{
|
|
||||||
var product = await _productService.GetProductByIdAsync(model.ProductId);
|
|
||||||
ViewData["Product"] = product;
|
|
||||||
return View("CreateVariant", model);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _productService.CreateProductVariantAsync(model);
|
|
||||||
return RedirectToAction(nameof(Variants), new { id = model.ProductId });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IActionResult> EditVariant(Guid id)
|
|
||||||
{
|
|
||||||
var variant = await _productService.GetProductVariantByIdAsync(id);
|
|
||||||
if (variant == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
var product = await _productService.GetProductByIdAsync(variant.ProductId);
|
|
||||||
ViewData["Product"] = product;
|
|
||||||
|
|
||||||
var model = new UpdateProductVariantDto
|
|
||||||
{
|
|
||||||
Name = variant.Name,
|
|
||||||
VariantType = variant.VariantType,
|
|
||||||
StockLevel = variant.StockLevel,
|
|
||||||
SortOrder = variant.SortOrder,
|
|
||||||
IsActive = variant.IsActive
|
|
||||||
};
|
|
||||||
|
|
||||||
return View("EditVariant", model);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
[ValidateAntiForgeryToken]
|
|
||||||
public async Task<IActionResult> EditVariant(Guid id, UpdateProductVariantDto model)
|
|
||||||
{
|
|
||||||
if (!ModelState.IsValid)
|
|
||||||
{
|
|
||||||
var variant = await _productService.GetProductVariantByIdAsync(id);
|
|
||||||
var product = await _productService.GetProductByIdAsync(variant!.ProductId);
|
|
||||||
ViewData["Product"] = product;
|
|
||||||
return View("EditVariant", model);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _productService.UpdateProductVariantAsync(id, model);
|
|
||||||
var variantToRedirect = await _productService.GetProductVariantByIdAsync(id);
|
|
||||||
return RedirectToAction(nameof(Variants), new { id = variantToRedirect!.ProductId });
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
[ValidateAntiForgeryToken]
|
|
||||||
public async Task<IActionResult> DeleteVariant(Guid id)
|
|
||||||
{
|
|
||||||
var variant = await _productService.GetProductVariantByIdAsync(id);
|
|
||||||
if (variant == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
await _productService.DeleteProductVariantAsync(id);
|
|
||||||
return RedirectToAction(nameof(Variants), new { id = variant.ProductId });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Product Import/Export
|
// Product Import/Export
|
||||||
public IActionResult Import()
|
public IActionResult Import()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -442,6 +442,22 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-3 border-danger">
|
||||||
|
<div class="card-header bg-danger text-white">
|
||||||
|
<h5><i class="fas fa-exclamation-triangle"></i> Danger Zone</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-muted">Once you delete a product, there is no going back. Please be certain.</p>
|
||||||
|
<form method="post" action="@Url.Action("Delete", new { id = productId })"
|
||||||
|
onsubmit="return confirm('Are you sure you want to delete this product? This action cannot be undone.')">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<button type="submit" class="btn btn-danger w-100">
|
||||||
|
<i class="fas fa-trash"></i> Delete Product
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -9,20 +9,9 @@
|
|||||||
<h1><i class="fas fa-box"></i> Products</h1>
|
<h1><i class="fas fa-box"></i> Products</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<div class="btn-group">
|
<a href="@Url.Action("Create")" class="btn btn-primary">
|
||||||
<a href="@Url.Action("Blazor")" class="btn btn-success">
|
<i class="fas fa-plus"></i> <span class="d-none d-sm-inline">Add Product</span>
|
||||||
<i class="fas fa-rocket"></i> <span class="d-none d-sm-inline">New</span> Blazor UI
|
</a>
|
||||||
</a>
|
|
||||||
<a href="@Url.Action("Create")" class="btn btn-primary">
|
|
||||||
<i class="fas fa-plus"></i> <span class="d-none d-sm-inline">Add Product</span>
|
|
||||||
</a>
|
|
||||||
<a href="@Url.Action("Import")" class="btn btn-outline-success">
|
|
||||||
<i class="fas fa-upload"></i> <span class="d-none d-sm-inline">Import</span>
|
|
||||||
</a>
|
|
||||||
<a href="@Url.Action("Export")" class="btn btn-outline-info">
|
|
||||||
<i class="fas fa-download"></i> <span class="d-none d-sm-inline">Export</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -109,24 +98,9 @@
|
|||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group btn-group-sm">
|
<a href="@Url.Action("Edit", new { id = product.Id })" class="btn btn-outline-primary btn-sm" title="View Details">
|
||||||
<a href="@Url.Action("Edit", new { id = product.Id })" class="btn btn-outline-primary" title="Edit Product">
|
<i class="fas fa-info-circle"></i> <span class="d-none d-sm-inline">Details</span>
|
||||||
<i class="fas fa-edit"></i>
|
</a>
|
||||||
</a>
|
|
||||||
<a href="@Url.Action("Variations", new { id = product.Id })" class="btn btn-outline-info" title="Manage Multi-Buys">
|
|
||||||
<i class="fas fa-tags"></i>
|
|
||||||
</a>
|
|
||||||
<a href="@Url.Action("Variants", new { id = product.Id })" class="btn btn-outline-success" title="Manage Variants">
|
|
||||||
<i class="fas fa-palette"></i>
|
|
||||||
</a>
|
|
||||||
<form method="post" action="@Url.Action("Delete", new { id = product.Id })" class="d-inline"
|
|
||||||
onsubmit="return confirm('Are you sure you want to delete this product?')">
|
|
||||||
@Html.AntiForgeryToken()
|
|
||||||
<button type="submit" class="btn btn-outline-danger" title="Delete Product">
|
|
||||||
<i class="fas fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,9 +46,6 @@
|
|||||||
<a class="navbar-brand" href="@Url.Action("Index", "Dashboard", new { area = "Admin" })">
|
<a class="navbar-brand" href="@Url.Action("Index", "Dashboard", new { area = "Admin" })">
|
||||||
<i class="fas fa-store"></i> TeleShop Admin
|
<i class="fas fa-store"></i> TeleShop Admin
|
||||||
</a>
|
</a>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse">
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
|
||||||
</button>
|
|
||||||
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
|
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
|
||||||
<ul class="navbar-nav flex-grow-1">
|
<ul class="navbar-nav flex-grow-1">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
@ -178,8 +175,8 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="mobile-nav-item">
|
<li class="mobile-nav-item">
|
||||||
<a href="#" class="mobile-nav-link" onclick="toggleSettingsDrawer(); return false;">
|
<a href="#" class="mobile-nav-link" onclick="toggleSettingsDrawer(); return false;">
|
||||||
<i class="fas fa-cog"></i>
|
<i class="fas fa-bars"></i>
|
||||||
<span>Settings</span>
|
<span>Menu</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -189,7 +186,7 @@
|
|||||||
<div class="drawer-overlay" onclick="toggleSettingsDrawer()"></div>
|
<div class="drawer-overlay" onclick="toggleSettingsDrawer()"></div>
|
||||||
<div class="settings-drawer" id="settingsDrawer">
|
<div class="settings-drawer" id="settingsDrawer">
|
||||||
<div class="settings-drawer-header">
|
<div class="settings-drawer-header">
|
||||||
<h5>Settings</h5>
|
<h5>Menu</h5>
|
||||||
<button class="settings-drawer-close" onclick="toggleSettingsDrawer()">
|
<button class="settings-drawer-close" onclick="toggleSettingsDrawer()">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -243,6 +240,24 @@
|
|||||||
System Settings
|
System Settings
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="settings-menu-item">
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
</li>
|
||||||
|
<li class="settings-menu-item">
|
||||||
|
<a href="@Url.Action("Import", "Products", new { area = "Admin" })" class="settings-menu-link">
|
||||||
|
<i class="fas fa-upload"></i>
|
||||||
|
Import Products
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="settings-menu-item">
|
||||||
|
<a href="@Url.Action("Export", "Products", new { area = "Admin" })" class="settings-menu-link">
|
||||||
|
<i class="fas fa-download"></i>
|
||||||
|
Export Products
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="settings-menu-item">
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
</li>
|
||||||
<li class="settings-menu-item">
|
<li class="settings-menu-item">
|
||||||
<form method="post" action="@Url.Action("Logout", "Account", new { area = "Admin" })">
|
<form method="post" action="@Url.Action("Logout", "Account", new { area = "Admin" })">
|
||||||
<button type="submit" class="settings-menu-link" style="width: 100%; border: none; background: none; text-align: left;">
|
<button type="submit" class="settings-menu-link" style="width: 100%; border: none; background: none; text-align: left;">
|
||||||
|
|||||||
@ -42,11 +42,37 @@ class AdminNotificationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showAdminNotificationPrompt() {
|
showAdminNotificationPrompt() {
|
||||||
|
// Check if notifications are supported
|
||||||
|
if (!('Notification' in window) || !('PushManager' in window)) {
|
||||||
|
console.log('Admin Notifications: Push notifications not supported in this browser');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already enabled
|
||||||
|
if (window.pwaManager && window.pwaManager.pushSubscription) {
|
||||||
|
console.log('Admin Notifications: Already subscribed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if prompt already exists
|
// Check if prompt already exists
|
||||||
if (document.getElementById('admin-notification-prompt')) {
|
if (document.getElementById('admin-notification-prompt')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if dismissed in current session
|
||||||
|
const sessionDismissed = sessionStorage.getItem('notificationPromptDismissed');
|
||||||
|
if (sessionDismissed === 'true') {
|
||||||
|
console.log('Admin Notifications: Prompt dismissed this session');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if permanently dismissed
|
||||||
|
const permanentlyDismissed = localStorage.getItem('pushNotificationDeclined');
|
||||||
|
if (permanentlyDismissed === 'true') {
|
||||||
|
console.log('Admin Notifications: Notifications declined permanently');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const promptDiv = document.createElement('div');
|
const promptDiv = document.createElement('div');
|
||||||
promptDiv.id = 'admin-notification-prompt';
|
promptDiv.id = 'admin-notification-prompt';
|
||||||
promptDiv.className = 'alert alert-warning alert-dismissible position-fixed';
|
promptDiv.className = 'alert alert-warning alert-dismissible position-fixed';
|
||||||
@ -74,7 +100,7 @@ class AdminNotificationManager {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert" id="close-notification-prompt"></button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
document.body.appendChild(promptDiv);
|
document.body.appendChild(promptDiv);
|
||||||
@ -84,6 +110,7 @@ class AdminNotificationManager {
|
|||||||
try {
|
try {
|
||||||
await this.enableNotifications();
|
await this.enableNotifications();
|
||||||
promptDiv.remove();
|
promptDiv.remove();
|
||||||
|
sessionStorage.setItem('notificationPromptDismissed', 'true');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to enable notifications:', error);
|
console.error('Failed to enable notifications:', error);
|
||||||
this.showNotificationError('Failed to enable notifications. Please try again.');
|
this.showNotificationError('Failed to enable notifications. Please try again.');
|
||||||
@ -92,8 +119,14 @@ class AdminNotificationManager {
|
|||||||
|
|
||||||
document.getElementById('remind-later').addEventListener('click', () => {
|
document.getElementById('remind-later').addEventListener('click', () => {
|
||||||
promptDiv.remove();
|
promptDiv.remove();
|
||||||
// Set reminder for 1 hour
|
// Mark as dismissed for this session only
|
||||||
setTimeout(() => this.showAdminNotificationPrompt(), 60 * 60 * 1000);
|
sessionStorage.setItem('notificationPromptDismissed', 'true');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('close-notification-prompt').addEventListener('click', () => {
|
||||||
|
promptDiv.remove();
|
||||||
|
// Mark as dismissed for this session only
|
||||||
|
sessionStorage.setItem('notificationPromptDismissed', 'true');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -220,10 +220,20 @@ class PWAManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showManualInstallButton() {
|
showManualInstallButton() {
|
||||||
|
// Don't show if PWA is not supported or already installed
|
||||||
if (this.isInstalled() || document.getElementById('pwa-install-btn')) {
|
if (this.isInstalled() || document.getElementById('pwa-install-btn')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only show if browser supports PWA (has beforeinstallprompt event or is iOS)
|
||||||
|
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||||
|
const supportsPWA = 'serviceWorker' in navigator && ('BeforeInstallPromptEvent' in window || isIOS);
|
||||||
|
|
||||||
|
if (!supportsPWA) {
|
||||||
|
console.log('PWA: Browser does not support PWA installation');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const installBtn = document.createElement('button');
|
const installBtn = document.createElement('button');
|
||||||
installBtn.id = 'pwa-install-btn';
|
installBtn.id = 'pwa-install-btn';
|
||||||
installBtn.className = 'btn btn-primary btn-sm';
|
installBtn.className = 'btn btn-primary btn-sm';
|
||||||
@ -269,12 +279,17 @@ class PWAManager {
|
|||||||
const isChrome = navigator.userAgent.includes('Chrome');
|
const isChrome = navigator.userAgent.includes('Chrome');
|
||||||
const isEdge = navigator.userAgent.includes('Edge');
|
const isEdge = navigator.userAgent.includes('Edge');
|
||||||
const isFirefox = navigator.userAgent.includes('Firefox');
|
const isFirefox = navigator.userAgent.includes('Firefox');
|
||||||
|
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||||
|
|
||||||
let instructions = 'To install this app:\n\n';
|
let instructions = 'To install this app:\n\n';
|
||||||
|
|
||||||
if (isChrome || isEdge) {
|
if (isIOS) {
|
||||||
|
instructions += '1. Tap the Share button (□↑) in Safari\n';
|
||||||
|
instructions += '2. Scroll down and tap "Add to Home Screen"\n';
|
||||||
|
instructions += '3. Tap "Add" to install';
|
||||||
|
} else if (isChrome || isEdge) {
|
||||||
instructions += '1. Look for the install icon (⬇️) in the address bar\n';
|
instructions += '1. Look for the install icon (⬇️) in the address bar\n';
|
||||||
instructions += '2. Or click the browser menu (⋮) → "Install LittleShop Admin"\n';
|
instructions += '2. Or click the browser menu (⋮) → "Install TeleShop Admin"\n';
|
||||||
instructions += '3. Or check if there\'s an "Install app" option in the browser menu';
|
instructions += '3. Or check if there\'s an "Install app" option in the browser menu';
|
||||||
} else if (isFirefox) {
|
} else if (isFirefox) {
|
||||||
instructions += '1. Firefox doesn\'t support PWA installation yet\n';
|
instructions += '1. Firefox doesn\'t support PWA installation yet\n';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user