Files
SkyArtShop/website/public/assets/js/customer-auth.js

539 lines
16 KiB
JavaScript
Raw Normal View History

2026-01-18 02:22:05 -06:00
/**
* Customer Authentication State Manager
* Handles checking login status and updating UI accordingly
*/
(function () {
"use strict";
console.log("[customer-auth.js] Loading...");
// Customer authentication state
window.CustomerAuth = {
user: null,
isLoggedIn: false,
isLoading: true,
// Initialize - check session on page load
async init() {
console.log("[CustomerAuth] Initializing...");
await this.checkSession();
this.updateNavbar();
this.setupEventListeners();
},
// Check if user is logged in
async checkSession() {
try {
const response = await fetch("/api/customers/session", {
credentials: "include",
});
if (response.ok) {
const data = await response.json();
if (data.success && data.loggedIn) {
this.user = data.customer;
this.isLoggedIn = true;
console.log("[CustomerAuth] User logged in:", this.user.firstName);
// Sync local cart/wishlist with server
await this.syncCartWithServer();
await this.syncWishlistWithServer();
} else {
this.user = null;
this.isLoggedIn = false;
}
} else {
this.user = null;
this.isLoggedIn = false;
}
} catch (error) {
console.error("[CustomerAuth] Session check failed:", error);
this.user = null;
this.isLoggedIn = false;
}
this.isLoading = false;
},
// Update navbar based on auth state
updateNavbar() {
// Find the sign in link/button in nav-actions
const navActions = document.querySelector(".nav-actions");
if (!navActions) {
console.warn("[CustomerAuth] nav-actions not found");
return;
}
// Find the existing sign in link
const signinLink = navActions.querySelector(
'a[href="/signin"], a[href="/account"]',
);
if (this.isLoggedIn && this.user) {
// User is logged in - update UI
if (signinLink) {
// Replace with user dropdown
const userDropdown = document.createElement("div");
userDropdown.className = "user-dropdown";
userDropdown.innerHTML = `
<button class="nav-icon-btn user-btn" title="Account" id="userDropdownBtn">
<i class="bi bi-person-check-fill"></i>
<span class="user-name-short">${this.escapeHtml(
this.user.firstName,
)}</span>
</button>
<div class="user-dropdown-menu" id="userDropdownMenu">
<div class="user-dropdown-header">
<i class="bi bi-person-circle"></i>
<div class="user-info">
<span class="welcome-text">Welcome back,</span>
<span class="user-name">${this.escapeHtml(
this.user.firstName,
)} ${this.escapeHtml(this.user.lastName || "")}</span>
</div>
</div>
<div class="user-dropdown-divider"></div>
<a href="/account" class="user-dropdown-item">
<i class="bi bi-person"></i> My Account
</a>
<a href="/account#orders" class="user-dropdown-item">
<i class="bi bi-bag-check"></i> My Orders
</a>
<a href="/account#wishlist" class="user-dropdown-item">
<i class="bi bi-heart"></i> My Wishlist
</a>
<div class="user-dropdown-divider"></div>
<button class="user-dropdown-item logout-btn" id="logoutBtn">
<i class="bi bi-box-arrow-right"></i> Sign Out
</button>
</div>
`;
signinLink.replaceWith(userDropdown);
// Add dropdown toggle
const dropdownBtn = document.getElementById("userDropdownBtn");
const dropdownMenu = document.getElementById("userDropdownMenu");
const logoutBtn = document.getElementById("logoutBtn");
if (dropdownBtn && dropdownMenu) {
dropdownBtn.addEventListener("click", (e) => {
e.stopPropagation();
dropdownMenu.classList.toggle("show");
});
// Close dropdown when clicking outside
document.addEventListener("click", () => {
dropdownMenu.classList.remove("show");
});
}
if (logoutBtn) {
logoutBtn.addEventListener("click", () => this.logout());
}
}
} else {
// User is not logged in - ensure sign in link is present
if (signinLink) {
// Update to show sign in icon
signinLink.innerHTML = '<i class="bi bi-person"></i>';
signinLink.href = "/signin";
signinLink.title = "Sign In";
}
}
// Add user dropdown styles if not present
this.addStyles();
},
// Sync local cart with server when logged in
async syncCartWithServer() {
if (!this.isLoggedIn) return;
try {
// Get server cart
const response = await fetch("/api/customers/cart", {
credentials: "include",
});
if (response.ok) {
const data = await response.json();
if (data.success && data.items) {
// Merge with local cart
const localCart = JSON.parse(
localStorage.getItem("skyart_cart") || "[]",
);
// If local cart has items, push them to server
for (const item of localCart) {
const exists = data.items.find((i) => i.productId === item.id);
if (!exists) {
await this.addToServerCart(item);
}
}
// Update local cart with server cart
const mergedCart = data.items.map((item) => ({
id: item.productId,
name: item.name,
price: item.price,
image: item.image,
quantity: item.quantity,
variantColor: item.variantColor,
variantSize: item.variantSize,
}));
localStorage.setItem("skyart_cart", JSON.stringify(mergedCart));
if (window.AppState) {
window.AppState.cart = mergedCart;
window.AppState.updateUI();
}
}
}
} catch (error) {
console.error("[CustomerAuth] Cart sync failed:", error);
}
},
// Sync local wishlist with server
async syncWishlistWithServer() {
if (!this.isLoggedIn) return;
try {
const response = await fetch("/api/customers/wishlist", {
credentials: "include",
});
if (response.ok) {
const data = await response.json();
if (data.success && data.items) {
// Merge with local wishlist
const localWishlist = JSON.parse(
localStorage.getItem("wishlist") || "[]",
);
// Push local items to server
for (const item of localWishlist) {
const exists = data.items.find((i) => i.productId === item.id);
if (!exists) {
await this.addToServerWishlist(item);
}
}
// Update local wishlist with server data
const mergedWishlist = data.items.map((item) => ({
id: item.productId,
name: item.name,
price: item.price,
image: item.image,
}));
localStorage.setItem("wishlist", JSON.stringify(mergedWishlist));
if (window.AppState) {
window.AppState.wishlist = mergedWishlist;
window.AppState.updateUI();
}
}
}
} catch (error) {
console.error("[CustomerAuth] Wishlist sync failed:", error);
}
},
// Add item to server cart
async addToServerCart(item) {
try {
await fetch("/api/customers/cart", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
productId: item.id,
quantity: item.quantity || 1,
variantColor: item.variantColor,
variantSize: item.variantSize,
}),
});
} catch (error) {
console.error("[CustomerAuth] Add to server cart failed:", error);
}
},
// Add item to server wishlist
async addToServerWishlist(item) {
try {
await fetch("/api/customers/wishlist", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ productId: item.id }),
});
} catch (error) {
console.error("[CustomerAuth] Add to server wishlist failed:", error);
}
},
// Setup event listeners for cart/wishlist changes
setupEventListeners() {
// Listen for cart changes to sync with server
window.addEventListener("cart-updated", async (e) => {
if (this.isLoggedIn && e.detail) {
// Debounce sync
clearTimeout(this._syncCartTimeout);
this._syncCartTimeout = setTimeout(() => {
this.syncCartChanges(e.detail);
}, 500);
}
});
// Listen for wishlist changes
window.addEventListener("wishlist-updated", async (e) => {
if (this.isLoggedIn && e.detail) {
clearTimeout(this._syncWishlistTimeout);
this._syncWishlistTimeout = setTimeout(() => {
this.syncWishlistChanges(e.detail);
}, 500);
}
});
},
// Sync cart changes with server
async syncCartChanges(cart) {
if (!this.isLoggedIn) return;
// For now, just ensure items are added/removed
// More sophisticated sync could be implemented
},
// Sync wishlist changes with server
async syncWishlistChanges(wishlist) {
if (!this.isLoggedIn) return;
// Sync wishlist changes
},
// Logout
async logout() {
try {
const response = await fetch("/api/customers/logout", {
method: "POST",
credentials: "include",
});
if (response.ok) {
this.user = null;
this.isLoggedIn = false;
// Show notification
if (window.AppState && window.AppState.showNotification) {
window.AppState.showNotification(
"You have been signed out",
"info",
);
}
// Redirect to home or refresh
window.location.href = "/home";
}
} catch (error) {
console.error("[CustomerAuth] Logout failed:", error);
}
},
// Helper: Escape HTML
escapeHtml(text) {
if (!text) return "";
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
},
// Add styles for user dropdown
addStyles() {
if (document.getElementById("customer-auth-styles")) return;
const style = document.createElement("style");
style.id = "customer-auth-styles";
style.textContent = `
.user-dropdown {
position: relative;
}
.user-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px !important;
border-radius: 20px;
background: var(--primary-pink-light, #fff5f7);
transition: all 0.2s ease;
}
.user-btn:hover {
background: var(--primary-pink, #ff85a2);
color: white;
}
.user-btn i {
font-size: 1.1rem;
color: var(--primary-pink, #ff85a2);
}
.user-btn:hover i {
color: white;
}
.user-name-short {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-primary, #333);
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-btn:hover .user-name-short {
color: white;
}
.user-dropdown-menu {
position: absolute;
top: calc(100% + 10px);
right: 0;
min-width: 220px;
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.15);
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.2s ease;
z-index: 1000;
overflow: hidden;
}
.user-dropdown-menu.show {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.user-dropdown-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: var(--primary-pink-light, #fff5f7);
}
.user-dropdown-header > i {
font-size: 2rem;
color: var(--primary-pink, #ff85a2);
}
.user-dropdown-header .user-info {
display: flex;
flex-direction: column;
}
.user-dropdown-header .welcome-text {
font-size: 0.75rem;
color: var(--text-light, #999);
}
.user-dropdown-header .user-name {
font-size: 0.9rem;
font-weight: 600;
color: var(--text-primary, #333);
}
.user-dropdown-divider {
height: 1px;
background: var(--border-light, #eee);
margin: 4px 0;
}
.user-dropdown-item {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
color: var(--text-primary, #333);
text-decoration: none;
font-size: 0.9rem;
transition: all 0.2s ease;
cursor: pointer;
border: none;
background: none;
width: 100%;
text-align: left;
}
.user-dropdown-item:hover {
background: var(--primary-pink-light, #fff5f7);
color: var(--primary-pink, #ff85a2);
}
.user-dropdown-item i {
font-size: 1rem;
width: 20px;
text-align: center;
}
.logout-btn {
color: #e74c3c;
}
.logout-btn:hover {
background: #fdf2f2;
color: #c0392b;
}
@media (max-width: 768px) {
.user-name-short {
display: none;
}
.user-btn {
padding: 8px !important;
border-radius: 50%;
}
.user-dropdown {
-webkit-transform: translateZ(0);
transform: translateZ(0);
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
}
.user-dropdown-menu {
right: -10px;
min-width: 200px;
position: fixed;
top: 70px;
right: 10px;
-webkit-transform: translateZ(0);
transform: translateZ(0);
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
will-change: opacity, visibility;
}
.user-dropdown-menu.show {
transform: translateZ(0);
}
}
`;
document.head.appendChild(style);
},
};
// Initialize on DOM ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
window.CustomerAuth.init();
});
} else {
window.CustomerAuth.init();
}
})();