index.vue 7.46 KB
<script lang="ts" setup>
import type { WhereSecondaryClosure } from "pinia-orm";
import type { CreditsResponse } from "~/interfaces/credit";
import type { MovieInterface } from "~/interfaces/movie";
import type { MovieCommentInterface } from "~/interfaces/movieComment";
// #region --import--.
import { AlertTriangleIcon, ArrowLeftIcon } from "lucide-vue-next";
import { computed, onMounted, ref } from "vue";
import { useTMDB } from "~/composables/tMDB";
import { Credit } from "~/models/credit";
import { Movie } from "~/models/movie";
import { MovieComment } from "~/models/movieComment";
// #endregion

// #region --Declaration--.
const { fetchMovieDetails, fetchMovieCredits } = useTMDB();
// #endregion

// #region --Declaration--.
const { currentRoute } = useRouter();
// #endregion

// #region --Data/ref--.
const isLoading = ref(true);
const isSubmitting = ref(false);
// #endregion

// #region --Computed--.
const movieId = computed(() => {
  if (!currentRoute.value.params.id) return null;
  if (typeof currentRoute.value.params.id !== "string") return null;

  if (typeof Number(+currentRoute.value.params.id) === "number") return +currentRoute.value.params.id as number;
  return currentRoute.value.params.id as string;
});

const movie = computed(() => {
  if (!unref(movieId)) return null;
  return useRepo(Movie)
    .query()
    .where("id", movieId.value as WhereSecondaryClosure<never> | null | undefined)
    .withAll()
    .first() as unknown as MovieInterface;
});

/**
 * Computed property for director
 */
const director = computed(() => {
  if (!unref(movie)?.credit?.crew) return null;
  return movie.value?.credit.crew.find(person => person.job === "Director");
});

/**
 * Retourne les commentaires liés au film, du plus récent au plus ancien.
 */
const comments = computed(() => {
  return useRepo(MovieComment)
    .query()
    .where((comment) => {
      const searched = comment as unknown as MovieCommentInterface;
      return searched.movie_id === unref(movieId);
    })
    .orderBy("createdAt", "desc")
    .get();
});
// #endregion

// #region --Function--.
/**
 * Fetch movie details
 */
async function fetchDetails(id: number | string) {
  try {
    isLoading.value = true;

    const data = await fetchMovieDetails(id);
    // Add to store collection.
    useRepo(Movie).save(data);
  }
  catch (error) {
    throw new Error(`Error fetching movie details: ${error}`);
  }
  finally {
    isLoading.value = false;
  }
}

/**
 * Format runtime
 * @param minutes
 */
function formatRuntime(minutes: number) {
  if (!minutes) return "Durée inconnue";
  // Find nb hours.
  const hours = Math.floor(minutes / 60);
  // Find last minutes.
  const mins = minutes % 60;

  return `${hours}h ${mins}min`;
}

async function fetchCredits(id: number | string) {
  try {
    const data = (await fetchMovieCredits(id)) as CreditsResponse;
    data.movie_id = id;
    // Add to store collection.
    useRepo(Credit).save(data);
  }
  catch (error) {
    throw new Error(`Error fetching movie credits: ${error}`);
  }
}

// Ce n'est pas le film du siècle cependant, il est suffisamment intéressant pour passer une bonne soirée !
function handleSubmitEvent(event: MovieCommentInterface) {
  isSubmitting.value = true;
  event.movie_id = unref(movieId);
  event.createdAt = `${new Date(Date.now())}`;
  useRepo(MovieComment).save(event);
  isSubmitting.value = false;
}

// #endregion

// #region --Global event--.
onMounted(() => {
  // Fetch data on component mount.
  if (unref(movieId)) {
    const id = unref(movieId) as string | number;
    fetchDetails(id);
    fetchCredits(id);
  }
  // loadComments()
});
// #endregion
</script>

<template>
  <section>
    <!-- Skeleton loader pendant le chargement -->
    <ui-components-skeleton-movie-detail-loader v-if="isLoading" />

    <!-- Contenu du film -->
    <div
      v-else-if="movie"
      class="relative"
    >
      <!-- Backdrop image -->
      <ui-components-backdrop-image
        v-if="movie.backdrop_path"
        :src="movie.backdrop_path"
        :title="movie.title"
      />

      <!-- Contenu principal -->
      <div class="container mx-auto px-4 py-8 relative z-10 pt-20">
        <button
          class="flex items-center text-gray-400 hover:text-white mb-8 transition-colors"
          @click="navigateTo('/')"
        >
          <ArrowLeftIcon
            :size="20"
            class="mr-2"
          />
          Retour
        </button>

        <div class="flex flex-col md:flex-row gap-8">
          <!-- Poster -->
          <ui-components-poster
            v-if="movie.poster_path"
            :src="movie.poster_path"
            :title="movie.title"
          />

          <!-- Informations du film -->
          <section class="w-full md:w-2/3 lg:w-3/4">
            <h1 class="text-3xl md:text-4xl font-bold mb-2">
              {{ movie.title }}
            </h1>
            <p
              v-if="movie.release_date"
              class="text-gray-400 mb-4"
            >
              {{ useDateFormat(movie.release_date, "DD-MM-YYYY") }} • {{ formatRuntime(movie.runtime) }}
            </p>

            <!-- Note et votes -->
            <details-score-and-vote
              :nb-vote="movie.vote_count"
              :score="movie.vote_average"
            />

            <!-- Genres -->
            <details-movie-gender :genres="movie.genres" />

            <!-- Synopsis -->
            <div class="mb-6">
              <h2 class="text-xl font-bold mb-2">
                Synopsis
              </h2>
              <p class="text-gray-300">
                {{ movie.overview || "Aucun synopsis disponible." }}
              </p>
            </div>

            <!-- Réalisateur et têtes d'affiche -->
            <div
              v-if="movie.credit"
              class="mb-6"
            >
              <h2 class="text-xl font-bold mb-2">
                Équipe
              </h2>
              <div
                v-if="director"
                class="mb-2"
              >
                <span class="font-semibold">Réalisateur:</span> {{ director.name }}
              </div>
              <div v-if="movie.credit.cast.length > 0">
                <span class="font-semibold">Têtes d'affiche:</span>
                {{
                  movie.credit.cast
                    .slice(0, 15)
                    .map((person) => person.name)
                    .join(", ")
                }}
                <span v-if="movie.credit.cast.length > 15">..</span>
              </div>
            </div>
            <!--  Comments form.  -->
            <h3 class="text-xl font-bold mt-8 mb-4">
              Ajouter un commentaire
            </h3>
            <form-movie-comment-form @event-submit="handleSubmitEvent" />

            <!-- Liste des commentaires -->
            <movie-comment-list :comments="comments as unknown as MovieCommentInterface[]" />
          </section>
        </div>
      </div>
    </div>

    <!-- Erreur -->
    <section
      v-else
      class="container mx-auto px-4 py-16 text-center"
    >
      <AlertTriangleIcon
        :size="64"
        class="mx-auto mb-4 text-red-500"
      />
      <h2 class="text-2xl font-bold mb-2">
        Film non trouvé
      </h2>
      <p class="text-gray-400 mb-6">
        Nous n'avons pas pu trouver le film que vous cherchez.
      </p>
      <button
        class="px-6 py-2 bg-primary text-white font-bold rounded-md hover:bg-primary-dark transition-colors"
        @click="navigateTo('/')"
      >
        Retour à l'accueil
      </button>
    </section>
  </section>
</template>

<style scoped></style>