mirror of
				https://github.com/mmahdium/TBW.git
				synced 2025-11-04 04:28:13 +01:00 
			
		
		
		
	Compare commits
	
		
			11 Commits
		
	
	
		
			9375c6a9a7
			...
			main
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 184b0f66da | |||
| d7283b4d10 | |||
| 68d001837a | |||
| 2bd1783abb | |||
| 66ab88c924 | |||
| de8c0486cc | |||
| d2255cefb7 | |||
| 1fe13e5208 | |||
| 73d6143897 | |||
| c5e32fc26f | |||
| 4b1cd06ac7 | 
							
								
								
									
										0
									
								
								.editorconfig
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								.editorconfig
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										0
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										70
									
								
								.github/workflows/deploy.yml
									
									
									
									
										vendored
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										70
									
								
								.github/workflows/deploy.yml
									
									
									
									
										vendored
									
									
										
										
										Normal file → Executable file
									
								
							@@ -1,44 +1,44 @@
 | 
			
		||||
name: Deploy to GitHub Pages
 | 
			
		||||
# name: Deploy to GitHub Pages
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: [main]
 | 
			
		||||
  pull_request:
 | 
			
		||||
    branches: [main]
 | 
			
		||||
# on:
 | 
			
		||||
#   push:
 | 
			
		||||
#     branches: [main]
 | 
			
		||||
#   pull_request:
 | 
			
		||||
#     branches: [main]
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  build-and-deploy:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
# jobs:
 | 
			
		||||
#   build-and-deploy:
 | 
			
		||||
#     runs-on: ubuntu-latest
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout code
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
#     steps:
 | 
			
		||||
#       - name: Checkout code
 | 
			
		||||
#         uses: actions/checkout@v4
 | 
			
		||||
 | 
			
		||||
      - uses: pnpm/action-setup@v4
 | 
			
		||||
        name: Install pnpm
 | 
			
		||||
        with:
 | 
			
		||||
          version: 10
 | 
			
		||||
          run_install: false
 | 
			
		||||
#       - uses: pnpm/action-setup@v4
 | 
			
		||||
#         name: Install pnpm
 | 
			
		||||
#         with:
 | 
			
		||||
#           version: 10
 | 
			
		||||
#           run_install: false
 | 
			
		||||
 | 
			
		||||
      - name: Install Node.js
 | 
			
		||||
        uses: actions/setup-node@v4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 20
 | 
			
		||||
          cache: 'pnpm'
 | 
			
		||||
#       - name: Install Node.js
 | 
			
		||||
#         uses: actions/setup-node@v4
 | 
			
		||||
#         with:
 | 
			
		||||
#           node-version: 20
 | 
			
		||||
#           cache: 'pnpm'
 | 
			
		||||
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: pnpm install
 | 
			
		||||
#       - name: Install dependencies
 | 
			
		||||
#         run: pnpm install
 | 
			
		||||
 | 
			
		||||
      - name: Build project
 | 
			
		||||
        run: DEPLOY_ENV=GH_PAGES pnpm build
 | 
			
		||||
#       - name: Build project
 | 
			
		||||
#         run: DEPLOY_ENV=GH_PAGES pnpm build
 | 
			
		||||
 | 
			
		||||
      - name: Copy index.html to 404.html
 | 
			
		||||
        run: cp dist/index.html dist/404.html
 | 
			
		||||
#       - name: Copy index.html to 404.html
 | 
			
		||||
#         run: cp dist/index.html dist/404.html
 | 
			
		||||
 | 
			
		||||
      - name: Deploy to GitHub Pages
 | 
			
		||||
        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
 | 
			
		||||
        uses: peaceiris/actions-gh-pages@v4
 | 
			
		||||
        with:
 | 
			
		||||
          github_token: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
          publish_dir: ./dist
 | 
			
		||||
          publish_branch: gh-pages
 | 
			
		||||
#       - name: Deploy to GitHub Pages
 | 
			
		||||
#         if: github.event_name == 'push' && github.ref == 'refs/heads/main'
 | 
			
		||||
#         uses: peaceiris/actions-gh-pages@v4
 | 
			
		||||
#         with:
 | 
			
		||||
#           github_token: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
#           publish_dir: ./dist
 | 
			
		||||
#           publish_branch: gh-pages
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										0
									
								
								.prettierrc.json
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								.prettierrc.json
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										0
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										0
									
								
								eslint.config.ts
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								eslint.config.ts
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										0
									
								
								index.html
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								index.html
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										7
									
								
								package.json
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										7
									
								
								package.json
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							@@ -8,12 +8,13 @@
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "vite",
 | 
			
		||||
    "build": "run-p type-check \"build-only {@}\" --",
 | 
			
		||||
    "build": "run-p type-check \"build-only {@}\" -- && npm run inject-preloads",
 | 
			
		||||
    "preview": "vite preview",
 | 
			
		||||
    "build-only": "vite build",
 | 
			
		||||
    "type-check": "vue-tsc --build",
 | 
			
		||||
    "lint": "eslint . --fix",
 | 
			
		||||
    "format": "prettier --write src/"
 | 
			
		||||
    "format": "prettier --write src/",
 | 
			
		||||
    "inject-preloads": "node scripts/inject-preloads.js"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@formkit/auto-animate": "^0.9.0",
 | 
			
		||||
@@ -45,4 +46,4 @@
 | 
			
		||||
    "vite-plugin-vue-devtools": "^8.0.3",
 | 
			
		||||
    "vue-tsc": "^3.1.2"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										0
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										0
									
								
								public/favicon.ico
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								public/favicon.ico
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| 
		 Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB  | 
							
								
								
									
										0
									
								
								public/robots.txt
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								public/robots.txt
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										42
									
								
								scripts/inject-preloads.js
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										42
									
								
								scripts/inject-preloads.js
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
import fs from 'fs'
 | 
			
		||||
import path from 'path'
 | 
			
		||||
 | 
			
		||||
const distDir = path.resolve('dist')
 | 
			
		||||
const manifestPath = path.join(distDir, '.vite/manifest.json')
 | 
			
		||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
 | 
			
		||||
 | 
			
		||||
const indexPath = path.join(distDir, 'index.html')
 | 
			
		||||
let html = fs.readFileSync(indexPath, 'utf-8')
 | 
			
		||||
 | 
			
		||||
const filesToPreload = new Set()
 | 
			
		||||
 | 
			
		||||
function collectFiles(entryKey) {
 | 
			
		||||
  const chunk = manifest[entryKey]
 | 
			
		||||
  if (!chunk) return
 | 
			
		||||
  filesToPreload.add(chunk.file)
 | 
			
		||||
  for (const imp of chunk.imports || []) {
 | 
			
		||||
    if (imp === 'index.html') continue // skip entrypoint
 | 
			
		||||
    const dep = manifest[imp]
 | 
			
		||||
    if (dep) {
 | 
			
		||||
      filesToPreload.add(dep.file)
 | 
			
		||||
      collectFiles(imp) // recurse for transitive deps
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// automatically find all .vue entries that are dynamic (lazy‑loaded)
 | 
			
		||||
for (const key of Object.keys(manifest)) {
 | 
			
		||||
  if (key.endsWith('.vue') && manifest[key].isDynamicEntry) {
 | 
			
		||||
    collectFiles(key)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
for (const f of filesToPreload) {
 | 
			
		||||
  const tag = `<link rel="modulepreload" href="/${f}">`
 | 
			
		||||
  if (!html.includes(tag)) {
 | 
			
		||||
    html = html.replace('</head>', `  ${tag}\n</head>`)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fs.writeFileSync(indexPath, html)
 | 
			
		||||
console.log('Preload tags injected')
 | 
			
		||||
							
								
								
									
										4
									
								
								src/App.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										4
									
								
								src/App.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							@@ -1,11 +1,11 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import '@/style.css'
 | 
			
		||||
import NavBar from './components/NavBar.vue'
 | 
			
		||||
import { SpeedInsights } from "@vercel/speed-insights/vue"
 | 
			
		||||
import { SpeedInsights } from '@vercel/speed-insights/vue'
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <SpeedInsights/>
 | 
			
		||||
  <SpeedInsights />
 | 
			
		||||
  <NavBar />
 | 
			
		||||
 | 
			
		||||
  <RouterView />
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								src/components/AddMoreCard.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										2
									
								
								src/components/AddMoreCard.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							@@ -3,7 +3,7 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <router-link
 | 
			
		||||
    to="/add"
 | 
			
		||||
    class="card relative w-full h-full flex items-center justify-center aspect-2/3 bg-white/40 backdrop-blur-md border-2 border-dashed border-gray-300 rounded-lg shadow-sm hover:shadow-md hover:border-gray-400 transition-all duration-300"
 | 
			
		||||
    class="card relative w-full h-full flex items-center justify-center aspect-2/3 bg-white/40 backdrop-blur-md rounded-lg transition-all duration-300"
 | 
			
		||||
    v-motion-fade-visible-once
 | 
			
		||||
  >
 | 
			
		||||
    <!-- Plus icon -->
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								src/components/ImageWithFallback.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										3
									
								
								src/components/ImageWithFallback.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							@@ -23,7 +23,7 @@ const imageSource = computed(() => {
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <figure class="overflow-hidden flex items-center justify-center bg-gray-50">
 | 
			
		||||
  <figure class="overflow-hidden flex items-center justify-center bg-gray-50 aspect-2/3">
 | 
			
		||||
    <span v-if="!loaded" class="loading loading-ring loading-lg text-primary"></span>
 | 
			
		||||
 | 
			
		||||
    <div v-else-if="imageLoadFailed" class="flex items-center justify-center">
 | 
			
		||||
@@ -44,6 +44,7 @@ const imageSource = computed(() => {
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <img
 | 
			
		||||
      v-motion-fade-visible-once
 | 
			
		||||
      v-show="loaded && !imageLoadFailed"
 | 
			
		||||
      :src="imageSource"
 | 
			
		||||
      :alt="props.alt"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								src/components/LoadingSpinner.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								src/components/LoadingSpinner.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										144
									
								
								src/components/MediaCard.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										144
									
								
								src/components/MediaCard.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							@@ -3,6 +3,7 @@ import { useMediaStore } from '@/stores/media'
 | 
			
		||||
import { computed } from 'vue'
 | 
			
		||||
import type { MediaType } from '@/types/Media'
 | 
			
		||||
import ImageWithFallback from './ImageWithFallback.vue'
 | 
			
		||||
import ScoreRing from './ScoreRing.vue'
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  media: MediaType
 | 
			
		||||
@@ -20,51 +21,128 @@ const alreadyAdded = computed(() => store.mediaList.some((media) => media.Id ===
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div
 | 
			
		||||
    class="card relative w-full h-full overflow-hidden bg-white/70 backdrop-blur-md border border-gray-200/60 shadow-md hover:shadow-xl transition-all duration-300"
 | 
			
		||||
    class="card relative w-full h-full overflow-hidden bg-white/70 backdrop-blur-md border border-gray-200/60 shadow-xs hover:shadow-xl transition-all duration-300"
 | 
			
		||||
    v-motion-fade-visible-once
 | 
			
		||||
  >
 | 
			
		||||
    <!-- Poster -->
 | 
			
		||||
    <router-link
 | 
			
		||||
      :to="{ name: 'details', params: { type: props.media.MediaType, id: props.media.Id } }"
 | 
			
		||||
    >
 | 
			
		||||
      <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 -->
 | 
			
		||||
    <div class="card-body p-4 flex flex-col">
 | 
			
		||||
    <div class="relative w-full">
 | 
			
		||||
      <router-link
 | 
			
		||||
        :to="{ name: 'details', params: { type: props.media.MediaType, id: props.media.Id } }"
 | 
			
		||||
        class="block w-full aspect-2/3 overflow-hidden bg-gray-100 relative"
 | 
			
		||||
      >
 | 
			
		||||
        <ImageWithFallback
 | 
			
		||||
          :src="props.media.PosterPath"
 | 
			
		||||
          :alt="props.media.Title"
 | 
			
		||||
          size="w300"
 | 
			
		||||
          class="w-full h-full object-cover transform transition-transform duration-500 hover:scale-105"
 | 
			
		||||
        />
 | 
			
		||||
        <!-- Glassy gradient shadow overlay -->
 | 
			
		||||
        <div
 | 
			
		||||
          class="absolute bottom-0 left-0 right-0 h-16 bg-linear-to-t from-white/40 via-white/20 to-transparent backdrop-blur-[0.5px] pointer-events-none"
 | 
			
		||||
        ></div>
 | 
			
		||||
      </router-link>
 | 
			
		||||
 | 
			
		||||
      <!-- Radial progress placed on the right edge, centered on the seam between poster and body -->
 | 
			
		||||
      <div class="absolute right-3 bottom-0 z-20 translate-y-1/2" aria-hidden="true">
 | 
			
		||||
        <ScoreRing :VoteAverage="props.media.VoteAverage" />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Body -->
 | 
			
		||||
    <div class="card-body p-4 flex flex-col pt-6">
 | 
			
		||||
      <router-link
 | 
			
		||||
        :to="{ name: 'details', params: { type: props.media.MediaType, id: props.media.Id } }"
 | 
			
		||||
        class="block"
 | 
			
		||||
      >
 | 
			
		||||
        <h2
 | 
			
		||||
          class="card-title text-base font-semibold bg-linear-to-r from-gray-700 to-gray-500 bg-clip-text text-transparent"
 | 
			
		||||
          class="card-title text-base font-semibold bg-linear-to-r from-gray-700 to-gray-500 bg-clip-text text-transparent break-words"
 | 
			
		||||
        >
 | 
			
		||||
          {{ props.media.Title }}
 | 
			
		||||
        </h2>
 | 
			
		||||
        <p class="text-sm text-gray-400">{{ props.media.ReleaseDate.slice(0, 4) }}</p>
 | 
			
		||||
        <time
 | 
			
		||||
          class="text-sm text-gray-400"
 | 
			
		||||
          :datetime="
 | 
			
		||||
            props.media.ReleaseDate === '' && props.media.FirstAirDate === ''
 | 
			
		||||
              ? 'unknown'
 | 
			
		||||
              : new Date(
 | 
			
		||||
                  props.media.ReleaseDate === ''
 | 
			
		||||
                    ? props.media.FirstAirDate
 | 
			
		||||
                    : props.media.ReleaseDate,
 | 
			
		||||
                ).toISOString()
 | 
			
		||||
          "
 | 
			
		||||
        >
 | 
			
		||||
          {{
 | 
			
		||||
            props.media.ReleaseDate === '' && props.media.FirstAirDate === ''
 | 
			
		||||
              ? 'unknown'
 | 
			
		||||
              : new Intl.DateTimeFormat('default', {
 | 
			
		||||
                  year: 'numeric',
 | 
			
		||||
                  month: 'long',
 | 
			
		||||
                  day: 'numeric',
 | 
			
		||||
                }).format(
 | 
			
		||||
                  new Date(
 | 
			
		||||
                    props.media.ReleaseDate === ''
 | 
			
		||||
                      ? props.media.FirstAirDate
 | 
			
		||||
                      : props.media.ReleaseDate,
 | 
			
		||||
                  ),
 | 
			
		||||
                )
 | 
			
		||||
          }}
 | 
			
		||||
        </time>
 | 
			
		||||
      </router-link>
 | 
			
		||||
 | 
			
		||||
      <div class="card-actions justify-end mt-auto">
 | 
			
		||||
        <button
 | 
			
		||||
          v-motion-fade-visible-once
 | 
			
		||||
          v-if="!alreadyAdded"
 | 
			
		||||
          class="btn btn-sm px-4 bg-linear-to-r from-gray-100 to-gray-200 border border-gray-300 text-gray-700 hover:from-gray-200 hover:to-gray-300 hover:text-gray-900 transition"
 | 
			
		||||
          @click="$emit('add-media', props.media)"
 | 
			
		||||
      <!-- Actions row -->
 | 
			
		||||
      <div class="card-actions flex items-center justify-between mt-auto">
 | 
			
		||||
        <!-- Type badge -->
 | 
			
		||||
        <span
 | 
			
		||||
          class="badge badge-outline text-xs font-medium px-2 py-1"
 | 
			
		||||
          :class="props.media.Adult ? 'badge-error' : ''"
 | 
			
		||||
        >
 | 
			
		||||
          Add
 | 
			
		||||
        </button>
 | 
			
		||||
        <button
 | 
			
		||||
          v-motion-fade-visible-once
 | 
			
		||||
          v-else
 | 
			
		||||
          class="btn btn-sm px-4 bg-linear-to-r from-red-50 to-red-100 border border-red-200 text-red-600 hover:from-red-100 hover:to-red-200 hover:text-red-700 transition"
 | 
			
		||||
          @click="$emit('remove-media', props.media.Id)"
 | 
			
		||||
        >
 | 
			
		||||
          Remove
 | 
			
		||||
        </button>
 | 
			
		||||
          {{ props.media.MediaType === 'movie' ? 'Movie' : 'Show' }}
 | 
			
		||||
        </span>
 | 
			
		||||
 | 
			
		||||
        <!-- Bookmark buttons -->
 | 
			
		||||
        <div class="flex gap-2">
 | 
			
		||||
          <button
 | 
			
		||||
            v-motion-fade-visible-once
 | 
			
		||||
            v-if="!alreadyAdded"
 | 
			
		||||
            class="btn btn-circle btn-md px-1.5 bg-linear-to-r from-gray-100 to-gray-200 border-gray-300 text-gray-700 hover:from-gray-200 hover:to-gray-300 hover:text-gray-900 transition"
 | 
			
		||||
            @click="emit('add-media', props.media)"
 | 
			
		||||
          >
 | 
			
		||||
            <svg
 | 
			
		||||
              xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
              fill="none"
 | 
			
		||||
              viewBox="0 0 24 24"
 | 
			
		||||
              stroke-width="1.5"
 | 
			
		||||
              stroke="currentColor"
 | 
			
		||||
              class="size-6"
 | 
			
		||||
            >
 | 
			
		||||
              <path
 | 
			
		||||
                stroke-linecap="round"
 | 
			
		||||
                stroke-linejoin="round"
 | 
			
		||||
                d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0 1 11.186 0Z"
 | 
			
		||||
              />
 | 
			
		||||
            </svg>
 | 
			
		||||
          </button>
 | 
			
		||||
          <button
 | 
			
		||||
            v-motion-fade-visible-once
 | 
			
		||||
            v-else
 | 
			
		||||
            class="btn btn-circle btn-md px-1.5 bg-linear-to-r from-red-50 to-red-100 border border-red-200 text-red-600 hover:from-red-100 hover:to-red-200 hover:text-red-700 transition"
 | 
			
		||||
            @click="emit('remove-media', props.media.Id)"
 | 
			
		||||
          >
 | 
			
		||||
            <svg
 | 
			
		||||
              xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
              fill="none"
 | 
			
		||||
              viewBox="0 0 24 24"
 | 
			
		||||
              stroke-width="1.5"
 | 
			
		||||
              stroke="currentColor"
 | 
			
		||||
              class="size-6"
 | 
			
		||||
            >
 | 
			
		||||
              <path
 | 
			
		||||
                stroke-linecap="round"
 | 
			
		||||
                stroke-linejoin="round"
 | 
			
		||||
                d="m3 3 1.664 1.664M21 21l-1.5-1.5m-5.485-1.242L12 17.25 4.5 21V8.742m.164-4.078a2.15 2.15 0 0 1 1.743-1.342 48.507 48.507 0 0 1 11.186 0c1.1.128 1.907 1.077 1.907 2.185V19.5M4.664 4.664 19.5 19.5"
 | 
			
		||||
              />
 | 
			
		||||
            </svg>
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										85
									
								
								src/components/MediaFilters.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										85
									
								
								src/components/MediaFilters.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,85 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import type { SearchFilters } from '@/types/SearchFilters'
 | 
			
		||||
import { reactive, watch } from 'vue'
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  modelValue: SearchFilters // optional initial value
 | 
			
		||||
}>()
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
  (e: 'update:filters', value: SearchFilters): void
 | 
			
		||||
}>()
 | 
			
		||||
 | 
			
		||||
const local = reactive<SearchFilters>({
 | 
			
		||||
  includeAdult: props.modelValue?.includeAdult,
 | 
			
		||||
  onlyMovies: props.modelValue?.onlyMovies,
 | 
			
		||||
  onlySeries: props.modelValue?.onlySeries,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
function toggleIncludeAdult(e: Event) {
 | 
			
		||||
  local.includeAdult = (e.target as HTMLInputElement).checked
 | 
			
		||||
}
 | 
			
		||||
function toggleOnlyMovies(e: Event) {
 | 
			
		||||
  const checked = (e.target as HTMLInputElement).checked
 | 
			
		||||
  if (!checked && !local.onlySeries) {
 | 
			
		||||
    local.onlySeries = true
 | 
			
		||||
  }
 | 
			
		||||
  local.onlyMovies = checked
 | 
			
		||||
}
 | 
			
		||||
function toggleOnlySeries(e: Event) {
 | 
			
		||||
  const checked = (e.target as HTMLInputElement).checked
 | 
			
		||||
  if (!checked && !local.onlyMovies) {
 | 
			
		||||
    local.onlyMovies = true
 | 
			
		||||
  }
 | 
			
		||||
  local.onlySeries = checked
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
  () => ({ ...local }),
 | 
			
		||||
  (next) => {
 | 
			
		||||
    // ensureAtLeastOneGenre()
 | 
			
		||||
    const out: SearchFilters = {
 | 
			
		||||
      includeAdult: next.includeAdult,
 | 
			
		||||
      onlyMovies: next.onlyMovies,
 | 
			
		||||
      onlySeries: next.onlySeries,
 | 
			
		||||
    }
 | 
			
		||||
    emit('update:filters', out)
 | 
			
		||||
  },
 | 
			
		||||
  { deep: true, immediate: true },
 | 
			
		||||
)
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <form class="flex gap-2 justify-center mb-8">
 | 
			
		||||
    <label>
 | 
			
		||||
      <input
 | 
			
		||||
        class="btn"
 | 
			
		||||
        :class="local.includeAdult ? 'btn-error' : ''"
 | 
			
		||||
        type="checkbox"
 | 
			
		||||
        :checked="local.includeAdult"
 | 
			
		||||
        @change="toggleIncludeAdult"
 | 
			
		||||
        aria-label="Adult"
 | 
			
		||||
      />
 | 
			
		||||
    </label>
 | 
			
		||||
 | 
			
		||||
    <label>
 | 
			
		||||
      <input
 | 
			
		||||
        class="btn"
 | 
			
		||||
        type="checkbox"
 | 
			
		||||
        :checked="local.onlyMovies"
 | 
			
		||||
        @change="toggleOnlyMovies"
 | 
			
		||||
        aria-label="Movies"
 | 
			
		||||
      />
 | 
			
		||||
    </label>
 | 
			
		||||
 | 
			
		||||
    <label>
 | 
			
		||||
      <input
 | 
			
		||||
        class="btn"
 | 
			
		||||
        type="checkbox"
 | 
			
		||||
        :checked="local.onlySeries"
 | 
			
		||||
        @change="toggleOnlySeries"
 | 
			
		||||
        aria-label="TV Series"
 | 
			
		||||
      />
 | 
			
		||||
    </label>
 | 
			
		||||
  </form>
 | 
			
		||||
</template>
 | 
			
		||||
							
								
								
									
										8
									
								
								src/components/MediaList.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										8
									
								
								src/components/MediaList.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							@@ -32,9 +32,13 @@ const emit = defineEmits<{
 | 
			
		||||
    <ul
 | 
			
		||||
      v-auto-animate
 | 
			
		||||
      v-else
 | 
			
		||||
      class="grid gap-4 grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5"
 | 
			
		||||
      class="grid gap-4 grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6"
 | 
			
		||||
    >
 | 
			
		||||
      <li v-for="media in props.medias" :key="media.Id" v-auto-animate>
 | 
			
		||||
      <li
 | 
			
		||||
        v-for="media in props.medias.filter((media) => media.PosterPath)"
 | 
			
		||||
        :key="media.Id"
 | 
			
		||||
        v-auto-animate
 | 
			
		||||
      >
 | 
			
		||||
        <MediaCard
 | 
			
		||||
          :media="media"
 | 
			
		||||
          @add-media="$emit('add-media', $event)"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								src/components/MediaTypeBadge.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								src/components/MediaTypeBadge.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										63
									
								
								src/components/MediaDetails.vue → src/components/MovieDetails.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										63
									
								
								src/components/MediaDetails.vue → src/components/MovieDetails.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							@@ -1,6 +1,5 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import type { MovieDetailsType } from '@/types/Movie'
 | 
			
		||||
import type { TvSeriesDetailsType } from '@/types/TvSeries'
 | 
			
		||||
import { computed } from 'vue'
 | 
			
		||||
import { useMediaStore } from '@/stores/media'
 | 
			
		||||
import type { MediaType } from '@/types/Media'
 | 
			
		||||
@@ -8,38 +7,26 @@ import ImageWithFallback from './ImageWithFallback.vue'
 | 
			
		||||
import MediaTypeBadge from './MediaTypeBadge.vue'
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  type: 'movie' | 'tv'
 | 
			
		||||
  media?: MediaType
 | 
			
		||||
  movie?: MovieDetailsType | null
 | 
			
		||||
  tvSeries?: TvSeriesDetailsType | null
 | 
			
		||||
}>()
 | 
			
		||||
 | 
			
		||||
const store = useMediaStore()
 | 
			
		||||
 | 
			
		||||
const alreadyAdded = computed(() =>
 | 
			
		||||
  props.type === 'movie' && props.movie
 | 
			
		||||
    ? store.mediaList.some((m: MediaType) => m.Id === props.movie!.Id)
 | 
			
		||||
    : props.type === 'tv' && props.tvSeries
 | 
			
		||||
      ? store.mediaList.some((ts: MediaType) => ts.Id === props.tvSeries!.Id)
 | 
			
		||||
      : false,
 | 
			
		||||
  props.movie ? store.mediaList.some((m: MediaType) => m.Id === props.movie!.Id) : false,
 | 
			
		||||
)
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <!-- Error -->
 | 
			
		||||
  <!-- <ErrorAlert
 | 
			
		||||
    v-if="props.type === 'movie' && props.movie?.Error"
 | 
			
		||||
    :message="props.movie.ErrorMessage"
 | 
			
		||||
  /> -->
 | 
			
		||||
 | 
			
		||||
  <!-- Hero -->
 | 
			
		||||
  <div
 | 
			
		||||
    class="flex flex-col lg:flex-row gap-12 items-center max-w-6xl w-full bg-white/70 backdrop-blur-md border border-gray-200/60 shadow-md rounded-xl p-8 transition"
 | 
			
		||||
    class="flex flex-col lg:flex-row gap-12 items-center max-w-6xl w-full bg-white/70 border border-gray-200/60 rounded-xl p-8 transition"
 | 
			
		||||
    v-motion-fade-visible-once
 | 
			
		||||
  >
 | 
			
		||||
    <!-- Poster -->
 | 
			
		||||
    <ImageWithFallback
 | 
			
		||||
      :src="type === 'movie' ? props.movie!.PosterPath : props.tvSeries!.PosterPath"
 | 
			
		||||
      :src="props.movie!.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"
 | 
			
		||||
@@ -48,51 +35,29 @@ const alreadyAdded = computed(() =>
 | 
			
		||||
    <!-- Text -->
 | 
			
		||||
    <div class="flex-1">
 | 
			
		||||
      <h1 class="text-4xl font-bold text-gray-800 mb-2">
 | 
			
		||||
        <template v-if="props.type === 'movie'">
 | 
			
		||||
          {{ props.movie!.Title }}
 | 
			
		||||
          <span class="text-gray-400 text-lg font-normal"
 | 
			
		||||
            >({{ props.movie!.ReleaseDate?.slice(0, 4) }})</span
 | 
			
		||||
          >
 | 
			
		||||
        </template>
 | 
			
		||||
        <template v-else>
 | 
			
		||||
          {{ props.tvSeries!.Name }}
 | 
			
		||||
          <span class="text-gray-400 text-lg font-normal"
 | 
			
		||||
            >({{ props.tvSeries!.FirstAirDate?.slice(0, 4) }})</span
 | 
			
		||||
          >
 | 
			
		||||
        </template>
 | 
			
		||||
        {{ props.movie!.Title }}
 | 
			
		||||
        <span class="text-gray-400 text-lg font-normal"
 | 
			
		||||
          >({{ props.movie!.ReleaseDate?.slice(0, 4) }})</span
 | 
			
		||||
        >
 | 
			
		||||
      </h1>
 | 
			
		||||
 | 
			
		||||
      <p v-if="props.type === 'movie' && props.movie!.Tagline" class="italic text-gray-500 mb-2">
 | 
			
		||||
      <p v-if="props.movie!.Tagline" class="italic text-gray-500 mb-2">
 | 
			
		||||
        {{ props.movie!.Tagline }}
 | 
			
		||||
      </p>
 | 
			
		||||
      <p v-if="props.type === 'tv' && props.tvSeries!.Tagline" class="italic text-gray-500 mb-2">
 | 
			
		||||
        {{ props.tvSeries!.Tagline }}
 | 
			
		||||
      </p>
 | 
			
		||||
 | 
			
		||||
      <p class="text-gray-600 leading-relaxed mb-6">
 | 
			
		||||
        {{ props.type === 'movie' ? props.movie!.Overview : props.tvSeries!.Overview }}
 | 
			
		||||
        {{ props.movie!.Overview }}
 | 
			
		||||
      </p>
 | 
			
		||||
 | 
			
		||||
      <!-- Badges -->
 | 
			
		||||
      <div class="flex flex-wrap gap-2 mb-6">
 | 
			
		||||
        <MediaTypeBadge
 | 
			
		||||
          v-for="g in props.type === 'movie' ? props.movie!.Genres : props.tvSeries!.Genres"
 | 
			
		||||
          :key="g.id"
 | 
			
		||||
          :text="g.name"
 | 
			
		||||
        />
 | 
			
		||||
        <MediaTypeBadge
 | 
			
		||||
          v-if="props.type === 'movie'"
 | 
			
		||||
          :text="`Runtime: ${props.movie!.Runtime} min`"
 | 
			
		||||
        />
 | 
			
		||||
        <MediaTypeBadge
 | 
			
		||||
          v-if="props.type === 'tv'"
 | 
			
		||||
          :text="`Seasons: ${props.tvSeries!.NumberOfSeasons}`"
 | 
			
		||||
        />
 | 
			
		||||
        <MediaTypeBadge v-for="g in props.movie!.Genres" :key="g.id" :text="g.name" />
 | 
			
		||||
        <MediaTypeBadge :text="`Runtime: ${props.movie!.Runtime} min`" />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <!-- Actions -->
 | 
			
		||||
      <div class="card-actions" v-auto-animate>
 | 
			
		||||
        <template v-if="(props.movie && props.media) || (props.tvSeries && props.media)">
 | 
			
		||||
        <template v-if="props.movie && props.media">
 | 
			
		||||
          <button
 | 
			
		||||
            v-if="!alreadyAdded"
 | 
			
		||||
            class="btn px-6 bg-linear-to-r from-gray-100 to-gray-200 border border-gray-300 text-gray-700 hover:from-gray-200 hover:to-gray-300"
 | 
			
		||||
@@ -113,8 +78,8 @@ const alreadyAdded = computed(() =>
 | 
			
		||||
          :to="{
 | 
			
		||||
            name: 'watch',
 | 
			
		||||
            params: {
 | 
			
		||||
              name: props.type === 'movie' ? props.movie!.Title : props.tvSeries!.Name,
 | 
			
		||||
              id: props.type === 'movie' ? props.movie!.Id : props.tvSeries!.Id,
 | 
			
		||||
              name: props.movie!.Title,
 | 
			
		||||
              id: props.movie!.Id,
 | 
			
		||||
            },
 | 
			
		||||
          }"
 | 
			
		||||
        >
 | 
			
		||||
							
								
								
									
										0
									
								
								src/components/NavBar.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								src/components/NavBar.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										21
									
								
								src/components/ScoreRing.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										21
									
								
								src/components/ScoreRing.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
const props = defineProps<{ VoteAverage: number }>()
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div
 | 
			
		||||
    class="radial-progress bg-primary text-primary-content border-primary border-4 font-bold flex items-center justify-center"
 | 
			
		||||
    :style="{
 | 
			
		||||
      '--value': props.VoteAverage * 10,
 | 
			
		||||
      '--size': '2rem',
 | 
			
		||||
      '--thickness': '3px',
 | 
			
		||||
    }"
 | 
			
		||||
    :aria-valuenow="props.VoteAverage"
 | 
			
		||||
    role="progressbar"
 | 
			
		||||
  >
 | 
			
		||||
    <span class="flex items-baseline text-[0.7em]">
 | 
			
		||||
      {{ props.VoteAverage === 0 ? '?' : (props.VoteAverage * 10).toFixed(0) }}
 | 
			
		||||
      <span class="text-[0.5em] relative -top-1 ml-0.5">%</span>
 | 
			
		||||
    </span>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
							
								
								
									
										2
									
								
								src/components/SearchBar.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										2
									
								
								src/components/SearchBar.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							@@ -13,7 +13,7 @@ function onSubmit() {
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <form @submit.prevent="onSubmit" class="flex justify-center mb-8">
 | 
			
		||||
  <form @submit.prevent="onSubmit" class="flex justify-center mb-3">
 | 
			
		||||
    <div
 | 
			
		||||
      class="flex items-center w-full max-w-md bg-white/70 backdrop-blur-md border border-gray-200/60 shadow-sm hover:shadow-md transition rounded-lg overflow-hidden"
 | 
			
		||||
    >
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										109
									
								
								src/components/TVSeriesDetails.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										109
									
								
								src/components/TVSeriesDetails.vue
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,109 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
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'
 | 
			
		||||
import type { TvSeriesDetailsType } from '@/types/TvSeries'
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  media?: MediaType
 | 
			
		||||
  tvSeries?: TvSeriesDetailsType | null
 | 
			
		||||
}>()
 | 
			
		||||
 | 
			
		||||
const store = useMediaStore()
 | 
			
		||||
 | 
			
		||||
const alreadyAdded = computed(() =>
 | 
			
		||||
  props.tvSeries ? store.mediaList.some((m: MediaType) => m.Id === props.tvSeries!.Id) : false,
 | 
			
		||||
)
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <!-- Hero -->
 | 
			
		||||
  <div
 | 
			
		||||
    class="flex flex-col lg:flex-row gap-12 items-center max-w-6xl w-full bg-white/70 border border-gray-200/60 rounded-xl p-8 transition"
 | 
			
		||||
    v-motion-fade-visible-once
 | 
			
		||||
  >
 | 
			
		||||
    <!-- Poster -->
 | 
			
		||||
    <ImageWithFallback
 | 
			
		||||
      :src="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">
 | 
			
		||||
      <h1 class="text-4xl font-bold text-gray-800 mb-2">
 | 
			
		||||
        {{ props.tvSeries!.Name }}
 | 
			
		||||
        <span class="text-gray-400 text-lg font-normal"
 | 
			
		||||
          >({{ props.tvSeries!.FirstAirDate?.slice(0, 4) }})</span
 | 
			
		||||
        >
 | 
			
		||||
      </h1>
 | 
			
		||||
 | 
			
		||||
      <p v-if="props.tvSeries!.Tagline" class="italic text-gray-500 mb-2">
 | 
			
		||||
        {{ props.tvSeries!.Tagline }}
 | 
			
		||||
      </p>
 | 
			
		||||
 | 
			
		||||
      <p class="text-gray-600 leading-relaxed mb-6">
 | 
			
		||||
        {{ props.tvSeries!.Overview }}
 | 
			
		||||
      </p>
 | 
			
		||||
 | 
			
		||||
      <!-- Badges -->
 | 
			
		||||
      <div class="flex flex-wrap gap-2 mb-6">
 | 
			
		||||
        <MediaTypeBadge v-for="g in props.tvSeries!.Genres" :key="g.id" :text="g.name" />
 | 
			
		||||
        <MediaTypeBadge :text="`Seasons: ${props.tvSeries!.NumberOfSeasons}`" />
 | 
			
		||||
        <MediaTypeBadge :text="`Episodes: ${props.tvSeries!.NumberOfEpisodes}`" />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <!-- Actions -->
 | 
			
		||||
      <div class="card-actions" v-auto-animate>
 | 
			
		||||
        <template v-if="props.tvSeries && props.media">
 | 
			
		||||
          <button
 | 
			
		||||
            v-if="!alreadyAdded"
 | 
			
		||||
            class="btn px-6 bg-linear-to-r from-gray-100 to-gray-200 border border-gray-300 text-gray-700 hover:from-gray-200 hover:to-gray-300"
 | 
			
		||||
            @click="store.addMedia(props.media)"
 | 
			
		||||
          >
 | 
			
		||||
            Add to list
 | 
			
		||||
          </button>
 | 
			
		||||
          <button
 | 
			
		||||
            v-else
 | 
			
		||||
            class="btn px-6 bg-linear-to-r from-red-50 to-red-100 border border-red-200 text-red-600 hover:from-red-100 hover:to-red-200"
 | 
			
		||||
            @click="store.removeMedia(props.media.Id)"
 | 
			
		||||
          >
 | 
			
		||||
            Remove from library
 | 
			
		||||
          </button>
 | 
			
		||||
        </template>
 | 
			
		||||
 | 
			
		||||
        <!-- <RouterLink
 | 
			
		||||
          :to="{
 | 
			
		||||
            name: 'watch',
 | 
			
		||||
            params: {
 | 
			
		||||
              name: props.tvSeries!.Name,
 | 
			
		||||
              id: props.tvSeries!.Id,
 | 
			
		||||
            },
 | 
			
		||||
          }"
 | 
			
		||||
        >
 | 
			
		||||
          <button
 | 
			
		||||
            class="btn relative flex items-center gap-2 px-6 bg-linear-to-r from-indigo-500 to-violet-500 text-white border-0 shadow-md hover:opacity-90 transition"
 | 
			
		||||
          >
 | 
			
		||||
            <svg
 | 
			
		||||
              xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
              class="h-5 w-5"
 | 
			
		||||
              fill="currentColor"
 | 
			
		||||
              viewBox="0 0 20 20"
 | 
			
		||||
            >
 | 
			
		||||
              <path
 | 
			
		||||
                fill-rule="evenodd"
 | 
			
		||||
                d="M6.5 5.5a1 1 0 0 1 1.52-.85l6 4.5a1 1 0 0 1 0 1.7l-6 4.5A1 1 0 0 1 6.5 14.5v-9z"
 | 
			
		||||
                clip-rule="evenodd"
 | 
			
		||||
              />
 | 
			
		||||
            </svg>
 | 
			
		||||
            <span>Watch</span>
 | 
			
		||||
            <span class="badge badge-sm badge-secondary absolute -top-2 -right-2">Beta</span>
 | 
			
		||||
          </button>
 | 
			
		||||
        </RouterLink> -->
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
							
								
								
									
										0
									
								
								src/components/alerts/ErrorAlert.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								src/components/alerts/ErrorAlert.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										136
									
								
								src/lib/api.ts
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										136
									
								
								src/lib/api.ts
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							@@ -1,7 +1,9 @@
 | 
			
		||||
/* eslint-disable @typescript-eslint/no-explicit-any */
 | 
			
		||||
import type { MediaResponseType } from '@/types/Media'
 | 
			
		||||
import { mapMedia } from '@/types/MediaMap'
 | 
			
		||||
import type { MovieDetailsType } from '@/types/Movie'
 | 
			
		||||
import { mapMovieDetails } from '@/types/MovieMap'
 | 
			
		||||
import type { SearchFilters } from '@/types/SearchFilters'
 | 
			
		||||
import type { TvSeriesDetailsType } from '@/types/TvSeries'
 | 
			
		||||
import { mapTvSeriesDetails } from '@/types/TvSeriesMap'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
@@ -18,71 +20,99 @@ const instance = axios.create({
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const searchMovies = async (query: string): Promise<MediaResponseType> => {
 | 
			
		||||
  const response = await instance.get(`/search/multi`, {
 | 
			
		||||
    params: {
 | 
			
		||||
      query: query,
 | 
			
		||||
      include_adult: true,
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
export const loadMedia = async (
 | 
			
		||||
  query: string,
 | 
			
		||||
  page = 1,
 | 
			
		||||
  filters: SearchFilters,
 | 
			
		||||
): Promise<MediaResponseType> => {
 | 
			
		||||
  const params = {
 | 
			
		||||
    query,
 | 
			
		||||
    include_adult: filters.includeAdult || false,
 | 
			
		||||
    page,
 | 
			
		||||
  }
 | 
			
		||||
  const requests: Promise<any>[] = []
 | 
			
		||||
  if (filters.onlyMovies) {
 | 
			
		||||
    requests.push(instance.get(`/search/movie`, { params }))
 | 
			
		||||
  }
 | 
			
		||||
  if (filters.onlySeries) {
 | 
			
		||||
    requests.push(instance.get(`/search/tv`, { params }))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (response.status !== 200) {
 | 
			
		||||
  const responses = await Promise.all(requests)
 | 
			
		||||
 | 
			
		||||
  // If only one type was requested, handle that directly
 | 
			
		||||
  if (filters.onlyMovies && !filters.onlySeries) {
 | 
			
		||||
    const movieSearch = responses[0]
 | 
			
		||||
    if (movieSearch.status !== 200) {
 | 
			
		||||
      return {
 | 
			
		||||
        Results: [],
 | 
			
		||||
        Page: 0,
 | 
			
		||||
        totalResults: 0,
 | 
			
		||||
        totalPages: 0,
 | 
			
		||||
        ErrorMessage: movieSearch.data.Error,
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    const movies = movieSearch.data.results.map((r: any) => mapMedia({ ...r, media_type: 'movie' }))
 | 
			
		||||
    return {
 | 
			
		||||
      Results: movies,
 | 
			
		||||
      Page: movieSearch.data.page,
 | 
			
		||||
      totalResults: movieSearch.data.total_results,
 | 
			
		||||
      totalPages: movieSearch.data.total_pages,
 | 
			
		||||
      ErrorMessage: '',
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (filters.onlySeries && !filters.onlyMovies) {
 | 
			
		||||
    const tvSearch = responses[0]
 | 
			
		||||
    if (tvSearch.status !== 200) {
 | 
			
		||||
      return {
 | 
			
		||||
        Results: [],
 | 
			
		||||
        Page: 0,
 | 
			
		||||
        totalResults: 0,
 | 
			
		||||
        totalPages: 0,
 | 
			
		||||
        ErrorMessage: tvSearch.data.Error,
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    const tv = tvSearch.data.results.map((r: any) => mapMedia({ ...r, media_type: 'tv' }))
 | 
			
		||||
    return {
 | 
			
		||||
      Results: tv,
 | 
			
		||||
      Page: tvSearch.data.page,
 | 
			
		||||
      totalResults: tvSearch.data.total_results,
 | 
			
		||||
      totalPages: tvSearch.data.total_pages,
 | 
			
		||||
      ErrorMessage: '',
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Otherwise, both
 | 
			
		||||
  const [movieSearch, tvSearch] = responses
 | 
			
		||||
  if (movieSearch.status !== 200 || tvSearch.status !== 200) {
 | 
			
		||||
    return {
 | 
			
		||||
      Results: [],
 | 
			
		||||
      Page: 0,
 | 
			
		||||
      totalResults: 0,
 | 
			
		||||
      totalPages: 0,
 | 
			
		||||
      ErrorMessage: response.data.Error,
 | 
			
		||||
      ErrorMessage: movieSearch?.data?.Error || tvSearch?.data?.Error,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const filtered = response.data.results.filter((result: { media_type: string }) => {
 | 
			
		||||
    return result.media_type === 'movie' || result.media_type === 'tv'
 | 
			
		||||
  })
 | 
			
		||||
  const filteredMovies = movieSearch.data.results.map((r: any) =>
 | 
			
		||||
    mapMedia({ ...r, media_type: 'movie' }),
 | 
			
		||||
  )
 | 
			
		||||
  const filteredTV = tvSearch.data.results.map((r: any) => mapMedia({ ...r, media_type: 'tv' }))
 | 
			
		||||
 | 
			
		||||
  const data: MediaResponseType = {
 | 
			
		||||
    Results: filtered.map(mapMedia),
 | 
			
		||||
    Page: response.data.page,
 | 
			
		||||
    totalResults: response.data.total_results,
 | 
			
		||||
    totalPages: response.data.total_pages,
 | 
			
		||||
  const res: any[] = []
 | 
			
		||||
  for (let i = 0; i < Math.max(filteredMovies.length, filteredTV.length); i++) {
 | 
			
		||||
    if (i < filteredMovies.length) res.push(filteredMovies[i])
 | 
			
		||||
    if (i < filteredTV.length) res.push(filteredTV[i])
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    Results: res,
 | 
			
		||||
    Page: Math.max(movieSearch.data.page, tvSearch.data.page),
 | 
			
		||||
    totalResults: movieSearch.data.total_results + tvSearch.data.total_results,
 | 
			
		||||
    totalPages: Math.max(movieSearch.data.total_pages, tvSearch.data.total_pages),
 | 
			
		||||
    ErrorMessage: '',
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const loadMoreMovies = async (query: string, page: number): Promise<MediaResponseType> => {
 | 
			
		||||
  const response = await instance.get(`/search/multi`, {
 | 
			
		||||
    params: {
 | 
			
		||||
      query: query,
 | 
			
		||||
      include_adult: true,
 | 
			
		||||
      page: page,
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  if (response.status !== 200) {
 | 
			
		||||
    return {
 | 
			
		||||
      Results: [],
 | 
			
		||||
      Page: 0,
 | 
			
		||||
      totalResults: 0,
 | 
			
		||||
      totalPages: 0,
 | 
			
		||||
      ErrorMessage: response.data.Error,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const filtered = response.data.results.filter((result: { media_type: string }) => {
 | 
			
		||||
    return result.media_type === 'movie' || result.media_type === 'tv'
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const data: MediaResponseType = {
 | 
			
		||||
    Results: filtered.map(mapMedia),
 | 
			
		||||
    Page: response.data.page,
 | 
			
		||||
    totalResults: response.data.total_results,
 | 
			
		||||
    totalPages: response.data.total_pages,
 | 
			
		||||
    ErrorMessage: '',
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getMovieDetails = async (id: string): Promise<MovieDetailsType | null> => {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								src/main.ts
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								src/main.ts
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										0
									
								
								src/router/index.ts
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								src/router/index.ts
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										7
									
								
								src/stores/media.ts
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										7
									
								
								src/stores/media.ts
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							@@ -1,6 +1,7 @@
 | 
			
		||||
import { ref } from 'vue'
 | 
			
		||||
import { defineStore } from 'pinia'
 | 
			
		||||
import type { MediaType } from '@/types/Media'
 | 
			
		||||
import type { SearchFilters } from '@/types/SearchFilters'
 | 
			
		||||
 | 
			
		||||
function saveMedias(medias: MediaType[]) {
 | 
			
		||||
  localStorage.setItem('medias', JSON.stringify(medias))
 | 
			
		||||
@@ -33,11 +34,13 @@ export const useSearchPageStore = defineStore('searchPage', () => {
 | 
			
		||||
  const mediaList = ref<MediaType[]>()
 | 
			
		||||
  const searchPage = ref(0)
 | 
			
		||||
  const searchQuery = ref('')
 | 
			
		||||
  const searchFilters = ref<SearchFilters>()
 | 
			
		||||
 | 
			
		||||
  function setState(page: number, query: string, medias: MediaType[]) {
 | 
			
		||||
  function setState(page: number, query: string, medias: MediaType[], filters: SearchFilters) {
 | 
			
		||||
    searchPage.value = page
 | 
			
		||||
    searchQuery.value = query
 | 
			
		||||
    mediaList.value = medias
 | 
			
		||||
    searchFilters.value = filters
 | 
			
		||||
  }
 | 
			
		||||
  return { searchPage, searchQuery, mediaList, setState }
 | 
			
		||||
  return { searchPage, searchQuery, mediaList, searchFilters, setState }
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								src/style.css
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								src/style.css
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										0
									
								
								src/types/Media.ts
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								src/types/Media.ts
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										0
									
								
								src/types/MediaMap.ts
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								src/types/MediaMap.ts
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										0
									
								
								src/types/Movie.ts
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								src/types/Movie.ts
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										0
									
								
								src/types/MovieMap.ts
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								src/types/MovieMap.ts
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										5
									
								
								src/types/SearchFilters.ts
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										5
									
								
								src/types/SearchFilters.ts
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
export type SearchFilters = {
 | 
			
		||||
  includeAdult: boolean
 | 
			
		||||
  onlyMovies: boolean
 | 
			
		||||
  onlySeries: boolean
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										0
									
								
								src/types/TvSeries.ts
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								src/types/TvSeries.ts
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										0
									
								
								src/types/TvSeriesMap.ts
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								src/types/TvSeriesMap.ts
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										39
									
								
								src/views/AddView.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										39
									
								
								src/views/AddView.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							@@ -1,11 +1,13 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { loadMoreMovies, searchMovies } from '@/lib/api'
 | 
			
		||||
import { loadMedia } from '@/lib/api'
 | 
			
		||||
import { onMounted, ref, watch } from 'vue'
 | 
			
		||||
import SearchBar from '@/components/SearchBar.vue'
 | 
			
		||||
import MediaList from '@/components/MediaList.vue'
 | 
			
		||||
import type { MediaType } from '@/types/Media'
 | 
			
		||||
import ErrorAlert from '@/components/alerts/ErrorAlert.vue'
 | 
			
		||||
import { useSearchPageStore, useMediaStore } from '@/stores/media'
 | 
			
		||||
import MediaFilters from '@/components/MediaFilters.vue'
 | 
			
		||||
import type { SearchFilters } from '@/types/SearchFilters'
 | 
			
		||||
 | 
			
		||||
const medias = ref<MediaType[]>()
 | 
			
		||||
const seachError = ref<string>('')
 | 
			
		||||
@@ -17,6 +19,13 @@ const isLoadingMore = ref(false)
 | 
			
		||||
const state = useSearchPageStore()
 | 
			
		||||
const store = useMediaStore()
 | 
			
		||||
 | 
			
		||||
// Initialize filters with stored values from the state, defaulting to initial values if not available
 | 
			
		||||
const filters = ref<SearchFilters>({
 | 
			
		||||
  includeAdult: state.searchFilters?.includeAdult ?? false,
 | 
			
		||||
  onlyMovies: state.searchFilters?.onlyMovies ?? true,
 | 
			
		||||
  onlySeries: state.searchFilters?.onlySeries ?? false,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const handleAddMedia = (media: MediaType) => {
 | 
			
		||||
  store.addMedia(media)
 | 
			
		||||
}
 | 
			
		||||
@@ -29,8 +38,7 @@ async function searchMovie() {
 | 
			
		||||
  try {
 | 
			
		||||
    isSearching.value = true
 | 
			
		||||
    searchPage.value = 1
 | 
			
		||||
    const result = await searchMovies(searchQuery.value)
 | 
			
		||||
    console.log(result)
 | 
			
		||||
    const result = await loadMedia(searchQuery.value, searchPage.value, filters.value)
 | 
			
		||||
    if (result.totalResults === 0) {
 | 
			
		||||
      medias.value = []
 | 
			
		||||
      seachError.value = 'No results found'
 | 
			
		||||
@@ -39,7 +47,7 @@ async function searchMovie() {
 | 
			
		||||
    seachError.value = ''
 | 
			
		||||
    medias.value = result.Results
 | 
			
		||||
 | 
			
		||||
    state.setState(searchPage.value, searchQuery.value, result.Results)
 | 
			
		||||
    state.setState(searchPage.value, searchQuery.value, result.Results, filters.value)
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    medias.value = []
 | 
			
		||||
    console.error(error)
 | 
			
		||||
@@ -53,9 +61,14 @@ async function loadMore() {
 | 
			
		||||
  try {
 | 
			
		||||
    isLoadingMore.value = true
 | 
			
		||||
    searchPage.value++
 | 
			
		||||
    const result = await loadMoreMovies(searchQuery.value, searchPage.value)
 | 
			
		||||
    const result = await loadMedia(searchQuery.value, searchPage.value, filters.value)
 | 
			
		||||
    medias.value?.push(...result.Results)
 | 
			
		||||
    state.setState(searchPage.value, searchQuery.value, medias.value ? medias.value : [])
 | 
			
		||||
    state.setState(
 | 
			
		||||
      searchPage.value,
 | 
			
		||||
      searchQuery.value,
 | 
			
		||||
      medias.value ? medias.value : [],
 | 
			
		||||
      filters.value,
 | 
			
		||||
    )
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    searchPage.value = 1
 | 
			
		||||
    seachError.value = (error as Error).message
 | 
			
		||||
@@ -91,6 +104,18 @@ watch(searchQuery, () => {
 | 
			
		||||
    timeoutId = null
 | 
			
		||||
  }, 500)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
watch(filters, () => {
 | 
			
		||||
  if (searchQuery.value.length > 2) {
 | 
			
		||||
    state.setState(
 | 
			
		||||
      searchPage.value,
 | 
			
		||||
      searchQuery.value,
 | 
			
		||||
      medias.value ? medias.value : [],
 | 
			
		||||
      filters.value,
 | 
			
		||||
    )
 | 
			
		||||
    searchMovie()
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
@@ -105,6 +130,8 @@ watch(searchQuery, () => {
 | 
			
		||||
    <!-- Search bar -->
 | 
			
		||||
    <SearchBar v-model="searchQuery" @submit="searchMovie" />
 | 
			
		||||
 | 
			
		||||
    <MediaFilters @update:filters="(f) => (filters = f)" :modelValue="filters" />
 | 
			
		||||
 | 
			
		||||
    <!-- Loading spinner -->
 | 
			
		||||
    <div v-if="isSearching" class="flex justify-center my-16">
 | 
			
		||||
      <span class="loading loading-ring loading-lg text-primary"></span>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								src/views/DetailsView.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										19
									
								
								src/views/DetailsView.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							@@ -1,6 +1,7 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import ErrorAlert from '@/components/alerts/ErrorAlert.vue'
 | 
			
		||||
import MediaDetails from '@/components/MediaDetails.vue'
 | 
			
		||||
import MovieDetails from '@/components/MovieDetails.vue'
 | 
			
		||||
import TVSeriesDetails from '@/components/TVSeriesDetails.vue'
 | 
			
		||||
import { getMovieDetails, getSeriesDetails } from '@/lib/api'
 | 
			
		||||
import { mapMovieDetailsToMedia, mapTvSeriesDetailsToMedia } from '@/types/MediaMap'
 | 
			
		||||
import type { MovieDetailsType } from '@/types/Movie'
 | 
			
		||||
@@ -57,17 +58,23 @@ onMounted(async () => {
 | 
			
		||||
    <ErrorAlert v-if="errorMessage" :message="errorMessage" />
 | 
			
		||||
 | 
			
		||||
    <!-- Loading -->
 | 
			
		||||
    <div v-else-if="isLoading" class="flex justify-center items-center">
 | 
			
		||||
    <!-- Loading -->
 | 
			
		||||
    <div v-else-if="isLoading" class="flex justify-center items-center min-h-screen">
 | 
			
		||||
      <span class="loading loading-ring loading-lg text-primary"></span>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Details -->
 | 
			
		||||
    <MediaDetails
 | 
			
		||||
      v-else-if="movieDetails || tvSeriesDetails"
 | 
			
		||||
      :type="route.params.type === 'movie' ? 'movie' : 'tv'"
 | 
			
		||||
    <MovieDetails
 | 
			
		||||
      v-else-if="movieDetails"
 | 
			
		||||
      :media="media"
 | 
			
		||||
      :movie="movieDetails"
 | 
			
		||||
      :tv-series="tvSeriesDetails"
 | 
			
		||||
      v-motion-fade-visible-once
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <TVSeriesDetails
 | 
			
		||||
      v-else-if="tvSeriesDetails"
 | 
			
		||||
      :media="media"
 | 
			
		||||
      :tvSeries="tvSeriesDetails"
 | 
			
		||||
      v-motion-fade-visible-once
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								src/views/HomeView.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								src/views/HomeView.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										0
									
								
								src/views/ListView.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								src/views/ListView.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										0
									
								
								src/views/NotFoundView.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								src/views/NotFoundView.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										0
									
								
								src/views/WatchView.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								src/views/WatchView.vue
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										0
									
								
								tsconfig.app.json
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								tsconfig.app.json
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										0
									
								
								tsconfig.json
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								tsconfig.json
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										0
									
								
								tsconfig.node.json
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								tsconfig.node.json
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										3
									
								
								vite.config.ts
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										3
									
								
								vite.config.ts
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							@@ -20,4 +20,7 @@ export default defineConfig({
 | 
			
		||||
      '@': fileURLToPath(new URL('./src', import.meta.url)),
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  build: {
 | 
			
		||||
    manifest: true
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user