diff --git a/.dockerignore b/.dockerignore index bd28185..b653b8d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,6 +5,7 @@ .git .gitignore .jj +.woodpecker .zed Dockerfile* README.md @@ -12,4 +13,5 @@ cspell.json dist docker-compose* justfile +mise.toml node_modules diff --git a/.woodpecker/publish_instable.yaml b/.woodpecker/publish_instable.yaml new file mode 100644 index 0000000..2d04c31 --- /dev/null +++ b/.woodpecker/publish_instable.yaml @@ -0,0 +1,17 @@ +when: + - event: push + branch: instable + +steps: + - name: build_publish + image: woodpeckerci/plugin-kaniko:latest + pull: true + settings: + auto_tag: true + cache: true + registry: git.gcch.fr + repo: gcch/journal-media-vue + username: + from_secret: DOCKER_USER + password: + from_secret: DOCKER_PASSWORD diff --git a/.zed/settings.json b/.zed/settings.json index 4cebd4c..51d1869 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -20,15 +20,12 @@ "lsp": { "eslint": { "settings": { - "configFile": "./cfg/eslint.config.mts", "experimental": { "useFlatConfig": true }, - "options": { - "configFile": "./cfg/eslint.config.mts", - "overrideConfigFile": "./cfg/eslint.config.mts" - }, - "overrideConfigFile": "./cfg/eslint.config.mts" + "problems": { + "shortenToSingleLine": true + } } } } diff --git a/Dockerfile b/Dockerfile index 65c91c9..c438e4d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,12 @@ FROM oven/bun:slim AS base WORKDIR /usr/src/app -# Installe les dépendences. +# Installe les dépendances de développement. FROM base AS install RUN mkdir -p /temp/dev COPY package.json bun.lock /temp/dev/ RUN cd /temp/dev && bun install --frozen-lockfile - -# Installe les dépendences de production. +# Installe les dépendances de production. RUN mkdir -p /temp/prod COPY package.json bun.lock /temp/prod/ RUN cd /temp/prod && bun install --frozen-lockfile --production @@ -16,16 +15,15 @@ RUN cd /temp/prod && bun install --frozen-lockfile --production FROM base AS prerelease COPY --from=install /temp/dev/node_modules/ node_modules COPY . . - # Compile le projet. -ENV NODE_ENV production +ENV NODE_ENV=production RUN bun --bun vite build -# Créé le nécessaire pour Angie. +# Créé le nécessaire pour Angie, le proxy inversé servant l'application. FROM docker.angie.software/angie:minimal AS release COPY --from=prerelease /usr/src/app/dist/ /usr/share/angie/html/ COPY ./docker/default.conf /etc/angie/http.d/default.conf -EXPOSE 80 # Démarre Angie. +EXPOSE 80 CMD ["angie", "-g", "daemon off;"] diff --git a/cfg/dprint.json b/cfg/dprint.json index e552fee..1d81251 100644 --- a/cfg/dprint.json +++ b/cfg/dprint.json @@ -30,7 +30,7 @@ "hexCase": "lower", "hexColorLength": "short", "indentWidth": 2, - "keyframeSelectorNotation": "keyword", + "keyframeSelectorNotation": "percentage", "lineBreak": "lf", "linebreakInPseudoParens": true, "omitNumberLeadingZero": false, diff --git a/cfg/eslint.config.mts b/cfg/eslint.config.mts index bc08986..fffcb7c 100644 --- a/cfg/eslint.config.mts +++ b/cfg/eslint.config.mts @@ -65,5 +65,11 @@ export default defineConfigWithVueTs( "vue/v-for-delimiter-style": "off", }, }, + { + name: "ts/no-annoying-rules", + rules: { + "@typescript-eslint/no-misused-spread": "off", + }, + }, perfectionist.configs["recommended-natural"], ); diff --git a/eslint.config.mts b/eslint.config.mts new file mode 100644 index 0000000..fffcb7c --- /dev/null +++ b/eslint.config.mts @@ -0,0 +1,75 @@ +import { defineConfigWithVueTs, vueTsConfigs } from "@vue/eslint-config-typescript"; +import perfectionist from "eslint-plugin-perfectionist"; +import vue from "eslint-plugin-vue"; +import globals from "globals"; + +export default defineConfigWithVueTs( + { + files: ["**/*.{js,mjs,ts,mts,vue}"], + languageOptions: { ecmaVersion: "latest", globals: { ...globals.browser, ...globals.es2025 } }, + name: "app/files-to-lint", + }, + { ignores: [".cache/", "dist/", "node_modules/"], name: "app/files-to-ignore" }, + vueTsConfigs.strictTypeChecked, + vueTsConfigs.stylisticTypeChecked, + vue.configs["flat/recommended"], + { + name: "app/no-vue-formatting", + rules: { + "vue/array-bracket-newline": "off", + "vue/array-bracket-spacing": "off", + "vue/array-element-newline": "off", + "vue/arrow-spacing": "off", + "vue/attributes-order": ["error", { alphabetical: true }], + "vue/block-spacing": "off", + "vue/block-tag-newline": "off", + "vue/brace-style": "off", + "vue/comma-dangle": "off", + "vue/comma-spacing": "off", + "vue/comma-style": "off", + "vue/dot-location": "off", + "vue/first-attribute-linebreak": "off", + "vue/func-call-spacing": "off", + "vue/html-closing-bracket-newline": "off", + "vue/html-closing-bracket-spacing": "off", + "vue/html-comment-content-newline": "off", + "vue/html-comment-content-spacing": "off", + "vue/html-comment-indent": "off", + "vue/html-indent": "off", + "vue/html-quotes": "off", + "vue/html-self-closing": "off", + "vue/key-spacing": "off", + "vue/keyword-spacing": "off", + "vue/max-attributes-per-line": "off", + "vue/max-len": "off", + "vue/multiline-html-element-content-newline": "off", + "vue/multiline-ternary": "off", + "vue/new-line-between-multi-line-property": "off", + "vue/no-extra-parens": "off", + "vue/no-multi-spaces": "off", + "vue/no-spaces-around-equal-signs-in-attribute": "off", + "vue/object-curly-newline": "off", + "vue/object-curly-spacing": "off", + "vue/object-property-newline": "off", + "vue/operator-linebreak": "off", + "vue/padding-line-between-blocks": "off", + "vue/padding-line-between-tags": "off", + "vue/padding-lines-in-component-definition": "off", + "vue/quote-props": "off", + "vue/script-indent": "off", + "vue/singleline-html-element-content-newline": "off", + "vue/space-in-parens": "off", + "vue/space-infix-ops": "off", + "vue/space-unary-ops": "off", + "vue/template-curly-spacing": "off", + "vue/v-for-delimiter-style": "off", + }, + }, + { + name: "ts/no-annoying-rules", + rules: { + "@typescript-eslint/no-misused-spread": "off", + }, + }, + perfectionist.configs["recommended-natural"], +); diff --git a/justfile b/justfile index ba21e7f..fac9640 100755 --- a/justfile +++ b/justfile @@ -13,6 +13,7 @@ stylelintConfigFile := "cfg/stylelint.config.mjs" # Variables de cache. cacheFolder := ".cache" +esLintCacheFile := "eslintcache" prettierCacheFile := "prettiercache" stylelintCacheFile := "stylelintcache" @@ -80,7 +81,10 @@ lint-css: # Analyse le code TypeScript et Vue. lint-js fix="": - bun --bun eslint --config "{{ esLintConfigFile }}" {{ fix }} + bun --bun eslint \ + --cache --cache-location "{{ cacheFolder }}/{{ esLintCacheFile }}" \ + --config "{{ esLintConfigFile }}" \ + {{ fix }} # Analyse le code CSS avec ESLint. lint-css-eslint fix="": diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..590e100 --- /dev/null +++ b/mise.toml @@ -0,0 +1,3 @@ +[tools] +bun = "latest" +just = "latest" diff --git a/public/app-loading.css b/public/app-loading.css index b46e74c..7d11b5a 100644 --- a/public/app-loading.css +++ b/public/app-loading.css @@ -22,7 +22,7 @@ } @keyframes loading { - from { + 0% { content: ""; } 25% { @@ -34,7 +34,7 @@ 75% { content: "..."; } - to { + 100% { content: ""; } } diff --git a/src/components/TmdbSearchResults.vue b/src/components/TmdbSearchResults.vue new file mode 100644 index 0000000..acf108a --- /dev/null +++ b/src/components/TmdbSearchResults.vue @@ -0,0 +1,53 @@ + + + + Aucun résultat. + + + + Nom + Année + + + + + + {{ result.original_title }} + {{ result.release_date }} + + + + + + diff --git a/src/libs/apis/tmdb/constants.ts b/src/libs/apis/tmdb/constants.ts index 137a1a7..60b98ec 100644 --- a/src/libs/apis/tmdb/constants.ts +++ b/src/libs/apis/tmdb/constants.ts @@ -1,9 +1,9 @@ import type { TmdbMovieSearchQueryParams } from "./schemas"; -export const DEFAULT_SEARCH_MOVIE_PARAMS: TmdbMovieSearchQueryParams = { +export const DEFAULT_SEARCH_MOVIE_PARAMS = { include_adult: false, language: "fr", page: 1, query: "", region: "fr-FR", -}; +} satisfies TmdbMovieSearchQueryParams; diff --git a/src/libs/apis/tmdb/schemas.ts b/src/libs/apis/tmdb/schemas.ts index 640e7c7..3634d8b 100644 --- a/src/libs/apis/tmdb/schemas.ts +++ b/src/libs/apis/tmdb/schemas.ts @@ -1,5 +1,7 @@ import { Schema } from "effect"; +// Requête + export class TmdbMovieSearchQueryParams extends Schema.Class("TmdbMovieSearchArgs")({ include_adult: Schema.Boolean.pipe( Schema.propertySignature, @@ -22,26 +24,30 @@ export class TmdbMovieSearchQueryParams extends Schema.Class("TmdbMovieSearchResponse")({ page: Schema.NonNegativeInt, - results: Schema.Array( - Schema.Struct({ - adult: Schema.Boolean, - backdrop_path: Schema.Union(Schema.String, Schema.Null), - genre_ids: Schema.Array(Schema.NonNegativeInt), - id: Schema.NonNegativeInt, - original_language: Schema.String, - original_title: Schema.String, - overview: Schema.String, - popularity: Schema.Number, - poster_path: Schema.Union(Schema.String, Schema.Null), - release_date: Schema.String, - title: Schema.String, - video: Schema.Boolean, - vote_average: Schema.Number, - vote_count: Schema.NonNegativeInt, - }), - ), + results: Schema.Array(TmdbMovieSearchResponseResults), total_pages: Schema.NonNegativeInt, total_results: Schema.NonNegativeInt, }) {} + +export class TmdbMovieSearchResponseResults + extends Schema.Class("TmdbMovieSearchResponseResults")({ + adult: Schema.Boolean, + backdrop_path: Schema.Union(Schema.String, Schema.Null), + genre_ids: Schema.Array(Schema.NonNegativeInt), + id: Schema.NonNegativeInt, + original_language: Schema.String, + original_title: Schema.String, + overview: Schema.String, + popularity: Schema.Number, + poster_path: Schema.Union(Schema.String, Schema.Null), + release_date: Schema.String, + title: Schema.String, + video: Schema.Boolean, + vote_average: Schema.Number, + vote_count: Schema.NonNegativeInt, + }) +{} diff --git a/src/libs/search/utils.ts b/src/libs/search/search.ts similarity index 72% rename from src/libs/search/utils.ts rename to src/libs/search/search.ts index 8e229ff..888fc3f 100644 --- a/src/libs/search/utils.ts +++ b/src/libs/search/search.ts @@ -4,18 +4,17 @@ import { UrlParams } from "@effect/platform"; import { Effect, pipe } from "effect"; /** - * Transform les valeurs d'un `FormData` en `Record` trié. + * Transforme les valeurs d'un `FormData` en `Record` trié. * * @param formData Les valeurs d'un formulaire. * @returns Un `Effect` des valeurs. */ -export const transformFormDataToRecord = ( - formData: FormData, -): Effect.Effect | string>> => +const formDataToRecord = (formData: FormData): Effect.Effect | string>> => pipe( Effect.succeed(Array.from(formData.entries())), // @ts-expect-error -- Impossible de typer les valeurs de FormData comme string. Effect.andThen(formData => new URLSearchParams(formData)), + // La conversion en URLSearchParams permet de trier les entrées. Effect.andThen((urlSearchParams: URLSearchParams) => { urlSearchParams.sort(); return urlSearchParams; @@ -23,3 +22,5 @@ export const transformFormDataToRecord = ( Effect.andThen((urlSearchParams: URLSearchParams) => UrlParams.fromInput(urlSearchParams)), Effect.andThen((urlParams: UrlParams.UrlParams) => UrlParams.toRecord(urlParams)), ); + +export default { formDataToRecord }; diff --git a/src/pages/SearchPage.vue b/src/pages/SearchPage.vue index e65435b..57653bb 100644 --- a/src/pages/SearchPage.vue +++ b/src/pages/SearchPage.vue @@ -1,13 +1,17 @@
Aucun résultat.