Merge pull request 'Add login, save and invite features' (#6) from feature/login into main
Reviewed-on: #6
This commit is contained in:
commit
437e6da769
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
.env
|
|
||||||
65
index.html
65
index.html
@ -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
224
js/app.js
@ -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
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);
|
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;
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
// =========================
|
// =========================
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user