Add login, save and invite features #6
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1 @@
|
||||
node_modules/
|
||||
.env
|
||||
node_modules/
|
||||
65
index.html
65
index.html
@ -24,17 +24,37 @@
|
||||
<nav class="container d-flex justify-content-between align-items-center">
|
||||
|
||||
<h1 class="site-logo">Encore</h1>
|
||||
|
||||
|
||||
<ul class="nav">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="index.html">Home</a>
|
||||
<a class="nav-link" href="#" id="nav-events">Events</a>
|
||||
</li>
|
||||
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">Events</a>
|
||||
<a class="nav-link d-none" href="#" id="nav-my-events">My Events</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link d-none" href="#" id="nav-invitations">Invitations</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
<!-- LOGIN AREA -->
|
||||
<div id="auth-area">
|
||||
<input id="username" placeholder="Username" class="form-control d-inline w-auto">
|
||||
<input id="password" type="password" placeholder="Password" class="form-control d-inline w-auto">
|
||||
<button id="login-btn" class="btn btn-success">Login</button>
|
||||
<button id="register-btn" class="btn btn-secondary">Register</button>
|
||||
</div>
|
||||
|
||||
<!-- USER AREA -->
|
||||
<div id="user-area" class="d-none">
|
||||
<span id="user-name"></span>
|
||||
<button id="logout-btn" class="btn btn-danger btn-sm">Logout</button>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
@ -42,7 +62,7 @@
|
||||
<main class="container mt-4">
|
||||
|
||||
<!-- SEARCH / FILTER SECTION -->
|
||||
<section class="search mb-4">
|
||||
<section class="search mb-4" id="search-section">
|
||||
|
||||
<h2>Find Events</h2>
|
||||
|
||||
@ -72,10 +92,7 @@
|
||||
</select>
|
||||
|
||||
<!-- Button -->
|
||||
<button
|
||||
id="load-events"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
<button id="load-events" class="btn btn-primary">
|
||||
Search
|
||||
</button>
|
||||
|
||||
@ -84,7 +101,7 @@
|
||||
</section>
|
||||
|
||||
<!-- EVENTS LIST -->
|
||||
<section class="events">
|
||||
<section class="events" id="events-section">
|
||||
|
||||
<h2>Upcoming Events</h2>
|
||||
|
||||
@ -94,6 +111,32 @@
|
||||
|
||||
</section>
|
||||
|
||||
<!-- ========================= -->
|
||||
<!-- MY EVENTS -->
|
||||
<!-- ========================= -->
|
||||
<section id="saved-section" class="d-none">
|
||||
|
||||
<h2>My Events</h2>
|
||||
|
||||
<div id="saved-list">
|
||||
<p class="text-muted">No saved events yet.</p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- ========================= -->
|
||||
<!-- INVITATIONS -->
|
||||
<!-- ========================= -->
|
||||
<section id="invitations-section" class="d-none">
|
||||
|
||||
<h2>My Invitations</h2>
|
||||
|
||||
<div id="invitation-list">
|
||||
<p class="text-muted">No invitations yet.</p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- FOOTER -->
|
||||
|
||||
224
js/app.js
224
js/app.js
@ -1,7 +1,112 @@
|
||||
import { getEvents } from "./services/eventService.js";
|
||||
import { renderEventList } from "./ui/eventList.js";
|
||||
import { getFilters } from "./ui/filters.js";
|
||||
import { login, register } from "./auth.js";
|
||||
|
||||
// =========================
|
||||
// STATE
|
||||
// =========================
|
||||
let currentUser = null;
|
||||
let currentPassword = null;
|
||||
|
||||
window.currentUser = null;
|
||||
window.currentPassword = null;
|
||||
|
||||
// =========================
|
||||
// AUTH ELEMENTS
|
||||
// =========================
|
||||
const loginBtn = document.querySelector("#login-btn");
|
||||
const registerBtn = document.querySelector("#register-btn");
|
||||
|
||||
// =========================
|
||||
// NAVIGATION ELEMENTS
|
||||
// =========================
|
||||
const navEvents = document.querySelector("#nav-events");
|
||||
const navSaved = document.querySelector("#nav-my-events");
|
||||
const navInv = document.querySelector("#nav-invitations");
|
||||
|
||||
// =========================
|
||||
// SECTIONS
|
||||
// =========================
|
||||
const searchSection = document.querySelector("#search-section");
|
||||
const eventsSection = document.querySelector("#events-section");
|
||||
const savedSection = document.querySelector("#saved-section");
|
||||
const invSection = document.querySelector("#invitations-section");
|
||||
|
||||
// =========================
|
||||
// AUTH UI
|
||||
// =========================
|
||||
const authArea = document.querySelector("#auth-area");
|
||||
const userArea = document.querySelector("#user-area");
|
||||
const userNameEl = document.querySelector("#user-name");
|
||||
const logoutBtn = document.querySelector("#logout-btn");
|
||||
|
||||
// =========================
|
||||
// LOGIN
|
||||
// =========================
|
||||
loginBtn.addEventListener("click", async () => {
|
||||
|
||||
const username = document.querySelector("#username").value;
|
||||
const password = document.querySelector("#password").value;
|
||||
|
||||
const success = await login(username, password);
|
||||
|
||||
if (success) {
|
||||
currentUser = username;
|
||||
currentPassword = password;
|
||||
|
||||
window.currentUser = username;
|
||||
window.currentPassword = password;
|
||||
|
||||
// UI switch
|
||||
authArea.classList.add("d-none");
|
||||
userArea.classList.remove("d-none");
|
||||
|
||||
userNameEl.textContent = `👤 ${username}`;
|
||||
|
||||
navSaved.classList.remove("d-none");
|
||||
navInv.classList.remove("d-none");
|
||||
|
||||
} else {
|
||||
alert("Login failed");
|
||||
}
|
||||
});
|
||||
|
||||
// =========================
|
||||
// REGISTER
|
||||
// =========================
|
||||
registerBtn.addEventListener("click", async () => {
|
||||
|
||||
const username = document.querySelector("#username").value;
|
||||
|
||||
const data = await register(username);
|
||||
|
||||
alert(`User created. Password: ${data.password}`);
|
||||
});
|
||||
|
||||
// =========================
|
||||
// LOGOUT
|
||||
// =========================
|
||||
logoutBtn.addEventListener("click", () => {
|
||||
|
||||
currentUser = null;
|
||||
currentPassword = null;
|
||||
|
||||
window.currentUser = null;
|
||||
window.currentPassword = null;
|
||||
|
||||
authArea.classList.remove("d-none");
|
||||
userArea.classList.add("d-none");
|
||||
|
||||
navSaved.classList.add("d-none");
|
||||
navInv.classList.add("d-none");
|
||||
|
||||
showSection("events");
|
||||
});
|
||||
|
||||
// =========================
|
||||
// SEARCH
|
||||
// =========================
|
||||
const button = document.querySelector("#load-events");
|
||||
const container = document.querySelector("#event-list");
|
||||
const cityInput = document.querySelector("#city-input");
|
||||
@ -30,6 +135,9 @@ async function handleSearch() {
|
||||
renderEventList(filteredEvents, container);
|
||||
}
|
||||
|
||||
// =========================
|
||||
// FILTERS
|
||||
// =========================
|
||||
function applyFilters(events, dateFrom, dateTo, category) {
|
||||
|
||||
return events.filter(event => {
|
||||
@ -43,4 +151,118 @@ function applyFilters(events, dateFrom, dateTo, category) {
|
||||
|
||||
return matchDateFrom && matchDateTo && matchCategory;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// =========================
|
||||
// NAVIGATION LOGIC
|
||||
// =========================
|
||||
navEvents.addEventListener("click", () => {
|
||||
showSection("events");
|
||||
});
|
||||
|
||||
navSaved.addEventListener("click", () => {
|
||||
showSection("saved");
|
||||
loadSavedEvents();
|
||||
});
|
||||
|
||||
navInv.addEventListener("click", () => {
|
||||
showSection("invitations");
|
||||
loadInvitations();
|
||||
});
|
||||
|
||||
function showSection(section) {
|
||||
|
||||
searchSection.classList.add("d-none");
|
||||
eventsSection.classList.add("d-none");
|
||||
savedSection.classList.add("d-none");
|
||||
invSection.classList.add("d-none");
|
||||
|
||||
if (section === "events") {
|
||||
searchSection.classList.remove("d-none");
|
||||
eventsSection.classList.remove("d-none");
|
||||
}
|
||||
|
||||
if (section === "saved") {
|
||||
savedSection.classList.remove("d-none");
|
||||
}
|
||||
|
||||
if (section === "invitations") {
|
||||
invSection.classList.remove("d-none");
|
||||
}
|
||||
}
|
||||
|
||||
// =========================
|
||||
// SAVED EVENTS
|
||||
// =========================
|
||||
function loadSavedEvents() {
|
||||
|
||||
const saved = JSON.parse(localStorage.getItem("savedEvents") || "[]");
|
||||
const container = document.querySelector("#saved-list");
|
||||
|
||||
container.innerHTML = "";
|
||||
|
||||
if (saved.length === 0) {
|
||||
container.innerHTML = "<p>No saved events yet.</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
saved.forEach(event => {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = event.name;
|
||||
container.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
// =========================
|
||||
// INVITATIONS
|
||||
// =========================
|
||||
async function loadInvitations() {
|
||||
|
||||
if (!currentUser) return;
|
||||
|
||||
const res = await fetch("http://localhost:3000/api/invitation", {
|
||||
headers: {
|
||||
"X-Username": currentUser
|
||||
}
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
const container = document.querySelector("#invitation-list");
|
||||
|
||||
container.innerHTML = "";
|
||||
|
||||
if (data.length === 0) {
|
||||
container.innerHTML = "<p>No invitations.</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
data.forEach(inv => {
|
||||
|
||||
const div = document.createElement("div");
|
||||
|
||||
div.innerHTML = `
|
||||
<p><strong>${inv.fromUser}</strong> invited you to <em>${inv.eventName}</em></p>
|
||||
<button onclick="accept(${inv.id})">Accept</button>
|
||||
<button onclick="decline(${inv.id})">Decline</button>
|
||||
`;
|
||||
|
||||
container.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
// =========================
|
||||
// ACCEPT / DECLINE
|
||||
// =========================
|
||||
window.accept = async (id) => {
|
||||
await fetch(`http://localhost:3000/api/invitation/${id}/accept`, {
|
||||
method: "POST"
|
||||
});
|
||||
loadInvitations();
|
||||
};
|
||||
|
||||
window.decline = async (id) => {
|
||||
await fetch(`http://localhost:3000/api/invitation/${id}/decline`, {
|
||||
method: "POST"
|
||||
});
|
||||
loadInvitations();
|
||||
};
|
||||
27
js/auth.js
Normal file
27
js/auth.js
Normal file
@ -0,0 +1,27 @@
|
||||
const BASE_URL = "http://localhost:3000/api";
|
||||
|
||||
export async function register(username) {
|
||||
|
||||
const res = await fetch(`${BASE_URL}/user`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ username })
|
||||
});
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function login(username, password) {
|
||||
|
||||
const res = await fetch(`${BASE_URL}/user`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Username": username,
|
||||
"X-Password": password
|
||||
}
|
||||
});
|
||||
|
||||
return res.status === 201;
|
||||
}
|
||||
@ -33,5 +33,82 @@ export function createEventCard(event) {
|
||||
|
||||
article.append(title, date, venue);
|
||||
|
||||
// =========================
|
||||
// BUTTON CONTAINER
|
||||
// =========================
|
||||
const buttonContainer = document.createElement("div");
|
||||
buttonContainer.className = "d-flex gap-2 mt-2";
|
||||
|
||||
// =========================
|
||||
// SAVE BUTTON
|
||||
// =========================
|
||||
const saveBtn = document.createElement("button");
|
||||
saveBtn.textContent = "Save";
|
||||
saveBtn.className = "btn btn-outline-primary btn-sm";
|
||||
|
||||
saveBtn.addEventListener("click", () => {
|
||||
|
||||
if (!window.currentUser) {
|
||||
alert("Please login to save events");
|
||||
return;
|
||||
}
|
||||
|
||||
const saved = JSON.parse(localStorage.getItem("savedEvents") || "[]");
|
||||
|
||||
// prevent duplicates
|
||||
if (saved.find(e => e.id === event.id)) {
|
||||
alert("Event already saved");
|
||||
return;
|
||||
}
|
||||
|
||||
saved.push(event);
|
||||
localStorage.setItem("savedEvents", JSON.stringify(saved));
|
||||
|
||||
alert("Event saved!");
|
||||
});
|
||||
|
||||
saveBtn.disabled = !window.currentUser;
|
||||
|
||||
// =========================
|
||||
// INVITE BUTTON
|
||||
// =========================
|
||||
const inviteBtn = document.createElement("button");
|
||||
inviteBtn.textContent = "Invite";
|
||||
inviteBtn.className = "btn btn-primary btn-sm";
|
||||
|
||||
inviteBtn.addEventListener("click", async () => {
|
||||
|
||||
if (!window.currentUser) {
|
||||
alert("Please login to invite users");
|
||||
return;
|
||||
}
|
||||
|
||||
const toUser = prompt("Enter username to invite:");
|
||||
if (!toUser) return;
|
||||
|
||||
await fetch("http://localhost:3000/api/invitation", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Username": window.currentUser
|
||||
},
|
||||
body: JSON.stringify({
|
||||
toUser,
|
||||
eventId: event.id,
|
||||
eventName: event.name
|
||||
})
|
||||
});
|
||||
|
||||
alert("Invitation sent!");
|
||||
});
|
||||
|
||||
inviteBtn.disabled = !window.currentUser;
|
||||
|
||||
// =========================
|
||||
// APPEND BUTTONS
|
||||
// =========================
|
||||
buttonContainer.append(saveBtn, inviteBtn);
|
||||
article.appendChild(buttonContainer);
|
||||
|
||||
return article;
|
||||
}
|
||||
@ -10,7 +10,7 @@ app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// =========================
|
||||
// ROOT (optional)
|
||||
// ROOT
|
||||
// =========================
|
||||
app.get("/", (req, res) => {
|
||||
res.send("Encore API is running");
|
||||
@ -121,6 +121,44 @@ app.post("/api/invitation/:id/decline", (req, res) => {
|
||||
res.json(inv);
|
||||
});
|
||||
|
||||
let users = [];
|
||||
|
||||
// CREATE USER
|
||||
app.post("/api/user", (req, res) => {
|
||||
|
||||
const { username } = req.body;
|
||||
|
||||
if (!username) {
|
||||
return res.status(400).json({ message: "Username required" });
|
||||
}
|
||||
|
||||
if (users.find(u => u.username === username)) {
|
||||
return res.status(400).json({ message: "User exists" });
|
||||
}
|
||||
|
||||
const password = "1234";
|
||||
|
||||
const user = { username, password };
|
||||
users.push(user);
|
||||
|
||||
res.json({ name: username, password });
|
||||
});
|
||||
|
||||
// LOGIN
|
||||
app.get("/api/user", (req, res) => {
|
||||
|
||||
const username = req.header("X-Username");
|
||||
const password = req.header("X-Password");
|
||||
|
||||
const user = users.find(
|
||||
u => u.username === username && u.password === password
|
||||
);
|
||||
|
||||
if (!user) return res.sendStatus(401);
|
||||
|
||||
res.sendStatus(201);
|
||||
});
|
||||
|
||||
// =========================
|
||||
// START SERVER
|
||||
// =========================
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user