This commit is contained in:
parent
a510899ff1
commit
ff2a3a25e5
21 changed files with 473 additions and 210 deletions
11
src/components/ErrorMessage.vue
Normal file
11
src/components/ErrorMessage.vue
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p class="error-message">
|
||||
<slot></slot>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
15
src/components/LoadingMessage.vue
Normal file
15
src/components/LoadingMessage.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p class="loading-message">
|
||||
<slot></slot>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.loading-message::after {
|
||||
content: "";
|
||||
animation: loading 2s both infinite;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<p v-show="!searchResults || searchResults?.length === 0">Aucun résultat.</p>
|
||||
<p v-show="!searchResults || searchResults?.length === 0">/</p>
|
||||
<table v-show="searchResults?.length">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -17,8 +17,13 @@
|
|||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr v-for="result in searchResults" :key="result.id">
|
||||
<th class="name" scope="row">{{ result.original_title }}</th>
|
||||
<tr
|
||||
v-for="result in searchResults" :key="result.id" class="row-link"
|
||||
role="button"
|
||||
>
|
||||
<th class="name" scope="row">
|
||||
{{ result.original_title }}
|
||||
</th>
|
||||
<td class="release-date">{{ result.release_date }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
@ -29,25 +34,50 @@
|
|||
table {
|
||||
border-collapse: collapse;
|
||||
|
||||
thead tr th:last-of-type, tr td:last-of-type {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:is(td, th) {
|
||||
padding: var(--s-4);
|
||||
padding: var(--s-3);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
thead {
|
||||
border-block-end: 1px solid var(--color-primary);
|
||||
|
||||
tr > * + * {
|
||||
padding-inline-start: var(--s-2);
|
||||
}
|
||||
}
|
||||
|
||||
thead th {
|
||||
font-weight: 120;
|
||||
font-size: var(--s0);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
tbody tr > * + * {
|
||||
padding-inline-start: var(--s-2);
|
||||
}
|
||||
}
|
||||
|
||||
.release-date {
|
||||
min-inline-size: 6rem;
|
||||
}
|
||||
|
||||
.row-link {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
&:active {
|
||||
outline: 1px solid var(--color-secondary);
|
||||
outline-offset: -0.1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export const createUrlWithParams = (stringUrl: string) => (params: Input) =>
|
|||
export const createGetHttpRequest = (url: URL): HttpClientRequest.HttpClientRequest =>
|
||||
pipe(
|
||||
HttpClientRequest.get(url),
|
||||
HttpClientRequest.bearerToken(import.meta.env["VITE_TMDB_API_KEY"]),
|
||||
HttpClientRequest.bearerToken(import.meta.env["VITE_TMDB_API_KEY"] ?? ""),
|
||||
HttpClientRequest.acceptJson,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import type { TmdbMovieSearchQueryParams } from "./schemas";
|
|||
|
||||
export const DEFAULT_SEARCH_MOVIE_PARAMS = {
|
||||
include_adult: false,
|
||||
language: "fr",
|
||||
language: "en",
|
||||
page: 1,
|
||||
query: "",
|
||||
region: "fr-FR",
|
||||
region: "en-US",
|
||||
} satisfies TmdbMovieSearchQueryParams;
|
||||
|
|
|
|||
|
|
@ -25,14 +25,6 @@ export class TmdbMovieSearchQueryParams extends Schema.Class<TmdbMovieSearchQuer
|
|||
}) {}
|
||||
|
||||
// Réponse
|
||||
|
||||
export class TmdbMovieSearchResponse extends Schema.Class<TmdbMovieSearchResponse>("TmdbMovieSearchResponse")({
|
||||
page: Schema.NonNegativeInt,
|
||||
results: Schema.Array(TmdbMovieSearchResponseResults),
|
||||
total_pages: Schema.NonNegativeInt,
|
||||
total_results: Schema.NonNegativeInt,
|
||||
}) {}
|
||||
|
||||
export class TmdbMovieSearchResponseResults
|
||||
extends Schema.Class<TmdbMovieSearchResponseResults>("TmdbMovieSearchResponseResults")({
|
||||
adult: Schema.Boolean,
|
||||
|
|
@ -51,3 +43,10 @@ export class TmdbMovieSearchResponseResults
|
|||
vote_count: Schema.NonNegativeInt,
|
||||
})
|
||||
{}
|
||||
|
||||
export class TmdbMovieSearchResponse extends Schema.Class<TmdbMovieSearchResponse>("TmdbMovieSearchResponse")({
|
||||
page: Schema.NonNegativeInt,
|
||||
results: Schema.Array(TmdbMovieSearchResponseResults),
|
||||
total_pages: Schema.NonNegativeInt,
|
||||
total_results: Schema.NonNegativeInt,
|
||||
}) {}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import type { NonEmptyArray } from "effect/Array";
|
||||
import type { Ref } from "vue";
|
||||
import type { ComputedRef } from "vue";
|
||||
|
||||
import ErrorMessage from "@/components/ErrorMessage.vue";
|
||||
import LoadingMessage from "@/components/LoadingMessage.vue";
|
||||
import TmdbSearchResults from "@/components/TmdbSearchResults.vue";
|
||||
import { MEDIA_TYPES } from "@/db/schemas/constants.ts";
|
||||
import { DEFAULT_SEARCH_MOVIE_PARAMS } from "@/libs/apis/tmdb/constants.ts";
|
||||
import { TmdbMovieSearchQueryParams, TmdbMovieSearchResponse } from "@/libs/apis/tmdb/schemas.ts";
|
||||
import {
|
||||
TmdbMovieSearchQueryParams,
|
||||
TmdbMovieSearchResponse,
|
||||
TmdbMovieSearchResponseResults,
|
||||
} from "@/libs/apis/tmdb/schemas.ts";
|
||||
import { SearchPageQueryParams } from "@/libs/search/schemas.ts";
|
||||
import Search from "@/libs/search/search.ts";
|
||||
import { getCurrentYear } from "@/libs/utils/dates.ts";
|
||||
|
|
@ -26,6 +33,8 @@
|
|||
* Un effet est déclenché à chaque mise à jour des paramètres d'URL : une nouvelle recherche est déclenchée et les résultats sont mis à jour.
|
||||
*/
|
||||
|
||||
// États
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
/** L'année courante pour la limite supérieure du champs Année de la recherché. */
|
||||
|
|
@ -36,13 +45,22 @@
|
|||
/** Le formulaire de recherche. */
|
||||
const form = useTemplateRef("form");
|
||||
/** Les valeurs du formulaire de recherche. */
|
||||
const searchFormData = ref<SearchPageQueryParams>();
|
||||
const searchFormData: Ref<SearchPageQueryParams | undefined> = ref<SearchPageQueryParams>();
|
||||
/** Le retour de la requête de recherche de films auprès de l'API TMDB. */
|
||||
const search = ref<TmdbMovieSearchResponse>();
|
||||
const search: Ref<TmdbMovieSearchResponse | undefined> = ref<TmdbMovieSearchResponse>();
|
||||
/** Les résultats de la requête de recherche de films auprès de l'API TMDB. */
|
||||
const searchResults = computed(() => search.value?.results);
|
||||
/** L'état de chargement de la requête auprès de l'API TMDB. */
|
||||
const loading: Ref<boolean> = ref(false);
|
||||
const searchResults: ComputedRef<readonly TmdbMovieSearchResponseResults[] | undefined> = computed(() =>
|
||||
search.value?.results
|
||||
);
|
||||
|
||||
/** État du chargement de la requête auprès de l'API TMDB. */
|
||||
const isLoading: Ref<boolean> = ref(false);
|
||||
/** Présence d'une erreur lors de la requête. */
|
||||
const isErrored: Ref<boolean> = ref(false);
|
||||
/** Message affiché à l'Utilisateur. */
|
||||
const message: Ref<string> = ref("");
|
||||
|
||||
// Fonctions
|
||||
|
||||
const updateUrlQuery = async (event?: Event): Promise<void> => {
|
||||
event?.preventDefault();
|
||||
|
|
@ -108,14 +126,14 @@
|
|||
pipe(
|
||||
routeQueryParams.value,
|
||||
Effect.tap(async (args: SearchPageQueryParams) => {
|
||||
loading.value = true;
|
||||
isLoading.value = true;
|
||||
|
||||
// Met à jour les valeurs du formulaire.
|
||||
searchFormData.value = { ...args };
|
||||
// Récupère les résultats d'une recherche avec les nouveaux termes.
|
||||
search.value = await getSearchResultsFromApi();
|
||||
|
||||
loading.value = false;
|
||||
isLoading.value = false;
|
||||
}),
|
||||
Effect.tapError(Effect.logError),
|
||||
Effect.ignore,
|
||||
|
|
@ -123,8 +141,9 @@
|
|||
Effect.runPromise,
|
||||
);
|
||||
|
||||
//
|
||||
watch(route, async () => {
|
||||
// Cycles
|
||||
|
||||
watch(route, async (): Promise<void> => {
|
||||
await updateSearchResults();
|
||||
}, { immediate: true });
|
||||
|
||||
|
|
@ -134,7 +153,7 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wrapper stack">
|
||||
<div class="search stack">
|
||||
<h2>Rechercher</h2>
|
||||
|
||||
<section id="search-terms" class="stack">
|
||||
|
|
@ -165,7 +184,7 @@
|
|||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="field stack">
|
||||
<div class="stack field">
|
||||
<label for="query">Titre</label>
|
||||
<input
|
||||
id="query" for="add-media-form" name="query"
|
||||
|
|
@ -173,7 +192,7 @@
|
|||
:value="searchFormData?.query"
|
||||
>
|
||||
</div>
|
||||
<div class="field stack">
|
||||
<div class="stack field">
|
||||
<label for="year">Année de sortie</label>
|
||||
<input
|
||||
id="year" for="add-media-form" :max="currentYear"
|
||||
|
|
@ -189,17 +208,20 @@
|
|||
</form>
|
||||
</section>
|
||||
|
||||
<section id="results" class="stack">
|
||||
<section id="search-results" class="stack">
|
||||
<h3>Résultats</h3>
|
||||
|
||||
<p v-if="loading" class="loading">Recherche en cours</p>
|
||||
<p class="small">Cliquer sur un résultat pour l'ajouter.</p>
|
||||
|
||||
<LoadingMessage v-if="isLoading">Récupération des résultats</LoadingMessage>
|
||||
<ErrorMessage v-if="isErrored">{{ message }}</ErrorMessage>
|
||||
<TmdbSearchResults v-else :search-results="searchResults"></TmdbSearchResults>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="css">
|
||||
.wrapper {
|
||||
.search {
|
||||
> * {
|
||||
--space: var(--s3);
|
||||
|
||||
|
|
@ -209,6 +231,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: var(--s-1);
|
||||
}
|
||||
|
||||
#search-media-form {
|
||||
padding: var(--s1);
|
||||
border: 4px double var(--color-primary);
|
||||
|
|
@ -218,9 +244,4 @@
|
|||
margin-block-start: var(--s1);
|
||||
}
|
||||
}
|
||||
|
||||
.loading::after {
|
||||
content: "";
|
||||
animation: loading 2s both infinite;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ const router: Router = createRouter({
|
|||
});
|
||||
|
||||
router.beforeEach((to, _): void => {
|
||||
console.debug("router - to", to);
|
||||
pipe(
|
||||
Option.liftPredicate(Predicate.isString)(to.meta.title),
|
||||
Option.getOrElse((): string => "???"),
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ body {
|
|||
*::selection {
|
||||
color: var(--color-secondary);
|
||||
background: var(--color-primary);
|
||||
font-weight: 120;
|
||||
}
|
||||
|
||||
/* TODO: Prendre en compte a11y-dialog */
|
||||
|
|
|
|||
|
|
@ -33,6 +33,15 @@
|
|||
inline-size: fit-content;
|
||||
}
|
||||
|
||||
:where(a) {
|
||||
text-decoration: underline dashed;
|
||||
text-decoration-skip-ink: all;
|
||||
|
||||
.external {
|
||||
text-decoration: underline solid;
|
||||
}
|
||||
}
|
||||
|
||||
/* Évite le dépassement des textes. */
|
||||
:where(p, h1, h2, h3, h4, h5, h6) {
|
||||
overflow-wrap: break-word;
|
||||
|
|
@ -98,10 +107,6 @@
|
|||
margin-block-end: var(--s0);
|
||||
}
|
||||
|
||||
:where(a) {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* Empêche les marqueurs de listes de modifier la hauteur de ligne sur Firefox.
|
||||
* https://danburzo.ro/notes/moz-bullet-font
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ h1 {
|
|||
}
|
||||
|
||||
.container {
|
||||
--max-width: 90rem;
|
||||
--max-width: 100%;
|
||||
--space: var(--s1);
|
||||
|
||||
place-content: start;
|
||||
|
|
|
|||
|
|
@ -52,4 +52,29 @@ button {
|
|||
transform: translateX(2px) translateY(2px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Bouton sous forme de lien. */
|
||||
&.button-link {
|
||||
--button-border-color: transparent;
|
||||
padding-block: var(--s-5);
|
||||
padding-inline: var(--s-4);
|
||||
box-shadow: initial;
|
||||
line-height: var(--line-height-comfortable);
|
||||
outline-offset: initial;
|
||||
|
||||
&:hover {
|
||||
--button-border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline-offset: initial;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: initial;
|
||||
border: 1px solid var(--color-primary);
|
||||
outline: 1px solid var(--color-secondary);
|
||||
outline-offset: -0.1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue