mirror of
				https://github.com/mmahdium/TBW.git
				synced 2025-11-04 04:28:13 +01:00 
			
		
		
		
	Improve and add some components
This commit is contained in:
		
							
								
								
									
										60
									
								
								src/components/ImageWithFallback.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/components/ImageWithFallback.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed, ref } from 'vue'
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  src: string
 | 
			
		||||
  alt: string
 | 
			
		||||
  size?: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  size: 'w300'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const imageLoadFailed = ref(false)
 | 
			
		||||
const loaded = ref(false)
 | 
			
		||||
 | 
			
		||||
const imageSource = computed(() => {
 | 
			
		||||
  if (!props.src) {
 | 
			
		||||
    return ''
 | 
			
		||||
  }
 | 
			
		||||
  return `https://image.tmdb.org/t/p/${props.size}${props.src}`
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <figure class="overflow-hidden flex items-center justify-center bg-gray-50">
 | 
			
		||||
    <span v-if="!loaded" class="loading loading-ring loading-lg text-primary"></span>
 | 
			
		||||
 | 
			
		||||
    <div v-else-if="imageLoadFailed" class="flex items-center justify-center">
 | 
			
		||||
      <svg
 | 
			
		||||
        xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
        fill="none"
 | 
			
		||||
        viewBox="0 0 24 24"
 | 
			
		||||
        stroke-width="1.5"
 | 
			
		||||
        stroke="currentColor"
 | 
			
		||||
        class="w-12 h-12 text-gray-300"
 | 
			
		||||
      >
 | 
			
		||||
        <path
 | 
			
		||||
          stroke-linecap="round"
 | 
			
		||||
          stroke-linejoin="round"
 | 
			
		||||
          d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z"
 | 
			
		||||
        />
 | 
			
		||||
      </svg>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <img
 | 
			
		||||
      v-show="loaded && !imageLoadFailed"
 | 
			
		||||
      :src="imageSource"
 | 
			
		||||
      :alt="props.alt"
 | 
			
		||||
      class="object-cover w-full h-full"
 | 
			
		||||
      @load="loaded = true"
 | 
			
		||||
      @error="
 | 
			
		||||
        () => {
 | 
			
		||||
          imageLoadFailed = true
 | 
			
		||||
          loaded = true
 | 
			
		||||
        }
 | 
			
		||||
      "
 | 
			
		||||
    />
 | 
			
		||||
  </figure>
 | 
			
		||||
</template>
 | 
			
		||||
							
								
								
									
										17
									
								
								src/components/LoadingSpinner.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/components/LoadingSpinner.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
interface Props {
 | 
			
		||||
  size?: 'xs' | 'sm' | 'md' | 'lg'
 | 
			
		||||
  color?: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  size: 'md',
 | 
			
		||||
  color: 'primary'
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <span 
 | 
			
		||||
    :class="`loading loading-ring loading-${props.size} text-${props.color}`"
 | 
			
		||||
  ></span>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -1,24 +1,16 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { useMediaStore } from '@/stores/media'
 | 
			
		||||
import { computed, ref } from 'vue'
 | 
			
		||||
import { computed } from 'vue'
 | 
			
		||||
import type { MediaType } from '@/types/Media'
 | 
			
		||||
import ImageWithFallback from './ImageWithFallback.vue'
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  media: MediaType
 | 
			
		||||
}>()
 | 
			
		||||
 | 
			
		||||
const store = useMediaStore()
 | 
			
		||||
const imageLoadFailed = ref(false)
 | 
			
		||||
const loaded = ref(false)
 | 
			
		||||
 | 
			
		||||
const alreadyAdded = computed(() => store.mediaList.some((media) => media.Id === props.media.Id))
 | 
			
		||||
 | 
			
		||||
const imageSource = computed(() => {
 | 
			
		||||
  if (!props.media.PosterPath) {
 | 
			
		||||
    return ''
 | 
			
		||||
  }
 | 
			
		||||
  return `https://image.tmdb.org/t/p/w300${props.media.PosterPath}`
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
@@ -30,40 +22,12 @@ const imageSource = computed(() => {
 | 
			
		||||
    <router-link
 | 
			
		||||
      :to="{ name: 'details', params: { type: props.media.MediaType, id: props.media.Id } }"
 | 
			
		||||
    >
 | 
			
		||||
      <figure class="overflow-hidden flex items-center justify-center aspect-2/3 bg-gray-50">
 | 
			
		||||
        <span v-if="!loaded" class="loading loading-ring loading-lg text-primary"></span>
 | 
			
		||||
 | 
			
		||||
        <div v-else-if="imageLoadFailed" class="flex items-center justify-center">
 | 
			
		||||
          <svg
 | 
			
		||||
            xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
            fill="none"
 | 
			
		||||
            viewBox="0 0 24 24"
 | 
			
		||||
            stroke-width="1.5"
 | 
			
		||||
            stroke="currentColor"
 | 
			
		||||
            class="w-12 h-12 text-gray-300"
 | 
			
		||||
          >
 | 
			
		||||
            <path
 | 
			
		||||
              stroke-linecap="round"
 | 
			
		||||
              stroke-linejoin="round"
 | 
			
		||||
              d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z"
 | 
			
		||||
            />
 | 
			
		||||
          </svg>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <img
 | 
			
		||||
          v-show="loaded && !imageLoadFailed"
 | 
			
		||||
          :src="imageSource"
 | 
			
		||||
          :alt="props.media.Title"
 | 
			
		||||
          class="object-cover w-full h-full transform transition-transform duration-500 hover:scale-105"
 | 
			
		||||
          @load="loaded = true"
 | 
			
		||||
          @error="
 | 
			
		||||
            () => {
 | 
			
		||||
              imageLoadFailed = true
 | 
			
		||||
              loaded = true
 | 
			
		||||
            }
 | 
			
		||||
          "
 | 
			
		||||
        />
 | 
			
		||||
      </figure>
 | 
			
		||||
      <ImageWithFallback
 | 
			
		||||
        :src="props.media.PosterPath"
 | 
			
		||||
        :alt="props.media.Title"
 | 
			
		||||
        size="w300"
 | 
			
		||||
        class="aspect-2/3 object-cover w-full h-full transform transition-transform duration-500 hover:scale-105"
 | 
			
		||||
      />
 | 
			
		||||
    </router-link>
 | 
			
		||||
 | 
			
		||||
    <!-- Body -->
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,11 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import type { MovieDetailsType } from '@/types/Movie'
 | 
			
		||||
import type { TvSeriesDetailsType } from '@/types/TvSeries'
 | 
			
		||||
import { computed, ref } from 'vue'
 | 
			
		||||
import { computed } from 'vue'
 | 
			
		||||
import { useMediaStore } from '@/stores/media'
 | 
			
		||||
import type { MediaType } from '@/types/Media'
 | 
			
		||||
import ImageWithFallback from './ImageWithFallback.vue'
 | 
			
		||||
import MediaTypeBadge from './MediaTypeBadge.vue'
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  type: 'movie' | 'tv'
 | 
			
		||||
@@ -12,7 +14,6 @@ const props = defineProps<{
 | 
			
		||||
  tvSeries?: TvSeriesDetailsType | null
 | 
			
		||||
}>()
 | 
			
		||||
 | 
			
		||||
const imageLoaded = ref(false)
 | 
			
		||||
const store = useMediaStore()
 | 
			
		||||
 | 
			
		||||
const alreadyAdded = computed(() =>
 | 
			
		||||
@@ -37,20 +38,16 @@ const alreadyAdded = computed(() =>
 | 
			
		||||
    v-motion-fade-visible-once
 | 
			
		||||
  >
 | 
			
		||||
    <!-- Poster -->
 | 
			
		||||
    <figure class="shrink-0">
 | 
			
		||||
      <span v-if="!imageLoaded" class="loading loading-ring loading-lg text-primary"></span>
 | 
			
		||||
      <img
 | 
			
		||||
        v-show="imageLoaded"
 | 
			
		||||
        :src="
 | 
			
		||||
          type === 'movie'
 | 
			
		||||
            ? 'https://image.tmdb.org/t/p/w500' + props.movie!.PosterPath
 | 
			
		||||
            : 'https://image.tmdb.org/t/p/w500' + props.tvSeries!.PosterPath
 | 
			
		||||
        "
 | 
			
		||||
        alt="Poster"
 | 
			
		||||
        class="w-full max-w-sm rounded-lg shadow-lg transform transition-transform duration-500 hover:scale-105"
 | 
			
		||||
        @load="imageLoaded = true"
 | 
			
		||||
      />
 | 
			
		||||
    </figure>
 | 
			
		||||
    <ImageWithFallback
 | 
			
		||||
      :src="
 | 
			
		||||
        type === 'movie'
 | 
			
		||||
          ? props.movie!.PosterPath
 | 
			
		||||
          : props.tvSeries!.PosterPath
 | 
			
		||||
      "
 | 
			
		||||
      alt="Poster"
 | 
			
		||||
      size="w500"
 | 
			
		||||
      class="shrink-0 w-full max-w-sm rounded-lg shadow-lg transform transition-transform duration-500 hover:scale-105"
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <!-- Text -->
 | 
			
		||||
    <div class="flex-1">
 | 
			
		||||
@@ -82,25 +79,19 @@ const alreadyAdded = computed(() =>
 | 
			
		||||
 | 
			
		||||
      <!-- Badges -->
 | 
			
		||||
      <div class="flex flex-wrap gap-2 mb-6">
 | 
			
		||||
        <span
 | 
			
		||||
        <MediaTypeBadge
 | 
			
		||||
          v-for="g in props.type === 'movie' ? props.movie!.Genres : props.tvSeries!.Genres"
 | 
			
		||||
          :key="g.id"
 | 
			
		||||
          class="px-3 py-1 rounded-md text-sm bg-linear-to-r from-gray-100 to-gray-200 text-gray-700 border border-gray-300"
 | 
			
		||||
        >
 | 
			
		||||
          {{ g.name }}
 | 
			
		||||
        </span>
 | 
			
		||||
        <span
 | 
			
		||||
          :text="g.name"
 | 
			
		||||
        />
 | 
			
		||||
        <MediaTypeBadge
 | 
			
		||||
          v-if="props.type === 'movie'"
 | 
			
		||||
          class="px-3 py-1 rounded-md text-sm bg-linear-to-r from-gray-100 to-gray-200 text-gray-700 border border-gray-300"
 | 
			
		||||
        >
 | 
			
		||||
          Runtime: {{ props.movie!.Runtime }} min
 | 
			
		||||
        </span>
 | 
			
		||||
        <span
 | 
			
		||||
          :text="`Runtime: ${props.movie!.Runtime} min`"
 | 
			
		||||
        />
 | 
			
		||||
        <MediaTypeBadge
 | 
			
		||||
          v-if="props.type === 'tv'"
 | 
			
		||||
          class="px-3 py-1 rounded-md text-sm bg-linear-to-r from-gray-100 to-gray-200 text-gray-700 border border-gray-300"
 | 
			
		||||
        >
 | 
			
		||||
          Seasons: {{ props.tvSeries!.NumberOfSeasons }}
 | 
			
		||||
        </span>
 | 
			
		||||
          :text="`Seasons: ${props.tvSeries!.NumberOfSeasons}`"
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <!-- Actions -->
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								src/components/MediaTypeBadge.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/components/MediaTypeBadge.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
interface Props {
 | 
			
		||||
  text: string
 | 
			
		||||
  type?: 'info' | 'success' | 'warning' | 'error' | 'primary' | 'secondary' | 'accent' | 'ghost'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  type: 'primary'
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <span 
 | 
			
		||||
    :class="`px-3 py-1 rounded-md text-sm bg-linear-to-r from-gray-100 to-gray-200 ${type ? 'badge-' + type : ''} text-gray-700 border border-gray-300`"
 | 
			
		||||
  >
 | 
			
		||||
    {{ props.text }}
 | 
			
		||||
  </span>
 | 
			
		||||
</template>
 | 
			
		||||
		Reference in New Issue
	
	Block a user