Compare commits
No commits in common. "main" and "feature/project-setup" have entirely different histories.
main
...
feature/pr
@ -1,55 +0,0 @@
|
||||
/* =========================
|
||||
EVENT CARD COMPONENT
|
||||
========================= */
|
||||
|
||||
.event-card {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
/* Hover only on devices that support it */
|
||||
@media (hover: hover) {
|
||||
.event-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Title */
|
||||
.event-card__title {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Date */
|
||||
.event-card__date {
|
||||
color: gray;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Venue */
|
||||
.event-card__venue {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
FORM ELEMENT IMPROVEMENTS
|
||||
========================= */
|
||||
|
||||
button,
|
||||
input {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
button:focus,
|
||||
input:focus {
|
||||
outline: 2px solid #007bff;
|
||||
}
|
||||
133
css/main.css
133
css/main.css
@ -1,123 +1,14 @@
|
||||
/* =========================
|
||||
RESET & BASE
|
||||
========================= */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f9f9f9;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
GLOBAL LAYOUT
|
||||
========================= */
|
||||
|
||||
main {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
HEADER
|
||||
========================= */
|
||||
|
||||
.site-header {
|
||||
background-color: #3dd0a9;
|
||||
color: #222; /* fixed contrast */
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
SEARCH SECTION
|
||||
========================= */
|
||||
|
||||
.event-search h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.search__controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
EVENTS LIST
|
||||
========================= */
|
||||
|
||||
.events h2 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#event-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
FOOTER
|
||||
========================= */
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
margin-top: 2rem;
|
||||
font-size: 0.9rem;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
TABLET (>=768px)
|
||||
========================= */
|
||||
|
||||
@media (min-width: 768px) {
|
||||
|
||||
.search__controls {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
.event-card {
|
||||
border: 1px solid #ddd;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.search__controls input,
|
||||
.search__controls select {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
|
||||
.event-card__title {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.search__controls button {
|
||||
flex: 0 0 auto;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
#event-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* =========================
|
||||
DESKTOP (>=1024px)
|
||||
========================= */
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
|
||||
#event-list {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.event-card__date {
|
||||
color: gray;
|
||||
}
|
||||
39
index.html
39
index.html
@ -7,14 +7,13 @@
|
||||
|
||||
<title>Encore – Discover Events</title>
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<!-- CSS -->
|
||||
<link rel="stylesheet" href="css/main.css">
|
||||
|
||||
<!-- Bootstrap (optional CSS framework) -->
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet">
|
||||
|
||||
<!-- CSS -->
|
||||
<link rel="stylesheet" href="css/components.css">
|
||||
<link rel="stylesheet" href="css/main.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@ -33,22 +32,23 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">Events</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
|
||||
<!-- MAIN CONTENT -->
|
||||
<main class="container mt-4">
|
||||
|
||||
<!-- SEARCH / FILTER SECTION -->
|
||||
<section class="search mb-4">
|
||||
<section class="event-search mb-4">
|
||||
|
||||
<h2>Find Events</h2>
|
||||
|
||||
<div class="search__controls">
|
||||
<div class="d-flex gap-2">
|
||||
|
||||
<!-- City -->
|
||||
<input
|
||||
id="city-input"
|
||||
type="text"
|
||||
@ -56,22 +56,6 @@
|
||||
placeholder="Enter city (e.g. Zurich)"
|
||||
>
|
||||
|
||||
<!-- Date -->
|
||||
<label for="date-from">From</label>
|
||||
<input id="date-from" type="date">
|
||||
|
||||
<label for="date-to">To</label>
|
||||
<input id="date-to" type="date">
|
||||
|
||||
<!-- Category -->
|
||||
<select id="category-input" class="form-control">
|
||||
<option value="">All categories</option>
|
||||
<option value="music">Music</option>
|
||||
<option value="sports">Sports</option>
|
||||
<option value="arts & theatre">Arts & Theatre</option>
|
||||
</select>
|
||||
|
||||
<!-- Button -->
|
||||
<button
|
||||
id="load-events"
|
||||
class="btn btn-primary"
|
||||
@ -83,12 +67,13 @@
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
<!-- EVENTS LIST -->
|
||||
<section class="events">
|
||||
|
||||
<h2>Upcoming Events</h2>
|
||||
|
||||
<div id="event-list">
|
||||
<div id="event-list" class="events__list">
|
||||
<p class="text-muted">Search for events to begin.</p>
|
||||
</div>
|
||||
|
||||
@ -96,11 +81,15 @@
|
||||
|
||||
</main>
|
||||
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer class="site-footer text-center mt-5 p-3">
|
||||
|
||||
<p>© 2026 Encore – Event Discovery</p>
|
||||
|
||||
</footer>
|
||||
|
||||
|
||||
<!-- JS -->
|
||||
<script type="module" src="js/app.js"></script>
|
||||
|
||||
|
||||
53
js/app.js
53
js/app.js
@ -1,46 +1,39 @@
|
||||
import { getEvents } from "./services/eventService.js";
|
||||
import { renderEventList } from "./ui/eventList.js";
|
||||
import { getFilters } from "./ui/filters.js";
|
||||
import { fetchEvents } from "./api/ticketmaster.js";
|
||||
import { createEventCard } from "./ui/eventCard.js";
|
||||
|
||||
const button = document.querySelector("#load-events");
|
||||
const container = document.querySelector("#event-list");
|
||||
const cityInput = document.querySelector("#city-input");
|
||||
|
||||
button.addEventListener("click", handleSearch);
|
||||
button.addEventListener("click", async () => {
|
||||
|
||||
cityInput.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") handleSearch();
|
||||
});
|
||||
|
||||
async function handleSearch() {
|
||||
|
||||
const { city, dateFrom, dateTo, category } = getFilters();
|
||||
const city = cityInput.value.trim();
|
||||
|
||||
if (!city) {
|
||||
container.innerHTML = "<p class='text-danger'>Please enter a city.</p>";
|
||||
container.innerHTML = "Please enter a city.";
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = "<p class='text-muted'>Loading events...</p>";
|
||||
container.innerHTML = "Loading events...";
|
||||
|
||||
const events = await getEvents(city);
|
||||
const events = await fetchEvents(city);
|
||||
|
||||
const filteredEvents = applyFilters(events, dateFrom, dateTo, category);
|
||||
container.innerHTML = "";
|
||||
|
||||
renderEventList(filteredEvents, container);
|
||||
}
|
||||
if (events.length === 0) {
|
||||
container.innerHTML = "No events found.";
|
||||
return;
|
||||
}
|
||||
|
||||
function applyFilters(events, dateFrom, dateTo, category) {
|
||||
|
||||
return events.filter(event => {
|
||||
|
||||
const matchDateFrom = dateFrom ? event.date >= dateFrom : true;
|
||||
const matchDateTo = dateTo ? event.date <= dateTo : true;
|
||||
|
||||
const matchCategory = category
|
||||
? event.category && event.category.includes(category)
|
||||
: true;
|
||||
|
||||
return matchDateFrom && matchDateTo && matchCategory;
|
||||
events.forEach(event => {
|
||||
const card = createEventCard(event);
|
||||
container.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
cityInput.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
button.click();
|
||||
}
|
||||
});
|
||||
@ -6,11 +6,7 @@ export async function getEvents(city) {
|
||||
return events.map(event => ({
|
||||
id: event.id,
|
||||
name: event.name,
|
||||
date: event.dates?.start?.localDate || null,
|
||||
time: event.dates?.start?.localTime || null,
|
||||
venue: event._embedded?.venues?.[0]?.name || "Unknown venue",
|
||||
category: event.classifications?.[0]?.segment?.name
|
||||
? event.classifications[0].segment.name.toLowerCase()
|
||||
: null
|
||||
date: event.dates.start.localDate,
|
||||
venue: event._embedded?.venues[0]?.name
|
||||
}));
|
||||
}
|
||||
@ -1,37 +1,17 @@
|
||||
export function createEventCard(event) {
|
||||
|
||||
const article = document.createElement("article");
|
||||
article.className = "event-card";
|
||||
|
||||
const title = document.createElement("h3");
|
||||
title.className = "event-card__title";
|
||||
title.textContent = event.name;
|
||||
|
||||
const formattedDate = event.date
|
||||
? new Date(event.date).toLocaleDateString("de-CH", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric"
|
||||
})
|
||||
: "Date not available";
|
||||
|
||||
const formattedTime = event.time
|
||||
? event.time.slice(0, 5)
|
||||
: "";
|
||||
|
||||
const dateTime = formattedTime
|
||||
? `${formattedDate}, ${formattedTime}`
|
||||
: formattedDate;
|
||||
|
||||
const date = document.createElement("p");
|
||||
date.className = "event-card__date";
|
||||
date.textContent = dateTime;
|
||||
|
||||
const venue = document.createElement("p");
|
||||
venue.className = "event-card__venue";
|
||||
venue.textContent = event.venue;
|
||||
|
||||
article.append(title, date, venue);
|
||||
|
||||
return article;
|
||||
}
|
||||
const article = document.createElement("article");
|
||||
article.className = "event-card";
|
||||
|
||||
const name = event.name;
|
||||
const date = event.dates.start.localDate;
|
||||
const venue = event._embedded?.venues[0]?.name;
|
||||
|
||||
article.innerHTML = `
|
||||
<h3 class="event-card__title">${name}</h3>
|
||||
<p class="event-card__date">${date}</p>
|
||||
<p class="event-card__venue">${venue}</p>
|
||||
`;
|
||||
|
||||
return article;
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
import { createEventCard } from "./eventCard.js";
|
||||
|
||||
export function renderEventList(events, container) {
|
||||
|
||||
container.innerHTML = "";
|
||||
|
||||
if (events.length === 0) {
|
||||
container.innerHTML = "No events found.";
|
||||
return;
|
||||
}
|
||||
|
||||
events.forEach(event => {
|
||||
const card = createEventCard(event);
|
||||
container.appendChild(card);
|
||||
});
|
||||
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
export function getFilters() {
|
||||
const city = document.querySelector("#city-input").value.trim();
|
||||
const dateFrom = document.querySelector("#date-from").value;
|
||||
const dateTo = document.querySelector("#date-to").value;
|
||||
const category = document.querySelector("#category-input").value;
|
||||
|
||||
return { city, dateFrom, dateTo, category };
|
||||
}
|
||||
3
js/utils/helpers.js
Normal file
3
js/utils/helpers.js
Normal file
@ -0,0 +1,3 @@
|
||||
export function formatDate(date) {
|
||||
return new Date(date).toLocaleDateString();
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user