diff --git a/.env b/.env
new file mode 100644
index 0000000..4307fdb
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+TM_API_KEY=0FLYFe9BnzGlk2OPHrWleCulNzHpWgtC
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 3ec544c..40b878d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1 @@
-node_modules/
-.env
\ No newline at end of file
+node_modules/
\ No newline at end of file
diff --git a/index.html b/index.html
index 755995f..2b9a63e 100644
--- a/index.html
+++ b/index.html
@@ -24,17 +24,37 @@
Encore
-
+
-
+
+
+
+
+
+ Login
+ Register
+
+
+
+
+
+ Logout
+
+
@@ -42,7 +62,7 @@
-
+
Find Events
@@ -72,10 +92,7 @@
-
+
Search
@@ -84,7 +101,7 @@
-
+
Upcoming Events
@@ -94,6 +111,32 @@
+
+
+
+
+
+
+
+
+
+
+ My Invitations
+
+
+
+
+
diff --git a/js/app.js b/js/app.js
index 38e10cc..17c7c72 100644
--- a/js/app.js
+++ b/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;
});
-}
\ No newline at end of file
+}
+
+// =========================
+// 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 = "
No saved events yet.
";
+ 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 = "No invitations.
";
+ return;
+ }
+
+ data.forEach(inv => {
+
+ const div = document.createElement("div");
+
+ div.innerHTML = `
+ ${inv.fromUser} invited you to ${inv.eventName}
+ Accept
+ Decline
+ `;
+
+ 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();
+};
\ No newline at end of file
diff --git a/js/auth.js b/js/auth.js
new file mode 100644
index 0000000..c7582ec
--- /dev/null
+++ b/js/auth.js
@@ -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;
+}
\ No newline at end of file
diff --git a/js/ui/eventCard.js b/js/ui/eventCard.js
index 1e96673..c9f84fd 100644
--- a/js/ui/eventCard.js
+++ b/js/ui/eventCard.js
@@ -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;
}
\ No newline at end of file
diff --git a/server/server.js b/server/server.js
index 017c115..27c5743 100644
--- a/server/server.js
+++ b/server/server.js
@@ -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
// =========================