@@ -0,0 +1,16 @@ | |||||
kind: pipeline | |||||
type: docker | |||||
name: default | |||||
steps: | |||||
- name: deploy | |||||
image: docker:dind | |||||
commands: | |||||
- apk add --upgrade npm bash findutils rsync sed | |||||
- WORKDIR="/var/docker-web/apps/$DRONE_REPO_NAME" | |||||
- rm -rf $WORKDIR | |||||
- mkdir $WORKDIR | |||||
- rsync -av --exclude ./node_modules /drone/src/ $WORKDIR | |||||
- cd $WORKDIR | |||||
- npm ci | |||||
- bash /var/docker-web/src/cli.sh up $DRONE_REPO_NAME |
@@ -0,0 +1,7 @@ | |||||
SEARCH_URL=https://developer.themoviedb.org/reference/search-movie | |||||
DETAILS_URL=https://developer.themoviedb.org/reference/movie-details | |||||
KEY=62efff3ae6af25d45688f7991b826ca8 | |||||
TOKEN=eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2MmVmZmYzYWU2YWYyNWQ0NTY4OGY3OTkxYjgyNmNhOCIsIm5iZiI6MTcyODA1NTIwMy4wNzcyNywic3ViIjoiNjcwMDA2ZjQ5ZWJlYTE5MDA2ZjgxZmJhIiwic2NvcGVzIjpbImFwaV9yZWFkIl0sInZlcnNpb24iOjF9.XuH-_0UggmULCgQQajKc-QsmlRYW2rqSenyhguE6wRU | |||||
DOMAIN=tmdb.erudi.fr | |||||
PORT=7777 | |||||
DASHBOARD_HIDDEN=false |
@@ -0,0 +1,19 @@ | |||||
# Nuxt dev/build outputs | |||||
.output | |||||
.data | |||||
.nuxt | |||||
.nitro | |||||
.cache | |||||
dist | |||||
# Node dependencies | |||||
node_modules | |||||
# Logs | |||||
logs | |||||
*.log | |||||
# Misc | |||||
.DS_Store | |||||
.fleet | |||||
.idea |
@@ -0,0 +1 @@ | |||||
18.20.2 |
@@ -0,0 +1,19 @@ | |||||
# INSTALL | |||||
FROM node:18-alpine as builder | |||||
WORKDIR /app | |||||
COPY . . | |||||
RUN npm ci && npm cache clean --force | |||||
ADD . . | |||||
# BUILD | |||||
RUN npm run build | |||||
# PROD | |||||
FROM node:18-alpine | |||||
WORKDIR /app | |||||
COPY --from=builder /app/.output /app/.output | |||||
COPY --from=builder /app/.nuxt /app/.nuxt | |||||
COPY --from=builder /app/.env /app/.env | |||||
ENV HOST 0.0.0.0 | |||||
EXPOSE 3000 | |||||
CMD source .env && node .output/server/index.mjs |
@@ -0,0 +1,47 @@ | |||||
Test technique | |||||
Ce test a pour vocation à évaluer les compétences techniques et le savoir-faire sur la | |||||
technologie. La qualité du test sera l’aspect le plus important dans ce rendu. Toute prise | |||||
d’initiative supplémentaire sera évidemment prise en compte. En cas de doute sur certains | |||||
points de ce test, ne pas hésiter à nous contacter à it@hellocse.fr. | |||||
À l’aide de VueJS 3 vous allez devoir réaliser un interface de films de cinéma en utilisant | |||||
l’API TMDB : https://developer.themoviedb.org/reference/intro/getting-started. Vous devrez | |||||
réaliser 3 pages : | |||||
- Liste des films | |||||
- Ajouter un défilement infini | |||||
- Ajouter un filtre de recherche sur le nom du film | |||||
(https://developer.themoviedb.org/reference/search-movie) | |||||
- Lorsque l’on clique sur un film, on souhaite voir le détail des informations de | |||||
ce film. | |||||
- Détail d’un film avec les informations relatives à ce film | |||||
(https://developer.themoviedb.org/reference/movie-details) | |||||
- Les données du film à afficher sur cette pages sont : | |||||
- L’affiche, le titre, le synopsis, le réalisateur, les stars / têtes d’affiche, | |||||
les catégories, la note TMDB et le nombre de personnes ayant noté le | |||||
film. | |||||
- En dessous des informations du film, on souhaite pouvoir laisser des | |||||
commentaires. Réalisez un formulaire contenant les champs suivants (à | |||||
stocker dans le local storage) et listez les commentaires du plus récent au | |||||
plus ancien : | |||||
- Nom d’utilisateur | |||||
- Chaîne de caractères alphas | |||||
- Longueur minimum de 3 caractères | |||||
- Longueur maximum de 50 caractères | |||||
- Champs requis | |||||
- Message | |||||
- Chaîne de caractères alphanumérique | |||||
- Longueur minimum de 3 caractères | |||||
- Longueur maximum de 500 caractères | |||||
- Champs requis | |||||
- L’utilisation d’un Wysiwyg tel que TinyMCE est un plus | |||||
- Note du film | |||||
- Valeur numérique de 1 à 10 | |||||
Indications : | |||||
- Les variables doivent être typées et les SFC doivent être réalisés en utilisant la | |||||
Composition API et TypeScript. | |||||
- L’utilisation de NuxtJS, Vuetify, TailwindCSS, SCSS, Vuelidate, VueUse est un | |||||
plus | |||||
- L’installation d’outils de lint, de tests, de stores est un plus | |||||
- Une présentation graphique attirante contenant de possibles animations est un plus | |||||
- L’ajout de skeletons est un plus | |||||
- N’hésitez pas à commenter votre code et créer des commits sur Git au fur et à | |||||
mesure de votre progression |
@@ -0,0 +1,3 @@ | |||||
<template> | |||||
<NuxtPage /> | |||||
</template> |
@@ -0,0 +1,42 @@ | |||||
@tailwind base; | |||||
@tailwind components; | |||||
@tailwind utilities; | |||||
.button { | |||||
text-decoration: none; | |||||
box-shadow: 0 8px 0 0 black; | |||||
transition: all .3s; | |||||
border: 8px black solid; | |||||
line-height: 100%; | |||||
height: 70px; | |||||
width: 70px; | |||||
border-width: 2px; | |||||
border-radius: 100px; | |||||
cursor: pointer; | |||||
color: black; | |||||
font-size: 26px; | |||||
background-color: #ffffff59; | |||||
} | |||||
.button:hover { | |||||
background-color: #fdec50ff; | |||||
} | |||||
.button:active { | |||||
box-shadow: 0 0 0 0 black; | |||||
} | |||||
.button--close { | |||||
right: 24px; | |||||
padding-top: 10px; | |||||
position: absolute; | |||||
} | |||||
.button--screened { | |||||
top: 74px; | |||||
} | |||||
.compilation { | |||||
cursor: pointer; | |||||
max-width: 420px; | |||||
} |
@@ -0,0 +1,105 @@ | |||||
<template> | |||||
<section class="flex flex-col items-center min-h-screen"> | |||||
<div class="container w-full p-6 max-w-6xl grow flex flex-col items-center"> | |||||
<nav | |||||
class="w-full flex bg-slate-800 font-semibold rounded-full backdrop-blur sticky top-0 justify-center items-center hover:ring"> | |||||
<input ref="searchInput" v-model="terms" autofocus placeholder="search" type="search" | |||||
class="rounded-full text-xl w-full text-center text-white p-4 bg-transparent focus-visible:border-none" | |||||
@keypress.enter="search()"> | |||||
<b v-if="films.length" class="px-4 py-2 absolute right-2 block bg-green-400 text-slate-800 rounded-full"> | |||||
{{ films.length }} | |||||
</b> | |||||
</nav> | |||||
<NuxtLink v-for="(film, index) in films" :key="index" :href="film.title" :to="'/details/' + 'id'" | |||||
class="hover:bg-slate-200 w-full p-4 flex-col border-b-2 border-GREY-100"> | |||||
<img :src="imgUrl + film.poster_path" alt=""> | |||||
<span class="flex flex-row"> | |||||
{{ film.title }} | |||||
{{ film.original_title }} | |||||
{{ film.vote_count }} | |||||
{{ film.release_date }} | |||||
</span> | |||||
</NuxtLink> | |||||
</div> | |||||
</section> | |||||
</template> | |||||
<script setup lang="ts"> | |||||
const imgUrl = 'https://media.themoviedb.org/t/p/w220_and_h330_face/' | |||||
const terms = ref('') | |||||
const films = ref([]) | |||||
const search = async () => { | |||||
if (terms.value !== '') { | |||||
films.value = [] | |||||
const { data } = await useFetch(`/api/search/${terms.value}`) | |||||
films.value = data.value | |||||
} else { | |||||
initList() | |||||
} | |||||
} | |||||
const initList = async () => { | |||||
const { data } = await useFetch(`/api/list`) | |||||
films.value = data.value | |||||
} | |||||
onMounted(() => { | |||||
nextTick(() => { | |||||
initList() | |||||
}) | |||||
}) | |||||
</script> | |||||
<style> | |||||
.title b { | |||||
@apply text-green-400 capitalize; | |||||
} | |||||
.icon { | |||||
@apply w-4 h-4 mx-2; | |||||
} | |||||
.icon-container { | |||||
color: #8094ae; | |||||
@apply flex items-center | |||||
} | |||||
.icon-container .icon { | |||||
color: rgb(109, 109, 109); | |||||
} | |||||
.icon-size p { | |||||
color: rgb(54, 150, 75); | |||||
} | |||||
.icon-seed p { | |||||
color: rgb(12, 108, 233); | |||||
} | |||||
.loader>div { | |||||
animation: loader 2.4s cubic-bezier(0, 0.2, 0.8, 1) infinite; | |||||
} | |||||
@keyframes loader { | |||||
0%, | |||||
100% { | |||||
animation-timing-function: cubic-bezier(0.5, 0, 1, 0.5); | |||||
} | |||||
0% { | |||||
transform: rotateY(0deg); | |||||
} | |||||
50% { | |||||
transform: rotateY(1800deg); | |||||
animation-timing-function: cubic-bezier(0, 0.5, 0.5, 1); | |||||
} | |||||
100% { | |||||
transform: rotateY(3600deg); | |||||
} | |||||
} | |||||
</style> |
@@ -0,0 +1,19 @@ | |||||
export function useMouse() { | |||||
// state encapsulated and managed by the composable | |||||
const x = ref(0) | |||||
const y = ref(0) | |||||
// a composable can update its managed state over time. | |||||
function update(event) { | |||||
x.value = event.pageX | |||||
y.value = event.pageY | |||||
} | |||||
// a composable can also hook into its owner component's | |||||
// lifecycle to setup and teardown side effects. | |||||
onMounted(() => window.addEventListener('mousemove', update)) | |||||
onUnmounted(() => window.removeEventListener('mousemove', update)) | |||||
// expose managed state as return value | |||||
return { x, y } | |||||
} |
@@ -0,0 +1,18 @@ | |||||
services: | |||||
tmdb: | |||||
build: . | |||||
container_name: tmdb | |||||
restart: unless-stopped | |||||
ports: | |||||
- $PORT:3000 | |||||
environment: | |||||
VIRTUAL_HOST: "${DOMAIN}" | |||||
LETSENCRYPT_HOST: "${DOMAIN}" | |||||
PUID: "${PUID}" | |||||
PGID: "${PGID}" | |||||
networks: | |||||
default: | |||||
name: dockerweb | |||||
external: true |
@@ -0,0 +1,21 @@ | |||||
// https://nuxt.com/docs/api/configuration/nuxt-config | |||||
export default defineNuxtConfig({ | |||||
devtools: { enabled: true }, | |||||
css: ['~/assets/css/main.css'], | |||||
postcss: { | |||||
plugins: { | |||||
tailwindcss: {}, | |||||
autoprefixer: {}, | |||||
}, | |||||
}, | |||||
app: { | |||||
head: { | |||||
charset: 'utf-8', | |||||
viewport: 'width=device-width, initial-scale=1', | |||||
} | |||||
}, | |||||
compatibilityDate: '2024-07-10' | |||||
}) |
@@ -0,0 +1,26 @@ | |||||
{ | |||||
"name": "nuxt-app", | |||||
"private": true, | |||||
"type": "module", | |||||
"scripts": { | |||||
"build": "nuxt build", | |||||
"dev": "nuxt dev --host", | |||||
"generate": "nuxt generate", | |||||
"preview": "nuxt preview", | |||||
"postinstall": "nuxt prepare" | |||||
}, | |||||
"dependencies": { | |||||
"axios": "^1.7.7", | |||||
"node-fetch": "^3.3.2", | |||||
"nuxt": "^3.12.3", | |||||
"unhead": "^1.9.15", | |||||
"vue": "^3.4.31", | |||||
"vue-router": "^4.4.0" | |||||
}, | |||||
"devDependencies": { | |||||
"autoprefixer": "^10.4.19", | |||||
"postcss": "^8.4.39", | |||||
"sass": "^1.77.6", | |||||
"tailwindcss": "^3.4.4" | |||||
} | |||||
} |
@@ -0,0 +1,12 @@ | |||||
<template> | |||||
<section> | |||||
{{ filmId }} | |||||
</section> | |||||
</template> | |||||
<script setup lang="ts"> | |||||
const route = useRoute() | |||||
const filmId = ref('') | |||||
filmId.value = route.params.id | |||||
</script> |
@@ -0,0 +1,5 @@ | |||||
<template> | |||||
<div> | |||||
<List /> | |||||
</div> | |||||
</template> |
@@ -0,0 +1,21 @@ | |||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 273.42 35.52"> | |||||
<defs> | |||||
<style> | |||||
.cls-1 { | |||||
fill: url(#linear-gradient); | |||||
} | |||||
</style> | |||||
<linearGradient id="linear-gradient" y1="17.76" x2="273.42" y2="17.76" gradientUnits="userSpaceOnUse"> | |||||
<stop offset="0" stop-color="#90cea1" /> | |||||
<stop offset="0.56" stop-color="#3cbec9" /> | |||||
<stop offset="1" stop-color="#00b3e5" /> | |||||
</linearGradient> | |||||
</defs> | |||||
<title>Asset 3</title> | |||||
<g id="Layer_2" data-name="Layer 2"> | |||||
<g id="Layer_1-2" data-name="Layer 1"> | |||||
<path class="cls-1" | |||||
d="M191.85,35.37h63.9A17.67,17.67,0,0,0,273.42,17.7h0A17.67,17.67,0,0,0,255.75,0h-63.9A17.67,17.67,0,0,0,174.18,17.7h0A17.67,17.67,0,0,0,191.85,35.37ZM10.1,35.42h7.8V6.92H28V0H0v6.9H10.1Zm28.1,0H46V8.25h.1L55.05,35.4h6L70.3,8.25h.1V35.4h7.8V0H66.45l-8.2,23.1h-.1L50,0H38.2ZM89.14.12h11.7a33.56,33.56,0,0,1,8.08,1,18.52,18.52,0,0,1,6.67,3.08,15.09,15.09,0,0,1,4.53,5.52,18.5,18.5,0,0,1,1.67,8.25,16.91,16.91,0,0,1-1.62,7.58,16.3,16.3,0,0,1-4.38,5.5,19.24,19.24,0,0,1-6.35,3.37,24.53,24.53,0,0,1-7.55,1.15H89.14Zm7.8,28.2h4a21.66,21.66,0,0,0,5-.55A10.58,10.58,0,0,0,110,26a8.73,8.73,0,0,0,2.68-3.35,11.9,11.9,0,0,0,1-5.08,9.87,9.87,0,0,0-1-4.52,9.17,9.17,0,0,0-2.63-3.18A11.61,11.61,0,0,0,106.22,8a17.06,17.06,0,0,0-4.68-.63h-4.6ZM133.09.12h13.2a32.87,32.87,0,0,1,4.63.33,12.66,12.66,0,0,1,4.17,1.3,7.94,7.94,0,0,1,3,2.72,8.34,8.34,0,0,1,1.15,4.65,7.48,7.48,0,0,1-1.67,5,9.13,9.13,0,0,1-4.43,2.82V17a10.28,10.28,0,0,1,3.18,1,8.51,8.51,0,0,1,2.45,1.85,7.79,7.79,0,0,1,1.57,2.62,9.16,9.16,0,0,1,.55,3.2,8.52,8.52,0,0,1-1.2,4.68,9.32,9.32,0,0,1-3.1,3A13.38,13.38,0,0,1,152.32,35a22.5,22.5,0,0,1-4.73.5h-14.5Zm7.8,14.15h5.65a7.65,7.65,0,0,0,1.78-.2,4.78,4.78,0,0,0,1.57-.65,3.43,3.43,0,0,0,1.13-1.2,3.63,3.63,0,0,0,.42-1.8A3.3,3.3,0,0,0,151,8.6a3.42,3.42,0,0,0-1.23-1.13A6.07,6.07,0,0,0,148,6.9a9.9,9.9,0,0,0-1.85-.18h-5.3Zm0,14.65h7a8.27,8.27,0,0,0,1.83-.2,4.67,4.67,0,0,0,1.67-.7,3.93,3.93,0,0,0,1.23-1.3,3.8,3.8,0,0,0,.47-1.95,3.16,3.16,0,0,0-.62-2,4,4,0,0,0-1.58-1.18,8.23,8.23,0,0,0-2-.55,15.12,15.12,0,0,0-2.05-.15h-5.9Z" /> | |||||
</g> | |||||
</g> | |||||
</svg> |
@@ -0,0 +1,14 @@ | |||||
import fetch from 'node-fetch' | |||||
export default eventHandler(async (req) => { | |||||
const terms = req.context.params?.terms || '' | |||||
const url = 'https://api.themoviedb.org/3/discover/movie?include_adult=false&include_video=false&language=en-US&page=1&sort_by=popularity.desc' | |||||
const response = await fetch(url, { | |||||
method: 'get', | |||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2MmVmZmYzYWU2YWYyNWQ0NTY4OGY3OTkxYjgyNmNhOCIsIm5iZiI6MTcyODA1NTIwMy4wNzcyNywic3ViIjoiNjcwMDA2ZjQ5ZWJlYTE5MDA2ZjgxZmJhIiwic2NvcGVzIjpbImFwaV9yZWFkIl0sInZlcnNpb24iOjF9.XuH-_0UggmULCgQQajKc-QsmlRYW2rqSenyhguE6wRU' } | |||||
}) | |||||
const data = await response.json() | |||||
return data.results | |||||
}) |
@@ -0,0 +1,27 @@ | |||||
import fetch from 'node-fetch' | |||||
interface Film { | |||||
title: string, | |||||
seed: number, | |||||
size: string, | |||||
age: string, | |||||
magnet: string, | |||||
provider: string, | |||||
} | |||||
let films: Film[] = [] | |||||
export default eventHandler(async (req) => { | |||||
const terms = req.context.params?.terms || '' | |||||
const url = `https://api.themoviedb.org/3/search/movie?query=${terms}&include_adult=false&language=en-US&page=1` | |||||
const response = await fetch(url, { | |||||
method: 'get', | |||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2MmVmZmYzYWU2YWYyNWQ0NTY4OGY3OTkxYjgyNmNhOCIsIm5iZiI6MTcyODA1NTIwMy4wNzcyNywic3ViIjoiNjcwMDA2ZjQ5ZWJlYTE5MDA2ZjgxZmJhIiwic2NvcGVzIjpbImFwaV9yZWFkIl0sInZlcnNpb24iOjF9.XuH-_0UggmULCgQQajKc-QsmlRYW2rqSenyhguE6wRU' } | |||||
}) | |||||
const data = await response.json() | |||||
return data.results | |||||
}) |
@@ -0,0 +1,3 @@ | |||||
{ | |||||
"extends": "../.nuxt/tsconfig.server.json" | |||||
} |
@@ -0,0 +1,19 @@ | |||||
/** @type {import('tailwindcss').Config} */ | |||||
module.exports = { | |||||
content: [ | |||||
"./components/**/*.{js,vue,ts}", | |||||
"./layouts/**/*.vue", | |||||
"./pages/**/*.vue", | |||||
"./plugins/**/*.{js,ts}", | |||||
"./app.vue", | |||||
"./error.vue", | |||||
], | |||||
theme: { | |||||
extend: { | |||||
colors: { | |||||
tmdbDarkBlue: '#272960', | |||||
}, | |||||
}, | |||||
}, | |||||
plugins: [], | |||||
} |
@@ -0,0 +1,4 @@ | |||||
{ | |||||
// https://nuxt.com/docs/guide/concepts/typescript | |||||
"extends": "./.nuxt/tsconfig.json" | |||||
} |