Merge pull request 'Add login, save and invite features' (#6) from feature/login into main

Reviewed-on: #6
This commit is contained in:
Karolina Thöny-Tyganova 2026-04-10 22:56:18 +02:00
commit 437e6da769
7 changed files with 422 additions and 15 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
TM_API_KEY=0FLYFe9BnzGlk2OPHrWleCulNzHpWgtC

3
.gitignore vendored
View File

@ -1,2 +1 @@
node_modules/ node_modules/
.env

View File

@ -24,17 +24,37 @@
<nav class="container d-flex justify-content-between align-items-center"> <nav class="container d-flex justify-content-between align-items-center">
<h1 class="site-logo">Encore</h1> <h1 class="site-logo">Encore</h1>
<ul class="nav"> <ul class="nav">
<li class="nav-item"> <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>
<li class="nav-item"> <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>
<li class="nav-item">
<a class="nav-link d-none" href="#" id="nav-invitations">Invitations</a>
</li>
</ul> </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> </nav>
</header> </header>
@ -42,7 +62,7 @@
<main class="container mt-4"> <main class="container mt-4">
<!-- SEARCH / FILTER SECTION --> <!-- SEARCH / FILTER SECTION -->
<section class="search mb-4"> <section class="search mb-4" id="search-section">
<h2>Find Events</h2> <h2>Find Events</h2>
@ -72,10 +92,7 @@
</select> </select>
<!-- Button --> <!-- Button -->
<button <button id="load-events" class="btn btn-primary">
id="load-events"
class="btn btn-primary"
>
Search Search
</button> </button>
@ -84,7 +101,7 @@
</section> </section>
<!-- EVENTS LIST --> <!-- EVENTS LIST -->
<section class="events"> <section class="events" id="events-section">
<h2>Upcoming Events</h2> <h2>Upcoming Events</h2>
@ -94,6 +111,32 @@
</section> </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> </main>
<!-- FOOTER --> <!-- FOOTER -->

224
js/app.js
View File

@ -1,7 +1,112 @@
import { getEvents } from "./services/eventService.js"; import { getEvents } from "./services/eventService.js";
import { renderEventList } from "./ui/eventList.js"; import { renderEventList } from "./ui/eventList.js";
import { getFilters } from "./ui/filters.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 button = document.querySelector("#load-events");
const container = document.querySelector("#event-list"); const container = document.querySelector("#event-list");
const cityInput = document.querySelector("#city-input"); const cityInput = document.querySelector("#city-input");
@ -30,6 +135,9 @@ async function handleSearch() {
renderEventList(filteredEvents, container); renderEventList(filteredEvents, container);
} }
// =========================
// FILTERS
// =========================
function applyFilters(events, dateFrom, dateTo, category) { function applyFilters(events, dateFrom, dateTo, category) {
return events.filter(event => { return events.filter(event => {
@ -43,4 +151,118 @@ function applyFilters(events, dateFrom, dateTo, category) {
return matchDateFrom && matchDateTo && matchCategory; 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
View 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;
}

View File

@ -33,5 +33,82 @@ export function createEventCard(event) {
article.append(title, date, venue); 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; return article;
} }

View File

@ -10,7 +10,7 @@ app.use(cors());
app.use(express.json()); app.use(express.json());
// ========================= // =========================
// ROOT (optional) // ROOT
// ========================= // =========================
app.get("/", (req, res) => { app.get("/", (req, res) => {
res.send("Encore API is running"); res.send("Encore API is running");
@ -121,6 +121,44 @@ app.post("/api/invitation/:id/decline", (req, res) => {
res.json(inv); 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 // START SERVER
// ========================= // =========================