539 lines
16 KiB
JavaScript
539 lines
16 KiB
JavaScript
|
|
/**
|
||
|
|
* 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();
|
||
|
|
}
|
||
|
|
})();
|