2025-02-28
Some checks failed
ci/woodpecker/push/publish_instable Pipeline failed

This commit is contained in:
gcch 2025-02-28 12:23:46 +01:00
commit 650f381148
29 changed files with 599 additions and 349 deletions

View file

@ -0,0 +1,10 @@
<script setup lang="ts">
import type { TmdbMovieSearchResponseResult } from "@/libs/apis/tmdb/schemas";
defineProps<{
entryId: number;
tmdbSearchData: TmdbMovieSearchResponseResult;
}>();
</script>
<template></template>

View file

@ -1,4 +1,5 @@
<script setup lang="ts">
import Search from "@/libs/search/search.ts";
import { Effect, pipe } from "effect";
import { useTemplateRef } from "vue";
import { useRouter } from "vue-router";
@ -21,15 +22,14 @@
toggleDialog();
};
const redirectToSearch = async (event: Event): void => {
const redirectToSearch = async (event: Event): Promise<void> => {
if (!form.value?.checkValidity()) return;
event.preventDefault();
await pipe(
Effect.fromNullable(form.value),
Effect.andThen((form: HTMLFormElement) => new FormData(form)),
Effect.andThen((formData: FormData) => new URLSearchParams(formData)),
Effect.andThen((searchParams: URLSearchParams) => Object.fromEntries(searchParams.entries())),
Effect.andThen((formData: FormData) => Search.formDataToRecord(formData)),
Effect.tap(query => router.push({ path: "/search", query })),
Effect.runPromise,
);

View file

@ -1,30 +1,59 @@
<script setup lang="ts">
import type { TmdbMovieSearchResponse } from "@/libs/apis/tmdb/schemas";
import type { MergedTmdbLocalData } from "@/libs/search/schemas";
import type { Values } from "@/libs/utils/types";
const { searchResults } = defineProps<{
searchResults: typeof TmdbMovieSearchResponse.Type.results | undefined;
}>();
import { tupleByTitle } from "@/libs/apis/tmdb/orders.ts";
import { Array as Arr, Match, pipe } from "effect";
import { computed, ref } from "vue";
const SORT_ORDERS = {
ORIGINAL: "original",
POPULARITY: "popularity",
RELEASE_DATE: "release_date",
TITLE: "title",
} as const;
const { searchData } = defineProps<{ searchData: Map<number, MergedTmdbLocalData> }>();
const sortOrder = ref<Values<typeof SORT_ORDERS>>(SORT_ORDERS.TITLE);
const sortedData = computed(() =>
pipe(
Array.from(searchData.entries()),
(result: [number, MergedTmdbLocalData][]) =>
Match.value(sortOrder.value).pipe(
Match.when(SORT_ORDERS.ORIGINAL, () => result),
Match.when(SORT_ORDERS.POPULARITY, () => Arr.sort(result, tupleByTitle)),
Match.when(SORT_ORDERS.RELEASE_DATE, () => Arr.sort(result, tupleByTitle)),
Match.when(SORT_ORDERS.TITLE, () => Arr.sort(result, tupleByTitle)),
Match.exhaustive,
),
)
);
console.debug(sortedData.value);
</script>
<template>
<p v-show="!searchResults || searchResults?.length === 0">/</p>
<table v-show="searchResults?.length">
<p v-show="!sortedData || sortedData?.length === 0">/</p>
<table v-show="sortedData?.length">
<thead>
<tr>
<th scope="col">Nom</th>
<th scope="col">Année</th>
<th scope="col">Popularité</th>
</tr>
</thead>
<tbody>
<tr
v-for="result in searchResults" :key="result.id" class="row-link"
role="button"
v-for="result in sortedData" :key="result[0]" class="row-link"
:data-artwork-id="result[1].artWorkId" :data-entry-id="result[1].entryId" :data-tmdb-id="result[0]"
tabindex="0"
>
<th class="name" scope="row">
{{ result.original_title }}
{{ result[1].original_title }}
</th>
<td class="release-date">{{ result.release_date }}</td>
<td class="release-date">{{ result[1].release_date }}</td>
<td class="popularite">{{ result[1].popularity }}</td>
</tr>
</tbody>
</table>
@ -44,7 +73,7 @@
}
thead {
border-block-end: 1px solid var(--color-primary);
border-block-end: 1px solid var(--root-text-color);
tr > * + * {
padding-inline-start: var(--s-2);
@ -71,13 +100,13 @@
cursor: pointer;
&:hover {
background: var(--color-primary);
color: var(--color-secondary);
background: var(--root-text-color);
color: var(--root-background-color);
}
&:active {
outline: 1px solid var(--color-secondary);
outline-offset: -0.1rem;
outline: 2px solid var(--root-background-color);
outline-offset: -0.3rem;
}
}
</style>

View file

@ -1,25 +1,15 @@
CREATE TABLE `art_works` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`medium_type_id` integer,
`name` text NOT NULL,
`release_date` text(10) NOT NULL,
FOREIGN KEY (`medium_type_id`) REFERENCES `media_types`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `diary_entries` (
`art_work_id` integer,
`art_work_id` integer NOT NULL,
`date_created` text(10) NOT NULL,
`date_modified` text(10) NOT NULL,
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`state_id` integer,
`user_id` integer,
`state_id` integer NOT NULL,
FOREIGN KEY (`art_work_id`) REFERENCES `art_works`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`state_id`) REFERENCES `diary_entries_states`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
FOREIGN KEY (`state_id`) REFERENCES `diary_entries_states`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `diary_entries_genres` (
`art_work_id` integer,
`art_work_id` integer NOT NULL,
`genre_id` integer,
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
FOREIGN KEY (`art_work_id`) REFERENCES `art_works`(`id`) ON UPDATE no action ON DELETE no action,
@ -32,6 +22,25 @@ CREATE TABLE `diary_entries_states` (
);
--> statement-breakpoint
CREATE UNIQUE INDEX `diary_entries_states_state_unique` ON `diary_entries_states` (`state`);--> statement-breakpoint
CREATE TABLE `viewings` (
`art_work_id` integer NOT NULL,
`date` text(10) NOT NULL,
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
FOREIGN KEY (`art_work_id`) REFERENCES `art_works`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `art_works` (
`cover_path` text,
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`medium_type_id` integer NOT NULL,
`name` text NOT NULL,
`release_date` text(10) NOT NULL,
`tmdb_id` integer NOT NULL,
FOREIGN KEY (`medium_type_id`) REFERENCES `media_types`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE UNIQUE INDEX `art_works_cover_path_unique` ON `art_works` (`cover_path`);--> statement-breakpoint
CREATE UNIQUE INDEX `art_works_tmdb_id_unique` ON `art_works` (`tmdb_id`);--> statement-breakpoint
CREATE TABLE `genres` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
@ -47,20 +56,4 @@ CREATE TABLE `media_types` (
);
--> statement-breakpoint
CREATE UNIQUE INDEX `media_types_name_unique` ON `media_types` (`name`);--> statement-breakpoint
CREATE UNIQUE INDEX `media_types_slug_unique` ON `media_types` (`slug`);--> statement-breakpoint
CREATE TABLE `users` (
`email` text NOT NULL,
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint
CREATE UNIQUE INDEX `users_name_unique` ON `users` (`name`);--> statement-breakpoint
CREATE TABLE `viewings` (
`art_work_id` integer,
`date` text(10) NOT NULL,
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`user_id` integer,
FOREIGN KEY (`art_work_id`) REFERENCES `art_works`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
CREATE UNIQUE INDEX `media_types_slug_unique` ON `media_types` (`slug`);

View file

@ -1,45 +1,9 @@
{
"version": "6",
"dialect": "sqlite",
"id": "7f64c80e-6fd1-4f8c-859c-dac137206238",
"id": "da9e1cf6-aba7-4b5a-a839-3b0fe3dce876",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"art_works": {
"name": "art_works",
"columns": {
"id": { "name": "id", "type": "integer", "primaryKey": true, "notNull": true, "autoincrement": true },
"medium_type_id": {
"name": "medium_type_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"name": { "name": "name", "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false },
"release_date": {
"name": "release_date",
"type": "text(10)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"art_works_medium_type_id_media_types_id_fk": {
"name": "art_works_medium_type_id_media_types_id_fk",
"tableFrom": "art_works",
"tableTo": "media_types",
"columnsFrom": ["medium_type_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"diary_entries": {
"name": "diary_entries",
"columns": {
@ -47,7 +11,7 @@
"name": "art_work_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"notNull": true,
"autoincrement": false
},
"date_created": {
@ -64,19 +28,18 @@
"notNull": true,
"autoincrement": false
},
"id": { "name": "id", "type": "integer", "primaryKey": true, "notNull": true, "autoincrement": true },
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"state_id": {
"name": "state_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"notNull": true,
"autoincrement": false
}
},
@ -86,8 +49,12 @@
"name": "diary_entries_art_work_id_art_works_id_fk",
"tableFrom": "diary_entries",
"tableTo": "art_works",
"columnsFrom": ["art_work_id"],
"columnsTo": ["id"],
"columnsFrom": [
"art_work_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
@ -95,17 +62,12 @@
"name": "diary_entries_state_id_diary_entries_states_id_fk",
"tableFrom": "diary_entries",
"tableTo": "diary_entries_states",
"columnsFrom": ["state_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"diary_entries_user_id_users_id_fk": {
"name": "diary_entries_user_id_users_id_fk",
"tableFrom": "diary_entries",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"columnsFrom": [
"state_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
@ -121,7 +83,7 @@
"name": "art_work_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"notNull": true,
"autoincrement": false
},
"genre_id": {
@ -131,7 +93,13 @@
"notNull": false,
"autoincrement": false
},
"id": { "name": "id", "type": "integer", "primaryKey": true, "notNull": true, "autoincrement": true }
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
}
},
"indexes": {},
"foreignKeys": {
@ -139,8 +107,12 @@
"name": "diary_entries_genres_art_work_id_art_works_id_fk",
"tableFrom": "diary_entries_genres",
"tableTo": "art_works",
"columnsFrom": ["art_work_id"],
"columnsTo": ["id"],
"columnsFrom": [
"art_work_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
@ -148,8 +120,12 @@
"name": "diary_entries_genres_genre_id_genres_id_fk",
"tableFrom": "diary_entries_genres",
"tableTo": "genres",
"columnsFrom": ["genre_id"],
"columnsTo": ["id"],
"columnsFrom": [
"genre_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
@ -161,13 +137,27 @@
"diary_entries_states": {
"name": "diary_entries_states",
"columns": {
"id": { "name": "id", "type": "integer", "primaryKey": true, "notNull": true, "autoincrement": true },
"state": { "name": "state", "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false }
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"state": {
"name": "state",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"diary_entries_states_state_unique": {
"name": "diary_entries_states_state_unique",
"columns": ["state"],
"columns": [
"state"
],
"isUnique": true
}
},
@ -176,54 +166,6 @@
"uniqueConstraints": {},
"checkConstraints": {}
},
"genres": {
"name": "genres",
"columns": {
"id": { "name": "id", "type": "integer", "primaryKey": true, "notNull": true, "autoincrement": true },
"name": { "name": "name", "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false },
"slug": { "name": "slug", "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false }
},
"indexes": {
"genres_name_unique": { "name": "genres_name_unique", "columns": ["name"], "isUnique": true },
"genres_slug_unique": { "name": "genres_slug_unique", "columns": ["slug"], "isUnique": true }
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"media_types": {
"name": "media_types",
"columns": {
"id": { "name": "id", "type": "integer", "primaryKey": true, "notNull": true, "autoincrement": true },
"name": { "name": "name", "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false },
"slug": { "name": "slug", "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false }
},
"indexes": {
"media_types_name_unique": { "name": "media_types_name_unique", "columns": ["name"], "isUnique": true },
"media_types_slug_unique": { "name": "media_types_slug_unique", "columns": ["slug"], "isUnique": true }
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"email": { "name": "email", "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false },
"id": { "name": "id", "type": "integer", "primaryKey": true, "notNull": true, "autoincrement": true },
"name": { "name": "name", "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false }
},
"indexes": {
"users_email_unique": { "name": "users_email_unique", "columns": ["email"], "isUnique": true },
"users_name_unique": { "name": "users_name_unique", "columns": ["name"], "isUnique": true }
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"viewings": {
"name": "viewings",
"columns": {
@ -231,17 +173,22 @@
"name": "art_work_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"notNull": true,
"autoincrement": false
},
"date": { "name": "date", "type": "text(10)", "primaryKey": false, "notNull": true, "autoincrement": false },
"id": { "name": "id", "type": "integer", "primaryKey": true, "notNull": true, "autoincrement": true },
"user_id": {
"name": "user_id",
"type": "integer",
"date": {
"name": "date",
"type": "text(10)",
"primaryKey": false,
"notNull": false,
"notNull": true,
"autoincrement": false
},
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
}
},
"indexes": {},
@ -250,17 +197,12 @@
"name": "viewings_art_work_id_art_works_id_fk",
"tableFrom": "viewings",
"tableTo": "art_works",
"columnsFrom": ["art_work_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"viewings_user_id_users_id_fk": {
"name": "viewings_user_id_users_id_fk",
"tableFrom": "viewings",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"columnsFrom": [
"art_work_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
@ -268,10 +210,189 @@
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"art_works": {
"name": "art_works",
"columns": {
"cover_path": {
"name": "cover_path",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"medium_type_id": {
"name": "medium_type_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"release_date": {
"name": "release_date",
"type": "text(10)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"tmdb_id": {
"name": "tmdb_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"art_works_cover_path_unique": {
"name": "art_works_cover_path_unique",
"columns": [
"cover_path"
],
"isUnique": true
},
"art_works_tmdb_id_unique": {
"name": "art_works_tmdb_id_unique",
"columns": [
"tmdb_id"
],
"isUnique": true
}
},
"foreignKeys": {
"art_works_medium_type_id_media_types_id_fk": {
"name": "art_works_medium_type_id_media_types_id_fk",
"tableFrom": "art_works",
"tableTo": "media_types",
"columnsFrom": [
"medium_type_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"genres": {
"name": "genres",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"slug": {
"name": "slug",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"genres_name_unique": {
"name": "genres_name_unique",
"columns": [
"name"
],
"isUnique": true
},
"genres_slug_unique": {
"name": "genres_slug_unique",
"columns": [
"slug"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"media_types": {
"name": "media_types",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"slug": {
"name": "slug",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"media_types_name_unique": {
"name": "media_types_name_unique",
"columns": [
"name"
],
"isUnique": true
},
"media_types_slug_unique": {
"name": "media_types_slug_unique",
"columns": [
"slug"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": { "schemas": {}, "tables": {}, "columns": {} },
"internal": { "indexes": {} }
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View file

@ -1,5 +1,13 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [{ "idx": 0, "version": "6", "when": 1739953332871, "tag": "0000_perfect_justice", "breakpoints": true }]
"entries": [
{
"idx": 0,
"version": "6",
"when": 1740666437095,
"tag": "0000_open_the_twelve",
"breakpoints": true
}
]
}

View file

@ -16,10 +16,12 @@ export const MediaTypes = table("media_types", {
});
export const ArtWorks = table("art_works", {
coverPath: t.text("cover_path").unique(),
id: t.integer("id").primaryKey({ autoIncrement: true }),
mediumTypeId: t.integer("medium_type_id").references((): AnySQLiteColumn => MediaTypes.id).notNull(),
name: t.text("name").notNull(),
releaseDate: t.text("release_date", { length: 10 }).notNull(),
tmdbId: t.integer("tmdb_id").notNull().unique(),
});
export const Genres = table("genres", {
@ -31,17 +33,28 @@ export const Genres = table("genres", {
// Schémas
export const MediaTypeSchema = Schema.Struct({
/** L'ID numérique du type de média. */
id: Schema.NonNegativeInt,
/** Le nom du type de média. */
name: Schema.NonEmptyString,
/** L'ID alphabétique du type de média. */
slug: Schema.Enums(MEDIA_TYPES),
});
export type MediaType = Schema.Schema.Type<typeof MediaTypeSchema>;
export const ArtWorkSchema = Schema.Struct({
/** Le chemin de l'image de la pochette de l'oeuvre d'art. */
coverPath: Schema.Union(Schema.String, Schema.Null),
/** L'ID numérique de l'ouvre d'art. */
id: Schema.NonNegativeInt,
/** L'ID numérique du type de l'oeuvre d'art. */
mediumTypeId: Schema.NonNegativeInt,
/** Le nom de l'oeuvre d'art. */
name: Schema.NonEmptyString,
/** La date de sortie de l'oeuvre d'art au format YYYY-MM-DD. */
releaseDate: Schema.String,
/** L'ID numérique de l'oeuvre sur la base de données TMDB. */
tmdbId: Schema.NonNegativeInt,
});
export type ArtWork = Schema.Schema.Type<typeof ArtWorkSchema>;

View file

@ -0,0 +1,22 @@
import type { MergedTmdbLocalData } from "@/libs/search/schemas";
import { Order } from "effect";
import type { TmdbMovieSearchResponseResult } from "./schemas";
export const byTitle = Order.mapInput(
Order.string,
(tmdbEntry: TmdbMovieSearchResponseResult) => tmdbEntry.original_title,
);
export const byReleaseDate = Order.mapInput(
Order.string,
(tmdbEntry: TmdbMovieSearchResponseResult) => tmdbEntry.release_date,
);
// Tuples
export const tupleByTitle = Order.mapInput(
Order.string,
(data: [number, MergedTmdbLocalData]) => data[1].title,
);

View file

@ -25,8 +25,9 @@ export class TmdbMovieSearchQueryParams extends Schema.Class<TmdbMovieSearchQuer
}) {}
// Réponse
export class TmdbMovieSearchResponseResults
extends Schema.Class<TmdbMovieSearchResponseResults>("TmdbMovieSearchResponseResults")({
export class TmdbMovieSearchResponseResult
extends Schema.Class<TmdbMovieSearchResponseResult>("TmdbMovieSearchResponseResult")({
adult: Schema.Boolean,
backdrop_path: Schema.Union(Schema.String, Schema.Null),
genre_ids: Schema.Array(Schema.NonNegativeInt),
@ -46,7 +47,7 @@ export class TmdbMovieSearchResponseResults
export class TmdbMovieSearchResponse extends Schema.Class<TmdbMovieSearchResponse>("TmdbMovieSearchResponse")({
page: Schema.NonNegativeInt,
results: Schema.Array(TmdbMovieSearchResponseResults),
results: Schema.Array(TmdbMovieSearchResponseResult),
total_pages: Schema.NonNegativeInt,
total_results: Schema.NonNegativeInt,
}) {}

View file

@ -6,3 +6,21 @@ export class SearchPageQueryParams extends Schema.Class<SearchPageQueryParams>("
type: Schema.Enums(MEDIA_TYPES),
year: Schema.String,
}) {}
export class MergedTmdbLocalData extends Schema.Class<MergedTmdbLocalData>("MergedTmdbLocalData")({
artWorkCoverPath: Schema.Union(Schema.String, Schema.Null),
artWorkId: Schema.NonNegativeInt.pipe(Schema.optional),
artWorkMediumTypeId: Schema.NonNegativeInt.pipe(Schema.optional),
entryDateCreated: Schema.String.pipe(Schema.optional),
entryDateModified: Schema.String.pipe(Schema.optional),
entryId: Schema.NonNegativeInt.pipe(Schema.optional),
entryStateId: Schema.NonNegativeInt.pipe(Schema.optional),
genre_ids: Schema.Array(Schema.NonNegativeInt),
original_language: Schema.String,
original_title: Schema.String,
overview: Schema.String,
popularity: Schema.Number,
release_date: Schema.String,
title: Schema.String,
tmdbId: Schema.NonNegativeInt,
}) {}

10
src/libs/search/types.d.ts vendored Normal file
View file

@ -0,0 +1,10 @@
import type { ArtWork, DiaryEntry } from "@/db/schemas";
import type { TmdbMovieSearchResponseResult } from "../apis/tmdb/schemas";
/** Page de réponse de l'API TMDB avec les données locales correspondantes. */
export interface TmdbDataWithLocalData {
artWork?: ArtWork;
entry?: DiaryEntry;
tmdbData: TmdbMovieSearchResponseResult;
}

View file

@ -13,3 +13,5 @@ export const singleResultOrFail = <A, E>(orFail: () => E) =>
Effect.mapError(_ => orFail()),
)
);
export const getOrUndefined = <A, E, R>(a: Effect.Effect<A, E, R>) => Effect.orElseSucceed(a, () => undefined);

View file

@ -1,7 +1,6 @@
<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";
@ -11,47 +10,39 @@
import {
TmdbMovieSearchQueryParams,
TmdbMovieSearchResponse,
TmdbMovieSearchResponseResults,
TmdbMovieSearchResponseResult,
} from "@/libs/apis/tmdb/schemas.ts";
import { SearchPageQueryParams } from "@/libs/search/schemas.ts";
import { MergedTmdbLocalData, SearchPageQueryParams } from "@/libs/search/schemas.ts";
import Search from "@/libs/search/search.ts";
import { getCurrentYear } from "@/libs/utils/dates.ts";
import { getOrUndefined } from "@/libs/utils/effects.ts";
import { PrettyLogger } from "@/services/logger.ts";
import { ReadApi } from "@/services/read-api.ts";
import { RuntimeClient } from "@/services/runtime-client.ts";
import { TmdbApi } from "@/services/tmdb-api.ts";
import { Effect, pipe, Schema } from "effect";
import { computed, onMounted } from "vue";
import { ref } from "vue";
import { watch } from "vue";
import { useTemplateRef } from "vue";
import { useRoute } from "vue-router";
import { useRouter } from "vue-router";
/*
* Le formulaire reçoit en valeurs initiales les paramètres de l'URL.
* Quand le formulaire est soumis, les paramètres de l'URL sont mis à jour.
* 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.
*/
import { Array as Arr, Effect, pipe, Schema } from "effect";
import { computed, onMounted, ref, useTemplateRef, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
// États
const route = useRoute();
const router = useRouter();
/** L'année courante pour la limite supérieure du champs Année de la recherché. */
const currentYear: number = getCurrentYear();
const route = useRoute();
const router = useRouter();
/** Effet des paramètres validés de la route. */
const routeQueryParams = computed(() => Schema.decodeUnknown(SearchPageQueryParams)(route.query));
/** Le formulaire de recherche. */
const form = useTemplateRef("form");
/** Les valeurs du formulaire de recherche. */
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 | undefined> = ref<TmdbMovieSearchResponse>();
/** Les résultats de la requête de recherche de films auprès de l'API TMDB. */
const searchResults: ComputedRef<readonly TmdbMovieSearchResponseResults[] | undefined> = computed(() =>
search.value?.results
);
const searchData: Ref<Map<number, MergedTmdbLocalData>> = ref(new Map<number, MergedTmdbLocalData>());
/** État du chargement de la requête auprès de l'API TMDB. */
const isLoading: Ref<boolean> = ref(false);
@ -143,6 +134,51 @@
// Cycles
watch(search, async (): Promise<void> => {
await RuntimeClient.runPromise(Effect.gen(function*() {
searchData.value.clear();
const results = search.value?.results ?? [];
const readApi = yield* ReadApi;
void results.map((result: TmdbMovieSearchResponseResult) =>
Effect.gen(function*() {
const entry = yield* pipe(
readApi.getEntryByTmdbId(result.id),
effect => getOrUndefined(effect),
);
const artWork = yield* pipe(
readApi.getArtworkByTmdbId(result.id),
effect => getOrUndefined(effect),
);
searchData.value.set(
result.id,
yield* Schema.decodeUnknown(MergedTmdbLocalData)(
{
artWorkCoverPath: result.poster_path,
artWorkId: artWork?.id,
artWorkMediumTypeId: artWork?.mediumTypeId,
entryDateCreated: entry?.dateCreated,
entryDateModified: entry?.dateModified,
entryId: entry?.id,
entryStateId: entry?.stateId,
genre_ids: result.genre_ids,
original_language: result.original_language,
original_title: result.original_title,
overview: result.overview,
popularity: result.popularity,
release_date: result.release_date,
title: result.title,
tmdbId: result.id,
} satisfies MergedTmdbLocalData,
),
);
}).pipe(Effect.runPromise)
);
}));
});
watch(route, async (): Promise<void> => {
await updateSearchResults();
}, { immediate: true });
@ -215,7 +251,7 @@
<LoadingMessage v-if="isLoading">Récupération des résultats</LoadingMessage>
<ErrorMessage v-if="isErrored">{{ message }}</ErrorMessage>
<TmdbSearchResults v-else :search-results="searchResults"></TmdbSearchResults>
<TmdbSearchResults v-else :search-data="searchData"></TmdbSearchResults>
</section>
</div>
</template>

View file

@ -1,6 +1,6 @@
import type { SQLocalDrizzle } from "sqlocal/drizzle";
import v0000 from "@/db/drizzle/0000_perfect_justice.sql?raw";
import v0000 from "@/db/drizzle/0000_open_the_twelve.sql?raw";
import { Data, Effect } from "effect";
import { LocalSqlite } from "./db";

View file

@ -1,6 +1,6 @@
import { DiaryEntries } from "@/db/schemas";
import { ArtWorks, DiaryEntries } from "@/db/schemas";
import { singleResultOrFail } from "@/libs/utils/effects";
import { desc } from "drizzle-orm";
import { desc, eq } from "drizzle-orm";
import { Data, Effect } from "effect";
import { LocalSqlite } from "./db";
@ -14,10 +14,37 @@ export class ReadApi extends Effect.Service<ReadApi>()("ReadApi", {
yield* Effect.logDebug("--- READ-API ---");
// TODO: Implémenter ReadApiError pour des requêtes avec zéro retour.
return {
getAllEntries: () => query(_ => _.select().from(DiaryEntries)),
getArtworkByTmdbId: (tmdbId: number) =>
query(_ =>
_.select()
.from(ArtWorks)
.where(eq(ArtWorks.tmdbId, tmdbId))
.limit(1)
).pipe(
singleResultOrFail(() => new ReadApiError({ cause: "Aucune oeuvre ne dispose de cet ID TMDB." })),
),
getEntryByTmdbId: (tmdbId: number) =>
query(_ =>
_.select()
.from(DiaryEntries)
.leftJoin(ArtWorks, eq(ArtWorks.tmdbId, DiaryEntries.artWorkId))
.where(eq(ArtWorks.tmdbId, tmdbId))
.limit(1)
).pipe(
singleResultOrFail(() => new ReadApiError({ cause: "Aucune entrée n'est liée à cet ID TMDB." })),
Effect.andThen(result => result.diary_entries),
),
getLastAddedEntry: () =>
query(_ => _.select().from(DiaryEntries).limit(1).orderBy(desc(DiaryEntries.dateCreated))).pipe(
query(_ =>
_.select()
.from(DiaryEntries)
.limit(1)
.orderBy(desc(DiaryEntries.dateCreated))
).pipe(
singleResultOrFail(() => new ReadApiError({ cause: "Aucune entrée n'a encore été ajoutée." })),
),
};

View file

@ -1,42 +1,42 @@
html {
box-sizing: border-box;
block-size: 100%;
text-size-adjust: none;
text-size-adjust: none;
text-size-adjust: none;
tab-size: 2;
color-scheme: dark light;
interpolate-size: allow-keywords;
-moz-text-size-adjust: none;
-webkit-text-size-adjust: none;
text-size-adjust: none;
block-size: 100%;
}
body {
-webkit-font-smoothing: antialiased;
accent-color: var(--color-tertiary);
background: var(--color-secondary);
block-size: 100%;
font-family: monospace, system-ui, sans-serif;
font-kerning: normal;
font-optical-sizing: auto;
font-kerning: normal;
font-variant-ligatures: common-ligatures no-discretionary-ligatures no-historical-ligatures
contextual;
-webkit-font-smoothing: antialiased;
line-height: var(--line-height-comfortable);
text-decoration-skip-ink: auto;
text-rendering: geometricprecision;
block-size: 100%;
accent-color: var(--color-tertiary);
background: var(--color-secondary);
}
*, *::before, *::after {
box-sizing: inherit;
border: 0 solid transparent;
background-repeat: no-repeat;
background-origin: border-box;
margin: 0;
padding: 0;
border: 0 solid transparent;
font: inherit;
font-feature-settings: inherit;
font-variation-settings: inherit;
color: inherit;
letter-spacing: inherit;
word-spacing: inherit;
background-repeat: no-repeat;
background-origin: border-box;
}
:where(.visually-hidden:not(:focus, :active, :focus-within)) {
@ -48,7 +48,7 @@ body {
clip-path: inset(50%);
}
:where([hidden]) {
:where([hidden]), :where([aria-hidden="true"]) {
display: none;
}

View file

@ -1,29 +1,40 @@
/* Élément conteneur de la fenêtre modale. */
.dialog {
--dialog-overlay-background: var(--bg25-secondary);
--dialog-background-color: var(--root-background-color);
--dialog-border-color: var(--root-text-color);
--dialog-shadow-color: var(--root-text-color);
--dialog-heading-font-size: var(--s1);
position: fixed;
z-index: 2;
inset: 0;
display: flex;
margin: 0;
background: var(--bg25-secondary);
background: var(--dialog-overlay-background);
&[aria-hidden="true"] {
&[aria-hidden] {
display: none;
}
/* Fenêtre à proprement parler. */
.dialog-content {
--layout-box-padding: 0;
position: relative;
margin: auto;
border: 1px solid var(--color-primary);
background-color: var(--color-secondary);
box-shadow: 0.5rem 0.5rem 0 0 var(--color-primary);
border: 1px solid var(--dialog-border-color);
background-color: var(--dialog-background-color);
box-shadow: 0.5rem 0.5rem 0 0 var(--dialog-shadow-color);
/* Barre d'en-tête. */
header {
overflow: hidden;
display: flex;
flex-flow: row nowrap;
align-items: center;
justify-content: space-between;
border-block-end: 1px solid var(--color-primary);
border-block-end: 1px solid var(--dialog-border-color);
h2 {
padding-inline: var(--s-1);
@ -37,11 +48,12 @@
}
}
/* Contenu de la fenêtre. */
main {
display: flex;
flex-flow: column nowrap;
align-items: start;
padding: var(--s0);
padding: var(--s1);
}
}
}

View file

@ -1,5 +1,5 @@
.box {
padding: var(--s0, 1rem);
outline: 0.125rem solid transparent; /* 1 */
outline-offset: -0.125rem; /* 1 */
padding: var(--layout-box-padding);
outline: 0.125rem solid transparent;
outline-offset: -0.125rem;
}

View file

@ -7,11 +7,10 @@
--brkly-font-weight-regular: 100;
--brkly-font-weight-semibold: 120;
--banquise-font-weight: 400;
--root-background-color: var(--color-secondary);
--root-text-color: var(--color-primary);
--root-font-weight: var(--brkly-font-weight-regular);
--layout-box-padding: var(--s0);
--layout-center-max-width: 80rem;
--layout-center-inline-padding: var(--s0);
--layout-cluster-gap: var(--s0);
@ -23,8 +22,8 @@
body {
font-family: BRKLY, sans-serif;
color: var(--root-text-color);
font-weight: var(--root-font-weight);
color: var(--root-text-color);
background-color: var(--root-background-color);
}
@ -50,8 +49,9 @@ body {
}
.container {
--max-width: 100%;
--space: var(--s1);
--layout-center-max-width: 100%;
--layout-center-inline-padding: var(--s1);
--layout-sidebar-last-child-basis: 8rem;
place-content: start;
place-items: start;
@ -64,7 +64,7 @@ body {
margin-block-start: var(--s2);
}
main {
.container > main {
inline-size: 100%;
> header {

View file

@ -56,11 +56,12 @@ button {
/* 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;
box-shadow: initial;
&:hover {
--button-border-color: var(--root-text-color);

View file

@ -1,5 +1,5 @@
:root {
--headings-font-family: Banquise, monospace;
--headings-font-family: banquise, monospace;
}
h1 {

View file

@ -1,5 +1,5 @@
a {
&[aria-current] {
&[aria-current="true"] {
font-weight: var(--brkly-font-weight-semibold);
}
}