diff --git a/README.md b/README.md index 377ea97..1387ca6 100644 --- a/README.md +++ b/README.md @@ -1,112 +1,120 @@ -# API @cmda-minor-web 2024 - 2025 -Het web is een geweldige plek en de beschikbare technologieën ervan zijn vandaag de dag krachtiger dan ooit tevoren. -De kracht van het web ligt in het feit dat het een platform is dat voor iedereen beschikbaar is en dat het gebaseerd is -op open standaarden. De technologieën worden ontworpen en gespecificeerd op basis van consensus en zijn niet in handen -van één enkele entiteit. +# API -Desondanks zijn er veel mensen en bedrijven die vinden dat het internet niet voldoet aan hun behoeften. Dit blijkt uit -de pogingen van grote techbedrijven om hun eigen afgesloten ecosystemen te creëren. Ze streven hiermee naar controle over -zowel de gebruikerservaring als de gegenereerde data. +Maker: **Tymo Smids** -**In dit vier weken durende vak zullen we de kracht van het web ervaren en kijken hoe we (mobiele) web apps kunnen maken die -net zo aantrekkelijk zijn als native mobiele apps. We beginnen met het maken van een server-side gerenderde applicatie -waarbij we geleidelijk de gebruikerservaring verbeteren met relevante beschikbare web API's.** +Datum: *2025/03/25* - *2025/04/29* -[TLDR; hoe zet ik mijn project op?](#Inrichten-ontwikkelomgeving) +## Randvoorwaarden -## Doelen +- Minimaal een overzichts- en detailpagina; +- Gebouwd in TinyHTTP + Liquid; +- Minimaal een content API; +- Minimaal twee Web API's. -Na deze cursus zul je: +## Mijn project -- In staat zijn om een server-side gerenderde applicatie te maken. -- In staat zijn om een enerverende gebruikerservaring te creëren. -- Een breder begrip hebben van het web en zijn mogelijkheden. +De pagina's die ik heb zijn: een homepagina, detailpagina en een favorieten pagina. Het project is gebouwd in **TinyHTTP** en **Liquid** is de templating taal die is gebruikt. De **content API** die is gebruikt is de `MovieDB API`. De **Web API's** die gebruikt worden zijn: de `Localstorage API` en de `View transition API`. -## Opdracht +### Verbinden met de API -In dit vak zullen we een van de meest voorkomende app-concepten van vandaag -gebruiken en ontdekken dat we deze kunnen maken met moderne webtechnologie -met als doel om een rijke gebruikerservaring creëeren. +De API key is opgeslagen in het `.env` bestand. Dit bestand staat in de `.gitignore`. Dit zorgt ervoor dat de key veilig wordt opgeslagen want de bestanden in de .gitignore worden niet meegestuurd naar Github. Als dit niet wordt gedaan kan iedereen bij de api key en zelf request afvuren. -Randvoorwaarden: +```js +// ApiKey and URL for The Movie Database API +const apiKey = process.env.movieDB_APIKey; +const apiUrl = 'https://api.themoviedb.org/3/discover/movie'; +``` -- Minimaal een overzichts- en detailpagina -- Gebouwd in TinyHTTP + Liquid -- Minimaal een content API -- Minimaal twee Web API's +Als de `apiUrl` direct in de browser zou worden gezet krijg je een Json bestand terug. Dit bestand is ook wat je krijgt als je een `Fetch()` afvuurt. -Voorbeelden: +```js + const response = await fetch(movieDetailsUrl); + const item = await response.json(); +``` -- Maak je eigen streamingplatform (Netflix/Spotify). -- Maak je eigen doom-scroll-app (Instagram/TikTok). -- Maak je eigen chatapplicatie (WhatsApp/Signal). -- Een andere app die je zelf leuk vindt... +De data die je terugkrijgt kan je nu gebruiken om de website te vullen. -Voorbeeld content API's die je kan gebruiken: +```js +
+ {{ item.title }} +

{{ item.title }}

+
+``` -- [MovieDB API](https://developer.themoviedb.org/reference/intro/getting-started) -- [Rijksmuseum API](https://data.rijksmuseum.nl/object-metadata/api/) -- [Spotify API](https://developer.spotify.com/documentation/web-api) -- ... +## Favorieten -Voorbeelden van Web API's die je kan gebruiken: +Om favorieten toe te voegen heb ik gebruik gemaakt van localstorage. Dit zorgt ervoor dat de films die worden toegevoegd aan de favorieten ook daar blijven tot deze worden verwijderd. Om naar de favorieten te gaan heb ik een link naar de favorieten pagina gemaakt zodat je een lijst hebt van je favorieten lijst. -- [Page Transition API voor animaties tusse npagina's](https://developer.mozilla.org/en-US/docs/Web/API/Page_Transitions_API) -- [Web Animations API voor complexe animaties](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API) -- [Service Worker API voor installable web apps](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) -- [Web Push API voor push notifications](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) -- [Server sent events voor realtime functionaliteit](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) -- [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API) -- [Web Speech API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API) -- [Web Share API voor sharen van content binnen de context van de gebruiker](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share) -- ... +```js +// Pad naar de favorieten pagina +app.get('/favorites', async (req, res) => { + const ids = req.query.ids ? req.query.ids.split(',') : []; -De lijst is eindeloos, laat je vooral inspireren op de overzichtspagina van [MDN](https://developer.mozilla.org/en-US/docs/Web/API). + // Check of er geen favorieten zijn + if (!ids.length) { + return res.send(renderTemplate('server/views/favorites.liquid', { + title: 'Favorites', + items: [] + })); + } -## Beoordeling -De beoordelingscriteria zijn te vinden op [DLO](https://dlo.mijnhva.nl/d2l/le/content/609470/Home) + const items = []; -## Planning + // Haal de de details op van de films met de opgegeven ids + for (const id of ids) { + const url = `https://api.themoviedb.org/3/movie/${id}?api_key=${apiKey}&language=en-US`; + const response = await fetch(url); + const movie = await response.json(); -| Planning | Maandag | Dinsdag | Vrijdag | -|----------------------------|-----------------------|--------------------|---------------------------------------------| -| Week 1 - Kickoff & concept | Introductie ne uitleg | Workshops | Feedback gesprekken | -| Week 2 - The baseline | College + workshops | Workshops | Feedback gesprekken | -| Week 3 - Enhance | College + workshops | Workshops | Feedback gesprekken(*DONDERDAG*) | -| Week 4 - Enhance & wrap up | Tweede paasdag | Individuele vragen | Beoordelingsgesprekken(*DONDERDAG/VRIJDAG*) | + // voeg de films toe aan de items array + items.push(movie); + } -## Bronnen + // Render de template met de opgehaalde films + return res.send(renderTemplate('server/views/favorites.liquid', { + title: 'Favorites', + items + })); +}); +``` -- [Nodejs.org](https://nodejs.org/en/), voor de installatie van NodeJS op jouw systeem, kies voor NodeJS 22.13.1 Long Term Support. Dit is de meest stabiele versie van NodeJS, welke ondersteund wordt met goede documentatie. -- [VSCode How To Open Terminal](https://www.youtube.com/watch?v=OmQhOnBzg_k), om iemand de terminal te zien openen en gebruiken op Youtube. -- [Introduction to NodeJS](https://nodejs.dev/en/learn/), voor een in depth introductie met de NodeJS ontwikkelomgeving. Let op: dit is best een technisch verhaal. De eerste zes pagina’s zijn interessant. -- Om serverside te kunnen renderen maken we gebruik van [TinyHttp](https://github.com/tinyhttp). -- Voor templating maken we gebruik van [LiquidJS](https://liquidjs.com/). -- [Liquid Filters](https://liquidjs.com/filters/overview.html) -- Voor build tooling(CSS en JS) maken we gebruik [Vite](https://vitejs.dev/). -- [Using the Fetch API @ MDN](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) -- [JSON.parse() @ MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) -- [Partial commits in GitHub Desktop](https://github.blog/news-insights/product-news/partial-commits-in-github-for-windows/) -- [Committing and reviewing changes to your project in GitHub Desktop](https://docs.github.com/en/desktop/making-changes-in-a-branch/committing-and-reviewing-changes-to-your-project-in-github-desktop) +## Zoeken -## Inrichten ontwikkelomgeving +Om te kunnen zoeken wordt er getest of **search** in de URL zit. Als dit zo is wordt de API url veranderd zodat alle films worden opgehaald relevant zijn voor de zoekterm. -1. Navigeer naar [nodejs.org](https://nodejs.org/en/) en installeer de NodeJS ontwikkelomgeving. Kies voor _NodeJS 22.13.1 with long-term support_, download de benodigde bestanden en doorloop het installatieproces. +```js + const searchQuery = req.query.search; -2. Fork daarna [deze repository](https://github.com/cmda-minor-web/API-2425) en *clone* deze op jouw computer. + let apiUrl; -3. Open deze repository in je code editor. + // Als search in de URL zit + if (searchQuery) { + // Gebruik de zoekendpoint van de API + apiUrl = `https://api.themoviedb.org/3/search/movie?api_key=${apiKey}&language=en-US&page=${page}&query=${encodeURIComponent(searchQuery)}`; + } else { + // Standaard ontdekking API + const genreQuery = selectedGenre ? `&with_genres=${selectedGenre}` : ''; + apiUrl = `https://api.themoviedb.org/3/discover/movie?api_key=${apiKey}&language=en-US&page=${page}&sort_by=${sort}${genreQuery}`; + } +``` -4. Open de _Terminal_ in Visual Studio Code door de toetscombinatie `` ^` `` (control + `) te gebruiken. Er opent een terminalscherm in de hoofdmap van jouw project. +## CSS -5. Voer in de terminal het commando `npm install` uit, door het in te typen en op enter te drukken. Je gebruikt _NPM_, de _NodeJS Package Manager_ om alle _afhankelijkheden_ voor dit project te installeren. NPM is een veelgebruikte package manager in frontend land. Voor dit project gebruiken we _TinyHTTP_ (om een _server_ te maken) en _Liquid_ (om HTML te _renderen_). -- (Optioneel) Na de installatie is de map `node_modules` aangemaakt, en gevuld met allerlei _packages_. Scroll eens door deze map heen; vele honderden *open source* ontwikkelaars hebben de packages die je ziet gebouwd en die mag je gratis gebruiken. Ontwikkelen in NodeJS is *standing on the shoulders of giants*. +Alle `liquid` bestanden hebben een los CSS bestand zodat het compact en los van elkaar staat. De kaarten worden via een render template. -### Project starten en stoppen -Start het voorbeeldproject op door in de terminal het commando `npm run dev` uit te voeren. Als het goed is, komt een melding te staan over het opstarten van de server: `Server available on http://localhost:3000` — Open deze URL in je browser. Let op: Vite draait op een andere poort dan TinyHTTP, dus je moet de poort van TinyHTTP gebruiken: http://localhost:3000 +Om de css die ik wil aanroepen voor elk bestand zet ik een algemene class die alleen voorkomt bij die pagina. Dit zorgt ervoor dat de css alleen wordt aangeroepen als dit nodig is. -Als het werkt, zet je je server weer uit door in de terminal de toetscombinatie `^c` (control + c) in te voeren. Deze toetsencombinatie wordt in de terminal gebruikt om de huidige taak te stoppen en *controle* (vandaar de c) terug te krijgen van het programma. +```css +/* Detail pagina */ +.movieDetails {} +``` -- Optioneel: Volg het [NodeJS ‘Hello World’ voorbeeld](https://medium.com/@mohammedijas/hello-world-in-node-js-b333275ddc89) -- Optioneel, iets technischer: Lees de eerste vijf delen van [Introduction to Node](https://nodejs.dev/en/learn/) als je een meer in-depth introductie wilt met de NodeJS ontwikkelomgeving. +## Link +[Link naar project](tymonl.github.io/API-2425/) + +[Link naar website](https://api-2425-rpyo.onrender.com/) diff --git a/client/fonts.css b/client/fonts.css new file mode 100644 index 0000000..d1d98da --- /dev/null +++ b/client/fonts.css @@ -0,0 +1,31 @@ +/* Light Weight */ +@font-face { + font-family: "Poppins"; + src: local("Poppins"), url("/fonts/Quicksand-Light.ttf") format("truetype"); + font-weight: 300; + font-style: normal; +} + +/* Regular Weight */ +@font-face { + font-family: "Poppins"; + src: local("Poppins"), url("/fonts/Quicksand-Regular.ttf") format("truetype"); + font-weight: normal; + font-style: normal; +} + +/* Medium Weight */ +@font-face { + font-family: "Poppins"; + src: local("Poppins"), url("/fonts/Quicksand-Medium.ttf") format("truetype"); + font-weight: 500; /* Medium weight */ + font-style: normal; +} + +/* Bold Weight */ +@font-face { + font-family: "Poppins"; + src: local("Poppins"), url("/fonts/Quicksand-Bold.ttf") format("truetype"); + font-weight: bold; + font-style: normal; +} \ No newline at end of file diff --git a/client/index.css b/client/index.css index 31cbc4b..e067510 100644 --- a/client/index.css +++ b/client/index.css @@ -1,20 +1,70 @@ /* global styles */ @import 'reset.css'; -@import "typography.css"; +@import 'typography.css'; +@import 'fonts.css'; /*.layout and view styling */ @import '../server/layouts/base.css'; @import '../server/views/index.css'; +@import '../server/views/details.css'; /* component styling */ @import '../server/components/card/card.css'; +html { + scroll-behavior: smooth; +} + body { - color: black; - max-width: 1440px; + color: #fff; margin: 0 auto; } +* { + box-sizing: border-box; + font-family: "Quicksand", sans-serif; +} + main { padding: 1rem; } + +.container { + max-width: 1440px; +} + +:root { + view-transition-name: root; +} + +@view-transition { + navigation: auto; +} + +html::view-transition { + animation-duration: 0.4s; +} + +::view-transition-old(movie_*) { + opacity: 1; +} + +::view-transition-new(movie_*) { + opacity: 0; + animation: fadeIn 0.5s ease forwards; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.fade-transition { + opacity: 1; + transition: opacity 0.4s ease-in-out; +} + +.fade-transition.fade-out { + opacity: 0; +} + diff --git a/client/index.js b/client/index.js index 5f2dbf3..8831c40 100644 --- a/client/index.js +++ b/client/index.js @@ -1,3 +1,57 @@ import './index.css'; -console.log('Hello, world!'); +document.querySelectorAll('.card_image').forEach(card => { + card.addEventListener('click', (e) => { + e.preventDefault(); + + const movieId = card.getAttribute('id'); + const targetUrl = `/movies/${movieId}`; // adjust depending on your site + + if (document.startViewTransition) { + document.startViewTransition(() => { + window.location.href = targetUrl; + }); + } else { + window.location.href = targetUrl; + } + }); +}); + +var favoritesLink = document.querySelector('.goToFavorites'); + +if (favoritesLink) { + favoritesLink.addEventListener('click', () => { + window.location.href = '/favorites'; // no query needed anymore + }); +} + +document.addEventListener('DOMContentLoaded', async () => { + const btn = document.querySelector('.addToFavorites'); + if (!btn) return; + + const id = btn.dataset.id; + const icon = btn.querySelector('i'); + if (!id || !icon) return; + + // Get current favorite status from server + const response = await fetch(`/api/favorites/${id}`); + const data = await response.json(); + + if (data.isFavorite) { + icon.classList.remove('fa-regular'); + icon.classList.add('fa-solid'); + } + + btn.addEventListener('click', async () => { + const res = await fetch(`/api/favorites/${id}`, { method: 'POST' }); + const result = await res.json(); + + if (result.status === 'added') { + icon.classList.remove('fa-regular'); + icon.classList.add('fa-solid'); + } else { + icon.classList.remove('fa-solid'); + icon.classList.add('fa-regular'); + } + }); +}); diff --git a/client/reset.css b/client/reset.css index d9f27b5..caea9a1 100644 --- a/client/reset.css +++ b/client/reset.css @@ -2,7 +2,7 @@ v2.0 | 20110126 License: none (public domain) */ - +/* html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, @@ -23,6 +23,7 @@ time, mark, audio, video { font: inherit; vertical-align: baseline; } + */ /* HTML5 display-role reset for older browsers */ article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { diff --git a/favorites/favorites b/favorites/favorites new file mode 100644 index 0000000..b2686d2 --- /dev/null +++ b/favorites/favorites @@ -0,0 +1 @@ +["1197306","950387"] \ No newline at end of file diff --git a/fonts/Quicksand-Bold.ttf b/fonts/Quicksand-Bold.ttf new file mode 100644 index 0000000..0106805 Binary files /dev/null and b/fonts/Quicksand-Bold.ttf differ diff --git a/fonts/Quicksand-Light.ttf b/fonts/Quicksand-Light.ttf new file mode 100644 index 0000000..fcf86af Binary files /dev/null and b/fonts/Quicksand-Light.ttf differ diff --git a/fonts/Quicksand-Medium.ttf b/fonts/Quicksand-Medium.ttf new file mode 100644 index 0000000..afca2d9 Binary files /dev/null and b/fonts/Quicksand-Medium.ttf differ diff --git a/fonts/Quicksand-Regular.ttf b/fonts/Quicksand-Regular.ttf new file mode 100644 index 0000000..c03548a Binary files /dev/null and b/fonts/Quicksand-Regular.ttf differ diff --git a/fonts/Quicksand-SemiBold.ttf b/fonts/Quicksand-SemiBold.ttf new file mode 100644 index 0000000..83475ea Binary files /dev/null and b/fonts/Quicksand-SemiBold.ttf differ diff --git a/images/noImage.jpg b/images/noImage.jpg new file mode 100644 index 0000000..0817846 Binary files /dev/null and b/images/noImage.jpg differ diff --git a/package-lock.json b/package-lock.json index ce70d77..8bda0fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "chokidar-cli": "^3.0.0", "dotenv": "^16.4.7", "liquidjs": "^10.21.0", + "node-localstorage": "^3.0.5", "nodemon": "^3.1.9", "npm-run-all": "^4.1.5", "sirv": "^3.0.1", @@ -1834,6 +1835,14 @@ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==" }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/internal-slot": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", @@ -2289,6 +2298,17 @@ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, + "node_modules/node-localstorage": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/node-localstorage/-/node-localstorage-3.0.5.tgz", + "integrity": "sha512-GCwtK33iwVXboZWYcqQHu3aRvXEBwmPkAMRBLeaX86ufhqslyUkLGsi4aW3INEfdQYpUB5M9qtYf3eHvAk2VBg==", + "dependencies": { + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/nodemon": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", @@ -3021,7 +3041,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "engines": { "node": ">=14" }, @@ -3609,6 +3628,18 @@ "node": ">=8" } }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", diff --git a/package.json b/package.json index 84461e9..b7adfab 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "chokidar-cli": "^3.0.0", "dotenv": "^16.4.7", "liquidjs": "^10.21.0", + "node-localstorage": "^3.0.5", "nodemon": "^3.1.9", "npm-run-all": "^4.1.5", "sirv": "^3.0.1", diff --git a/server/components/card/card.css b/server/components/card/card.css index 5be8712..978c572 100644 --- a/server/components/card/card.css +++ b/server/components/card/card.css @@ -1,10 +1,59 @@ +.grid__item { + background-color: #fff; + box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; + border-radius: 10px; + + a { + text-decoration: none; + } +} + +.movie__link { + + &:hover img, + &:focus img { + scale: 1.05; + } +} + .card { width: 100%; max-width: 300px; -} + display: grid; + overflow: hidden; + border-radius: 10px; + height: 100%; + position: relative; -.card__image { - width: 100%; - height: auto; - margin-top: 1rem; -} + .card__image { + width: 100%; + height: auto; + border-radius: 10px 10px 0 0; + transition: all 0.3s ease-out; + } + + h2 { + color: #fff; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + transition: all 0.3s ease-out; + background: linear-gradient(to top, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.5)); + margin: 0; + padding: 10px; + opacity: 0; + } + + *:not(.card__image, h2) { + padding: 10px; + margin: 0; + } + + &:hover { + h2 { + transition: all 0.3s ease-out; + opacity: 1; + } + } +} \ No newline at end of file diff --git a/server/components/card/card.liquid b/server/components/card/card.liquid index c1706e4..b3811e7 100644 --- a/server/components/card/card.liquid +++ b/server/components/card/card.liquid @@ -1,4 +1,9 @@ -
-

{{ item.name }}

- {{ item.image.alt }} -
+
+ {{ item.title }} +

{{ item.title }}

+
diff --git a/server/layouts/base.css b/server/layouts/base.css index 6efd0ce..d6d92d1 100644 --- a/server/layouts/base.css +++ b/server/layouts/base.css @@ -1,7 +1,120 @@ +body { + display: grid; + grid-template-rows: auto 1fr auto; + color: #fff; +} + header { - padding: 1rem; + display: grid; + grid-template-columns: 1fr; + grid-template-rows: auto auto; + align-items: center; + grid-gap: 1em 0; + background-color: #212121; + color: #fff; + + a { + color: #fff + } + + .topbar { + background-color: #1db954; + padding: 1em; + + .container { + display: flex; + justify-content: space-between; + align-items: center; + + .search { + display: flex; + align-items: center; + margin-bottom: 0; + gap: 0; + + input { + margin-right: 0; + border-radius: 5px 0 0 5px; + } + + button { + border-radius: 0 5px 5px 0; + border: none; + background-color: #fff; + color: #1aa34a; + } + } + } + } + + .main .container { + display: flex; + justify-content: center; + align-items: center; + padding: 1em; + flex-wrap: wrap; + gap: 1rem; + + @media screen and (min-width: 568px) { + justify-content: space-between; + } + } +} + +main { +background-color: #333; + } footer { - padding: 1rem; + display: grid; + grid-template-columns: 1fr; + grid-template-rows: auto; + color: #fff; + + .main { + background-color: #b3b3b3; + } + + .footerBottom { + background: #212121; + + p { + text-align: center; + } + } + + a { + color: #fff; + transition: color 0.3s ease; + + &:hover { + color: #1db954; + } + } } + +button, +a[role="button"] { + padding: 0.5rem 1rem; + background-color: #1db954; + color: #fff; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s ease; + text-decoration: none; + + &:hover { + background-color: #1aa34a; + } +} + +input, +select { + padding: 0.5rem; + border: 1px solid #ccc; + border-radius: 5px; + max-width: 300px; + margin-right: 1rem; +} \ No newline at end of file diff --git a/server/layouts/base.liquid b/server/layouts/base.liquid index ecf0462..e621e14 100644 --- a/server/layouts/base.liquid +++ b/server/layouts/base.liquid @@ -1,11 +1,17 @@ - - + + + {% block head %} - {{ title }} + + + + + {{ title }} + {% endblock %} - + {% block styles %}{% endblock %} {% block scripts %}{% endblock %} @@ -13,13 +19,33 @@
- Deze header staat op elke pagina +
+
+

+ Tymo's Movie App +

+ + + + +
+
- {% block content %}{% endblock %} +
+ {% block content %}{% endblock %} +
diff --git a/server/server.js b/server/server.js index 144cbfe..27c9b98 100644 --- a/server/server.js +++ b/server/server.js @@ -3,50 +3,105 @@ import { App } from '@tinyhttp/app'; import { logger } from '@tinyhttp/logger'; import { Liquid } from 'liquidjs'; import sirv from 'sirv'; +import { LocalStorage } from 'node-localstorage'; -const data = { - 'beemdkroon': { - id: 'beemdkroon', - name: 'Beemdkroon', - image: { - src: 'https://i.pinimg.com/736x/09/0a/9c/090a9c238e1c290bb580a4ebe265134d.jpg', - alt: 'Beemdkroon', - width: 695, - height: 1080, - } - }, - 'wilde-peen': { - id: 'wilde-peen', - name: 'Wilde Peen', - image: { - src: 'https://mens-en-gezondheid.infonu.nl/artikel-fotos/tom008/4251914036.jpg', - alt: 'Wilde Peen', - width: 418, - height: 600, - } - } -} +const localStorage = new LocalStorage('./favorites'); -const engine = new Liquid({ - extname: '.liquid', -}); +// ApiKey and URL for The Movie Database API +const apiKey = process.env.movieDB_APIKey; +const apiUrl = 'https://api.themoviedb.org/3/discover/movie'; +const genreListUrl = `https://api.themoviedb.org/3/genre/movie/list?api_key=${apiKey}&language=en-US`; +const engine = new Liquid({ extname: '.liquid' }); const app = new App(); +app.use(logger()).use('/', sirv('dist')); -app - .use(logger()) - .use('/', sirv('dist')) - .listen(3000, () => console.log('Server available on http://localhost:3000')); +// Genre map to store genre names +let genreMap = {}; + +// Get the genre list from the API and store it in genreMap +async function fetchGenres() { + try { + const response = await fetch(genreListUrl); + const genreData = await response.json(); + genreMap = genreData.genres; // Store the list of genres + return genreMap; + } catch (error) { + console.error('Error fetching genres:', error); + return []; + } +} +async function fetchMovieData(page = 1, sort = 'popularity.desc', selected_genre = '') { + try { + // Use the genre and sort parameters to dynamically build the API URL + const genreQuery = selected_genre ? `&with_genres=${selected_genre}` : ''; + const sortQuery = sort ? `&sort_by=${sort}` : ''; + const url = `${apiUrl}?api_key=${apiKey}&language=en-US&page=${page}${genreQuery}${sortQuery}`; + + const response = await fetch(url); + const data = await response.json(); + return data; + } catch (error) { + console.error('Error fetching movie data:', error); + } +} + +// Route for the home page app.get('/', async (req, res) => { - return res.send(renderTemplate('server/views/index.liquid', { title: 'Home', items: Object.values(data) })); + const selectedGenre = req.query.genre ? parseInt(req.query.genre, 10) : ""; + const page = req.query.page ? parseInt(req.query.page, 10) : 1; + const sort = req.query.sort || 'popularity.desc'; + const searchQuery = req.query.search; + + let apiUrl; + + // Als search in de URL zit + if (searchQuery) { + // Gebruik de zoekendpoint van de API + apiUrl = `https://api.themoviedb.org/3/search/movie?api_key=${apiKey}&language=en-US&page=${page}&query=${encodeURIComponent(searchQuery)}`; + } else { + // Standaard ontdekking API + const genreQuery = selectedGenre ? `&with_genres=${selectedGenre}` : ''; + apiUrl = `https://api.themoviedb.org/3/discover/movie?api_key=${apiKey}&language=en-US&page=${page}&sort_by=${sort}${genreQuery}`; + } + + try { + const response = await fetch(apiUrl); + const movieData = await response.json(); + + const genreResponse = await fetch(`https://api.themoviedb.org/3/genre/movie/list?api_key=${apiKey}&language=en-US`); + const genreData = await genreResponse.json(); + + return res.send(renderTemplate('server/views/index.liquid', { + title: 'Home', + items: movieData.results, + genre_names: genreData.genres, + selected_genre: selectedGenre, + sort: sort, + page: page, + total_pages: movieData.total_pages, + search: searchQuery || '' + })); + } catch (error) { + console.error('Error fetching movie data:', error); + } }); -app.get('/plant/:id/', async (req, res) => { + +// Route for handling movie details page +app.get('/movie/:id/', async (req, res) => { const id = req.params.id; - const item = data[id]; + + // Fetch detailed movie data by ID + const movieDetailsUrl = `https://api.themoviedb.org/3/movie/${id}?api_key=${apiKey}&language=en-US`; + const response = await fetch(movieDetailsUrl); + const item = await response.json(); + + console.log(item); + if (!item) { - return res.status(404).send('Not found'); + return res.status(404).send('Movie not found'); } return res.send(renderTemplate('server/views/detail.liquid', { title: `Detail page for ${id}`, @@ -54,12 +109,76 @@ app.get('/plant/:id/', async (req, res) => { })); }); +// Favorieten pagina +app.get('/favorites', async (req, res) => { + const ids = getFavorites(); + + if (!ids.length) { + return res.send(renderTemplate('server/views/favorites.liquid', { + title: 'Favorites', + items: [] + })); + } + + const items = []; + for (const id of ids) { + const url = `https://api.themoviedb.org/3/movie/${id}?api_key=${apiKey}&language=en-US`; + const response = await fetch(url); + const movie = await response.json(); + items.push(movie); + } + + return res.send(renderTemplate('server/views/favorites.liquid', { + title: 'Favorites', + items + })); +}); + +// Function om Liquid template te renderen const renderTemplate = (template, data) => { const templateData = { NODE_ENV: process.env.NODE_ENV || 'production', ...data }; - return engine.renderFileSync(template, templateData); }; +// Fetch genres before starting the server +fetchGenres().then(() => { + app.listen(3000, () => console.log('Server available on http://localhost:3000')); +}); + + +// Favorites API + +// Helper +function getFavorites() { + return JSON.parse(localStorage.getItem('favorites') || '[]'); +} + +function saveFavorites(favorites) { + localStorage.setItem('favorites', JSON.stringify(favorites)); +} + +// Toggle favorite +app.post('/api/favorites/:id', async (req, res) => { + const id = req.params.id; + let favorites = getFavorites(); + + if (favorites.includes(id)) { + favorites = favorites.filter(f => f !== id); + saveFavorites(favorites); + return res.json({ status: 'removed' }); + } else { + favorites.push(id); + saveFavorites(favorites); + return res.json({ status: 'added' }); + } +}); + +// Check if a movie is in favorites +app.get('/api/favorites/:id', (req, res) => { + const id = req.params.id; + const favorites = getFavorites(); + res.json({ isFavorite: favorites.includes(id) }); +}); \ No newline at end of file diff --git a/server/views/detail.liquid b/server/views/detail.liquid index 45c94b4..06ad0d2 100644 --- a/server/views/detail.liquid +++ b/server/views/detail.liquid @@ -1,6 +1,83 @@ {% layout "server/layouts/base.liquid" %} {% block content %} -

{{ item.name }}

-{{ item.image.alt }} +
+
+
+ +

{{ item.title }}

+

{{ item.tagline }}

+
+ +
+ {{ item.title }} +
+ +
+

Description

+

{{ item.overview }}

+ + {% if item.homepage %} +

{{item.homepage}}

+ {% endif %} + +
+ +

Info

+

+ Release Date: + {{ item.release_date }} +

+ + {% assign full_stars = item.vote_average | divided_by: 2 | floor %} + {% assign half_star = item.vote_average | modulo: 2 %} + {% assign empty_stars = 5 | minus: full_stars | minus: half_star %} + +

+ Rating: + {% for i in (1..full_stars) %} + + {% endfor %} + {% if half_star > 0 %} + + {% endif %} + {% for i in (1..empty_stars) %} + + {% endfor %} + ({{ item.vote_count }}) +

+ + Genres: +
    + {% for genre in item.genres %} +
  • {{ genre.name }}
  • + {% endfor %} +
+
+ +
+ {% if item.production_companies %} +

Production company's

+
+ {% for company in item.production_companies %} +
+ {% if company.logo_path %} + {{ company.name }} + {% else %} + {{ company.name }} + {% endif %} +

{{ company.name }}

+
+ {% endfor %} +
+ {% endif %} + +
+
+
{% endblock %} diff --git a/server/views/details.css b/server/views/details.css new file mode 100644 index 0000000..a54f006 --- /dev/null +++ b/server/views/details.css @@ -0,0 +1,118 @@ +.movieDetails { + .detailsGrid { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: auto 1fr; + gap: 1em; + + .movieDetails__header { + grid-column: 1; + grid-row: 1; + } + + .movieDetails__image { + grid-column: 1; + grid-row: 2; + } + + .movieDetails__info { + grid-column: 1; + grid-row: 3; + } + + .movieDetails__extra { + grid-column: 1; + grid-row: 4; + } + + @media screen and (min-width: 768px) { + grid-template-columns: 1fr 2fr; + grid-template-rows: auto; + + .movieDetails__header { + grid-column: 2; + grid-row: 1; + } + + .movieDetails__image { + grid-column: 1; + grid-row: 1 / span 2; + } + + .movieDetails__info { + grid-column: 2; + grid-row: 2; + } + + .movieDetails__extra { + grid-column: 1 / span 2; + grid-row: 3; + } + } + } + + .movieDetails__info { + h3 { + margin-top: 0; + } + + .rating i { + color: #1db954; + } + + .genres { + padding: 0 1em; + margin: 0; + list-style: initial; + } + } + + .movieDetails__header { + position: relative; + + button { + position: absolute; + top: 0; + right: 0; + z-index: 10; + background: none; + border: none; + + i { color: #1db954; } + + &:hover i.fa-regular {font-weight: 700; } + &:hover i.fa-solid { font-weight: 400; } + } + + } + + .movieDetails__image { + img { + max-width: 100%; + height: auto; + border-radius: 5px; + } + } + + .movieDetails__extra { + .companys { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 1em; + } + + .company { + display: flex; + align-items: center; + gap: 0.5em; + padding: 0.5em; + background-color: #444; + border-radius: 5px; + + img { + width: 50px; + height: auto; + } + } + } +} \ No newline at end of file diff --git a/server/views/favorites.liquid b/server/views/favorites.liquid new file mode 100644 index 0000000..c3844df --- /dev/null +++ b/server/views/favorites.liquid @@ -0,0 +1,21 @@ +{% layout "server/layouts/base.liquid" %} + +{% block content %} + +

{{ title }}

+ +{% if items.length > 0 %} + +{% else %} +

Geen favorieten gevonden.

+{% endif %} + +{% endblock %} \ No newline at end of file diff --git a/server/views/index.css b/server/views/index.css index 055029e..a220238 100644 --- a/server/views/index.css +++ b/server/views/index.css @@ -1,5 +1,25 @@ .grid { display: grid; gap: 1rem; - grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); } + +.filters { + display: flex; + justify-content: flex-end; + align-items: center; + margin-bottom: 1rem; +} + +main .container > ul { + padding: 0; + margin: 0; +} + +.pagination { + display: flex; + justify-content: center; + align-items: center; + gap: 1rem; + margin-top: 2rem; +} \ No newline at end of file diff --git a/server/views/index.liquid b/server/views/index.liquid index 0dc52f4..ba733d7 100644 --- a/server/views/index.liquid +++ b/server/views/index.liquid @@ -4,14 +4,60 @@

{{ title }}

-