mirror of
				https://github.com/mmahdium/TBW.git
				synced 2025-11-04 04:28:13 +01:00 
			
		
		
		
	first commit
This commit is contained in:
		
							
								
								
									
										8
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
 | 
			
		||||
charset = utf-8
 | 
			
		||||
indent_size = 2
 | 
			
		||||
indent_style = space
 | 
			
		||||
insert_final_newline = true
 | 
			
		||||
trim_trailing_whitespace = true
 | 
			
		||||
end_of_line = lf
 | 
			
		||||
max_line_length = 100
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
* text=auto eol=lf
 | 
			
		||||
							
								
								
									
										30
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
# Logs
 | 
			
		||||
logs
 | 
			
		||||
*.log
 | 
			
		||||
npm-debug.log*
 | 
			
		||||
yarn-debug.log*
 | 
			
		||||
yarn-error.log*
 | 
			
		||||
pnpm-debug.log*
 | 
			
		||||
lerna-debug.log*
 | 
			
		||||
 | 
			
		||||
node_modules
 | 
			
		||||
.DS_Store
 | 
			
		||||
dist
 | 
			
		||||
dist-ssr
 | 
			
		||||
coverage
 | 
			
		||||
*.local
 | 
			
		||||
 | 
			
		||||
/cypress/videos/
 | 
			
		||||
/cypress/screenshots/
 | 
			
		||||
 | 
			
		||||
# Editor directories and files
 | 
			
		||||
.vscode/*
 | 
			
		||||
!.vscode/extensions.json
 | 
			
		||||
.idea
 | 
			
		||||
*.suo
 | 
			
		||||
*.ntvs*
 | 
			
		||||
*.njsproj
 | 
			
		||||
*.sln
 | 
			
		||||
*.sw?
 | 
			
		||||
 | 
			
		||||
*.tsbuildinfo
 | 
			
		||||
							
								
								
									
										6
									
								
								.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "$schema": "https://json.schemastore.org/prettierrc",
 | 
			
		||||
  "semi": false,
 | 
			
		||||
  "singleQuote": true,
 | 
			
		||||
  "printWidth": 100
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
{
 | 
			
		||||
  "recommendations": [
 | 
			
		||||
    "Vue.volar",
 | 
			
		||||
    "dbaeumer.vscode-eslint",
 | 
			
		||||
    "EditorConfig.EditorConfig",
 | 
			
		||||
    "esbenp.prettier-vscode"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										48
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
			
		||||
# TBW
 | 
			
		||||
 | 
			
		||||
This template should help get you started developing with Vue 3 in Vite.
 | 
			
		||||
 | 
			
		||||
## Recommended IDE Setup
 | 
			
		||||
 | 
			
		||||
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
 | 
			
		||||
 | 
			
		||||
## Recommended Browser Setup
 | 
			
		||||
 | 
			
		||||
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
 | 
			
		||||
  - [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd) 
 | 
			
		||||
  - [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
 | 
			
		||||
- Firefox:
 | 
			
		||||
  - [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
 | 
			
		||||
  - [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
 | 
			
		||||
 | 
			
		||||
## Type Support for `.vue` Imports in TS
 | 
			
		||||
 | 
			
		||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
 | 
			
		||||
 | 
			
		||||
## Customize configuration
 | 
			
		||||
 | 
			
		||||
See [Vite Configuration Reference](https://vite.dev/config/).
 | 
			
		||||
 | 
			
		||||
## Project Setup
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
pnpm install
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Compile and Hot-Reload for Development
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
pnpm dev
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Type-Check, Compile and Minify for Production
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
pnpm build
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Lint with [ESLint](https://eslint.org/)
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
pnpm lint
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										2
									
								
								env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
/// <reference types="vite/client" />
 | 
			
		||||
declare module '*.vue'
 | 
			
		||||
							
								
								
									
										22
									
								
								eslint.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								eslint.config.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
import { globalIgnores } from 'eslint/config'
 | 
			
		||||
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
 | 
			
		||||
import pluginVue from 'eslint-plugin-vue'
 | 
			
		||||
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
 | 
			
		||||
 | 
			
		||||
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
 | 
			
		||||
// import { configureVueProject } from '@vue/eslint-config-typescript'
 | 
			
		||||
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
 | 
			
		||||
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
 | 
			
		||||
 | 
			
		||||
export default defineConfigWithVueTs(
 | 
			
		||||
  {
 | 
			
		||||
    name: 'app/files-to-lint',
 | 
			
		||||
    files: ['**/*.{ts,mts,tsx,vue}'],
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
 | 
			
		||||
 | 
			
		||||
  pluginVue.configs['flat/essential'],
 | 
			
		||||
  vueTsConfigs.recommended,
 | 
			
		||||
  skipFormatting,
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										14
									
								
								index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <link rel="icon" href="/favicon.ico">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
    <meta name="description" content="A simple and beautiful movie list app built with Vue 3 and Tailwind CSS (daisyUI).">
 | 
			
		||||
    <title>To Be Watched</title>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <div id="app"></div>
 | 
			
		||||
    <script type="module" src="/src/main.ts"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										44
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "tbw",
 | 
			
		||||
  "version": "0.0.0",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "engines": {
 | 
			
		||||
    "node": "^20.19.0 || >=22.12.0"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "vite",
 | 
			
		||||
    "build": "run-p type-check \"build-only {@}\" --",
 | 
			
		||||
    "preview": "vite preview",
 | 
			
		||||
    "build-only": "vite build",
 | 
			
		||||
    "type-check": "vue-tsc --build",
 | 
			
		||||
    "lint": "eslint . --fix",
 | 
			
		||||
    "format": "prettier --write src/"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@tailwindcss/vite": "^4.1.14",
 | 
			
		||||
    "axios": "^1.12.2",
 | 
			
		||||
    "pinia": "^3.0.3",
 | 
			
		||||
    "tailwindcss": "^4.1.14",
 | 
			
		||||
    "vue": "^3.5.22",
 | 
			
		||||
    "vue-router": "^4.5.1"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@tsconfig/node22": "^22.0.2",
 | 
			
		||||
    "@types/node": "^22.18.6",
 | 
			
		||||
    "@vitejs/plugin-vue": "^6.0.1",
 | 
			
		||||
    "@vue/eslint-config-prettier": "^10.2.0",
 | 
			
		||||
    "@vue/eslint-config-typescript": "^14.6.0",
 | 
			
		||||
    "@vue/tsconfig": "^0.8.1",
 | 
			
		||||
    "daisyui": "^5.3.6",
 | 
			
		||||
    "eslint": "^9.33.0",
 | 
			
		||||
    "eslint-plugin-vue": "~10.4.0",
 | 
			
		||||
    "jiti": "^2.5.1",
 | 
			
		||||
    "npm-run-all2": "^8.0.4",
 | 
			
		||||
    "prettier": "3.6.2",
 | 
			
		||||
    "typescript": "~5.9.0",
 | 
			
		||||
    "vite": "^7.1.7",
 | 
			
		||||
    "vite-plugin-vue-devtools": "^8.0.2",
 | 
			
		||||
    "vue-tsc": "^3.1.0"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3751
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										3751
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 4.2 KiB  | 
							
								
								
									
										2
									
								
								public/robots.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								public/robots.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
User-agent: *
 | 
			
		||||
Allow: /
 | 
			
		||||
							
								
								
									
										12
									
								
								src/App.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/App.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import '@/style.css'
 | 
			
		||||
import NavBar from './components/NavBar.vue'
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <NavBar />
 | 
			
		||||
 | 
			
		||||
  <RouterView />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
							
								
								
									
										52
									
								
								src/components/MovieCard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/components/MovieCard.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { useMoviesStore } from '@/stores/movies'
 | 
			
		||||
import { computed } from 'vue'
 | 
			
		||||
import type { MovieType } from '@/types/Movie'
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  movie: MovieType
 | 
			
		||||
  loading: boolean | undefined
 | 
			
		||||
}>()
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{ (e: 'loaded', id: string): void }>()
 | 
			
		||||
 | 
			
		||||
const store = useMoviesStore()
 | 
			
		||||
 | 
			
		||||
const alreadyAdded = computed(() =>
 | 
			
		||||
  store.movieList.some((movie) => movie.imdbID === props.movie.imdbID),
 | 
			
		||||
)
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="card card-side bg-base-100 shadow-md hover:shadow-lg transition rounded-sm">
 | 
			
		||||
    <router-link :to="{ name: 'details', params: { id: props.movie.imdbID } }">
 | 
			
		||||
      <figure
 | 
			
		||||
        class="overflow-hidden flex items-center justify-center bg-gray-100 aspect-[2/3] w-32 sm:w-40 md:w-48 rounded-l-sm"
 | 
			
		||||
      >
 | 
			
		||||
        <span v-if="props.loading" class="loading loading-ring loading-xl text-primary"></span>
 | 
			
		||||
 | 
			
		||||
        <img
 | 
			
		||||
          v-show="!props.loading"
 | 
			
		||||
          :src="props.movie.Poster"
 | 
			
		||||
          :alt="props.movie.Title"
 | 
			
		||||
          class="object-cover w-full h-full"
 | 
			
		||||
          @load="emit('loaded', props.movie.imdbID)"
 | 
			
		||||
        />
 | 
			
		||||
      </figure>
 | 
			
		||||
    </router-link>
 | 
			
		||||
    <div class="card-body p-4">
 | 
			
		||||
      <router-link :to="{ name: 'details', params: { id: props.movie.imdbID } }">
 | 
			
		||||
        <h2 class="card-title text-lg">{{ props.movie.Title }}</h2>
 | 
			
		||||
        <p class="text-sm text-gray-500">{{ props.movie.Year }}</p>
 | 
			
		||||
      </router-link>
 | 
			
		||||
      <div v-if="!alreadyAdded" class="card-actions justify-end mt-auto">
 | 
			
		||||
        <button class="btn btn-sm btn-primary" @click="store.addMovie(props.movie)">Add</button>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div v-else class="card-actions justify-end mt-auto">
 | 
			
		||||
        <button class="btn btn-sm btn-error" @click="store.removeMovie(props.movie.imdbID)">
 | 
			
		||||
          Delete
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
							
								
								
									
										74
									
								
								src/components/MovieDetails.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/components/MovieDetails.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import type { MovieDetailsType } from '@/types/Movie'
 | 
			
		||||
import { computed, ref } from 'vue'
 | 
			
		||||
import ErrorAlert from './alerts/ErrorAlert.vue'
 | 
			
		||||
import { useMoviesStore } from '@/stores/movies'
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{ movie: MovieDetailsType | undefined }>()
 | 
			
		||||
 | 
			
		||||
const imageLoaded = ref(false)
 | 
			
		||||
 | 
			
		||||
const store = useMoviesStore()
 | 
			
		||||
 | 
			
		||||
const alreadyAdded = computed(() =>
 | 
			
		||||
  props.movie ? store.movieList.some((movie) => movie.imdbID === props.movie!.imdbID) : false,
 | 
			
		||||
)
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <ErrorAlert class="" v-if="props.movie?.Error" :message="props.movie.ErrorMessage" />
 | 
			
		||||
 | 
			
		||||
  <div v-else class="hero bg-base-200 min-h-screen">
 | 
			
		||||
    <!-- Loading state for hero content -->
 | 
			
		||||
    <div v-if="props.movie === undefined" class="hero-content">
 | 
			
		||||
      <span class="loading loading-ring loading-lg text-primary"></span>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Actual hero content -->
 | 
			
		||||
    <div v-else class="hero bg-base-200 min-h-screen">
 | 
			
		||||
      <div class="hero-content flex-col lg:flex-row gap-12">
 | 
			
		||||
        <!-- Poster -->
 | 
			
		||||
        <figure class="flex-shrink-0">
 | 
			
		||||
          <span v-if="!imageLoaded" class="loading loading-ring loading-lg text-primary"></span>
 | 
			
		||||
          <img
 | 
			
		||||
            v-show="imageLoaded"
 | 
			
		||||
            :src="props.movie.Poster"
 | 
			
		||||
            alt="Poster"
 | 
			
		||||
            class="w-full max-w-lg rounded-lg shadow-2xl"
 | 
			
		||||
            @load="imageLoaded = true"
 | 
			
		||||
          />
 | 
			
		||||
        </figure>
 | 
			
		||||
 | 
			
		||||
        <!-- Text -->
 | 
			
		||||
        <div class="max-w-2xl">
 | 
			
		||||
          <h1 class="text-5xl font-bold">
 | 
			
		||||
            {{ props.movie.Title }}
 | 
			
		||||
            <span class="text-gray-500 text-sm">({{ props.movie.Year }})</span>
 | 
			
		||||
          </h1>
 | 
			
		||||
          <p class="py-6">{{ props.movie.Plot }}</p>
 | 
			
		||||
          <div class="flex flex-wrap gap-2">
 | 
			
		||||
            <span class="badge badge-dash badge-primary">
 | 
			
		||||
              Language: {{ props.movie.Language }}
 | 
			
		||||
            </span>
 | 
			
		||||
            <span class="badge badge-dash badge-secondary">
 | 
			
		||||
              Country: {{ props.movie.Country }}
 | 
			
		||||
            </span>
 | 
			
		||||
            <span class="badge badge-dash badge-info">
 | 
			
		||||
              IMDB Rating: {{ props.movie.imdbRating }}
 | 
			
		||||
            </span>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-if="!alreadyAdded" class="card-actions mt-4">
 | 
			
		||||
            <button class="btn -sm btn-primary" @click="store.addMovie(props.movie)">
 | 
			
		||||
              Add to list
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-else class="card-actions mt-4">
 | 
			
		||||
            <button class="btn btn-error" @click="store.removeMovie(props.movie.imdbID)">
 | 
			
		||||
              Remove from list
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
							
								
								
									
										38
									
								
								src/components/MovieList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/components/MovieList.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import MovieCard from './MovieCard.vue'
 | 
			
		||||
import type { MovieType } from '@/types/Movie'
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  movies: MovieType[]
 | 
			
		||||
  loadingImages: Record<string, boolean>
 | 
			
		||||
  loadingMore: boolean
 | 
			
		||||
  isSearch: boolean
 | 
			
		||||
}>()
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{ (e: 'loaded', id: string): void; (e: 'loadMore'): void }>()
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <p v-if="props.movies.length === 0" class="text-center text-gray-500 py-12">
 | 
			
		||||
      No movies found.<br />
 | 
			
		||||
      <RouterLink to="/add" class="text-black font-bold">Add a movie</RouterLink>
 | 
			
		||||
    </p>
 | 
			
		||||
 | 
			
		||||
    <ul v-else class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
 | 
			
		||||
      <li v-for="movie in props.movies" :key="movie.imdbID">
 | 
			
		||||
        <MovieCard
 | 
			
		||||
          :movie="movie"
 | 
			
		||||
          :loading="props.loadingImages[movie.imdbID]"
 | 
			
		||||
          @loaded="emit('loaded', $event)"
 | 
			
		||||
        />
 | 
			
		||||
      </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
    <div v-if="props.isSearch && props.movies.length > 0" class="flex justify-center mt-8">
 | 
			
		||||
      <button class="btn btn-outline btn-secondary" @click="emit('loadMore')" :disabled="props.loadingMore">
 | 
			
		||||
        <span v-if="props.loadingMore" class="loading loading-spinner loading-xs mr-2"></span>
 | 
			
		||||
        Load more
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
							
								
								
									
										43
									
								
								src/components/NavBar.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/components/NavBar.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="navbar bg-base-100 shadow-sm sticky top-0 z-10">
 | 
			
		||||
    <div class="navbar-start">
 | 
			
		||||
      <div class="dropdown">
 | 
			
		||||
        <div tabindex="0" role="button" class="btn btn-ghost lg:hidden" aria-label="Menu">
 | 
			
		||||
          <svg
 | 
			
		||||
            xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
            class="h-5 w-5"
 | 
			
		||||
            fill="none"
 | 
			
		||||
            viewBox="0 0 24 24"
 | 
			
		||||
            stroke="currentColor"
 | 
			
		||||
          >
 | 
			
		||||
            <path
 | 
			
		||||
              stroke-linecap="round"
 | 
			
		||||
              stroke-linejoin="round"
 | 
			
		||||
              stroke-width="2"
 | 
			
		||||
              d="M4 6h16M4 12h8m-8 6h16"
 | 
			
		||||
            />
 | 
			
		||||
          </svg>
 | 
			
		||||
        </div>
 | 
			
		||||
        <ul
 | 
			
		||||
          tabindex="-1"
 | 
			
		||||
          class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow"
 | 
			
		||||
        >
 | 
			
		||||
          <li><RouterLink class="text-xl" to="/">Home</RouterLink></li>
 | 
			
		||||
          <li><RouterLink class="text-xl" to="/list">List</RouterLink></li>
 | 
			
		||||
          <li><RouterLink class="text-xl" to="/add">Add</RouterLink></li>
 | 
			
		||||
        </ul>
 | 
			
		||||
      </div>
 | 
			
		||||
      <RouterLink class="btn btn-ghost text-xl" to="/">To Vue</RouterLink>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="navbar-center hidden lg:flex">
 | 
			
		||||
      <ul class="menu menu-horizontal px-1">
 | 
			
		||||
        <li><RouterLink class="text-xl" to="/">Home</RouterLink></li>
 | 
			
		||||
        <li><RouterLink class="text-xl" to="/list">List</RouterLink></li>
 | 
			
		||||
        <li><RouterLink class="text-xl" to="/add">Add</RouterLink></li>
 | 
			
		||||
      </ul>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="navbar-end">
 | 
			
		||||
      <RouterLink class="btn" to="/add">Get Started</RouterLink>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
							
								
								
									
										42
									
								
								src/components/SearchBar.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/components/SearchBar.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref } from 'vue'
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{ modelValue: string }>()
 | 
			
		||||
const emit = defineEmits<{ (e: 'update:modelValue', value: string): void; (e: 'submit'): void }>()
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
const localQuery = ref(props.modelValue)
 | 
			
		||||
 | 
			
		||||
function onSubmit() {
 | 
			
		||||
  emit('submit')
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <form @submit.prevent="onSubmit">
 | 
			
		||||
    <div class="join flex justify-center mb-8">
 | 
			
		||||
      <label class="input join-item">
 | 
			
		||||
        <svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
 | 
			
		||||
          <g
 | 
			
		||||
            stroke-linejoin="round"
 | 
			
		||||
            stroke-linecap="round"
 | 
			
		||||
            stroke-width="2.5"
 | 
			
		||||
            fill="none"
 | 
			
		||||
            stroke="currentColor"
 | 
			
		||||
          >
 | 
			
		||||
            <circle cx="11" cy="11" r="8"></circle>
 | 
			
		||||
            <path d="m21 21-4.3-4.3"></path>
 | 
			
		||||
          </g>
 | 
			
		||||
        </svg>
 | 
			
		||||
        <input
 | 
			
		||||
          type="search"
 | 
			
		||||
          placeholder="Search for a movie"
 | 
			
		||||
          required
 | 
			
		||||
          :value="props.modelValue"
 | 
			
		||||
          @input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
 | 
			
		||||
        />
 | 
			
		||||
      </label>
 | 
			
		||||
      <button class="btn btn-neutral join-item">Search</button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </form>
 | 
			
		||||
</template>
 | 
			
		||||
							
								
								
									
										21
									
								
								src/components/alerts/ErrorAlert.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/components/alerts/ErrorAlert.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
const props = defineProps<{ message: string }>()
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <div role="alert" class="alert alert-error">
 | 
			
		||||
    <svg
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
      class="h-6 w-6 shrink-0 stroke-current"
 | 
			
		||||
      fill="none"
 | 
			
		||||
      viewBox="0 0 24 24"
 | 
			
		||||
    >
 | 
			
		||||
      <path
 | 
			
		||||
        stroke-linecap="round"
 | 
			
		||||
        stroke-linejoin="round"
 | 
			
		||||
        stroke-width="2"
 | 
			
		||||
        d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
    <span>Error! {{ props.message }}</span>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
							
								
								
									
										63
									
								
								src/lib/api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								src/lib/api.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
			
		||||
import type { MovieResponseType, MovieType } from '@/types/Movie'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
 | 
			
		||||
const API_KEY = '595695c3' // I know this should not be here and I dont care
 | 
			
		||||
 | 
			
		||||
const instance = axios.create({
 | 
			
		||||
  baseURL: 'https://www.omdbapi.com/',
 | 
			
		||||
  timeout: 6969,
 | 
			
		||||
  params: {
 | 
			
		||||
    apikey: API_KEY,
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const searchMovies = async (query: string): Promise<MovieResponseType> => {
 | 
			
		||||
  const response = await instance.get(``, {
 | 
			
		||||
    params: {
 | 
			
		||||
      s: query,
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // if (response.data.Response === 'False') {
 | 
			
		||||
  //   throw new Error(response.data.Error)
 | 
			
		||||
  // }
 | 
			
		||||
 | 
			
		||||
  const data: MovieResponseType = {
 | 
			
		||||
    Search: response.data.Search as MovieType[],
 | 
			
		||||
    totalResults: response.data.totalResults,
 | 
			
		||||
    Response: response.data.Response === 'True',
 | 
			
		||||
    ErrorMessage: response.data.Error || '',
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const loadMoreMovies = async (query: string, page: number) => {
 | 
			
		||||
  const response = await instance.get(``, {
 | 
			
		||||
    params: {
 | 
			
		||||
      s: query,
 | 
			
		||||
      page: page,
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  if (response.data.Response === 'False') {
 | 
			
		||||
    throw new Error(response.data.Error)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return response.data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getMovie = async (id: string) => {
 | 
			
		||||
  const response = await instance.get(``, {
 | 
			
		||||
    params: {
 | 
			
		||||
      i: id,
 | 
			
		||||
      plot: 'full',
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  if (response.data.Response === 'False') {
 | 
			
		||||
    throw new Error(response.data.Error)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return response.data
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								src/main.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/main.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
import { createApp } from 'vue'
 | 
			
		||||
import { createPinia } from 'pinia'
 | 
			
		||||
 | 
			
		||||
import App from './App.vue'
 | 
			
		||||
import router from './router'
 | 
			
		||||
 | 
			
		||||
const app = createApp(App)
 | 
			
		||||
 | 
			
		||||
app.use(createPinia())
 | 
			
		||||
app.use(router)
 | 
			
		||||
 | 
			
		||||
app.mount('#app')
 | 
			
		||||
							
								
								
									
										37
									
								
								src/router/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/router/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
 | 
			
		||||
import HomeView from '@/views/HomeView.vue'
 | 
			
		||||
import NotFoundView from '@/views/NotFoundView.vue'
 | 
			
		||||
import ListView from '@/views/ListView.vue'
 | 
			
		||||
import DetailsView from '@/views/DetailsView.vue'
 | 
			
		||||
import AddView from '@/views/AddView.vue'
 | 
			
		||||
 | 
			
		||||
const routes: RouteRecordRaw[] = [
 | 
			
		||||
  { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFoundView },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/',
 | 
			
		||||
    name: 'home',
 | 
			
		||||
    component: HomeView,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/list',
 | 
			
		||||
    name: 'list',
 | 
			
		||||
    component: ListView,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/details/:id',
 | 
			
		||||
    name: 'details',
 | 
			
		||||
    component: DetailsView,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/add',
 | 
			
		||||
    name: 'add',
 | 
			
		||||
    component: AddView,
 | 
			
		||||
  },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
const router = createRouter({
 | 
			
		||||
  history: createWebHistory(import.meta.env.BASE_URL),
 | 
			
		||||
  routes: routes,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default router
 | 
			
		||||
							
								
								
									
										45
									
								
								src/stores/movies.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/stores/movies.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
import { ref } from 'vue'
 | 
			
		||||
import { defineStore } from 'pinia'
 | 
			
		||||
import type { MovieType } from '@/types/Movie'
 | 
			
		||||
 | 
			
		||||
function saveMovies(movies: MovieType[]) {
 | 
			
		||||
  localStorage.setItem('movies', JSON.stringify(movies))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function loadMovies(): MovieType[] {
 | 
			
		||||
  const movies = localStorage.getItem('movies')
 | 
			
		||||
  return movies ? JSON.parse(movies) : []
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useMoviesStore = defineStore('movies', () => {
 | 
			
		||||
  const movieList = ref<MovieType[]>(loadMovies())
 | 
			
		||||
 | 
			
		||||
  function addMovie(movie: MovieType) {
 | 
			
		||||
    if (!movieList.value.find((m) => m.imdbID === movie.imdbID)) {
 | 
			
		||||
      movieList.value.push(movie)
 | 
			
		||||
      saveMovies(movieList.value)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function removeMovie(movieId: string) {
 | 
			
		||||
    movieList.value = movieList.value.filter((m) => m.imdbID !== movieId)
 | 
			
		||||
    saveMovies(movieList.value)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return { movieList, addMovie, removeMovie }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// TODO: add state for search page
 | 
			
		||||
 | 
			
		||||
export const useSearchPageStore = defineStore('searchPage', () => {
 | 
			
		||||
  const movieList = ref<MovieType[]>()
 | 
			
		||||
  const searchPage = ref(0)
 | 
			
		||||
  const searchQuery = ref('')
 | 
			
		||||
 | 
			
		||||
  function setState(page: number, query: string, movies: MovieType[]) {
 | 
			
		||||
    searchPage.value = page
 | 
			
		||||
    searchQuery.value = query
 | 
			
		||||
    movieList.value = movies
 | 
			
		||||
  }
 | 
			
		||||
  return { searchPage, searchQuery, movieList, setState }
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										2
									
								
								src/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/style.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
@import 'tailwindcss';
 | 
			
		||||
@plugin "daisyui";
 | 
			
		||||
							
								
								
									
										28
									
								
								src/types/Movie.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/types/Movie.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
export type MovieType = {
 | 
			
		||||
  Title: string
 | 
			
		||||
  Year: string
 | 
			
		||||
  imdbID: string
 | 
			
		||||
  Type: string
 | 
			
		||||
  Poster: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type MovieResponseType = {
 | 
			
		||||
  Search: MovieType[]
 | 
			
		||||
  totalResults: string
 | 
			
		||||
  Response: boolean
 | 
			
		||||
  ErrorMessage: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type MovieDetailsType = {
 | 
			
		||||
  Title: string
 | 
			
		||||
  Year: string
 | 
			
		||||
  imdbID: string
 | 
			
		||||
  Type: string
 | 
			
		||||
  Poster: string
 | 
			
		||||
  Plot: string
 | 
			
		||||
  Language: string
 | 
			
		||||
  Country: string
 | 
			
		||||
  imdbRating: string
 | 
			
		||||
  Error: boolean
 | 
			
		||||
  ErrorMessage: string
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										110
									
								
								src/views/AddView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/views/AddView.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,110 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { loadMoreMovies, searchMovies } from '@/lib/api'
 | 
			
		||||
import { onMounted, ref } from 'vue'
 | 
			
		||||
import SearchBar from '@/components/SearchBar.vue'
 | 
			
		||||
import MovieList from '@/components/MovieList.vue'
 | 
			
		||||
import type { MovieType } from '@/types/Movie'
 | 
			
		||||
import ErrorAlert from '@/components/alerts/ErrorAlert.vue'
 | 
			
		||||
import { useSearchPageStore } from '@/stores/movies'
 | 
			
		||||
 | 
			
		||||
const movies = ref<MovieType[]>()
 | 
			
		||||
const seachError = ref<string>('')
 | 
			
		||||
const searchQuery = ref('')
 | 
			
		||||
const loadingImages = ref<Record<string, boolean>>({})
 | 
			
		||||
const searchPage = ref(1)
 | 
			
		||||
const isSearching = ref(false)
 | 
			
		||||
const isLoadingMore = ref(false)
 | 
			
		||||
 | 
			
		||||
const state = useSearchPageStore()
 | 
			
		||||
 | 
			
		||||
async function searchMovie() {
 | 
			
		||||
  try {
 | 
			
		||||
    isSearching.value = true
 | 
			
		||||
    searchPage.value = 1
 | 
			
		||||
    const result = await searchMovies(searchQuery.value)
 | 
			
		||||
    if (!result.Response) {
 | 
			
		||||
      seachError.value = result.ErrorMessage
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    seachError.value = ''
 | 
			
		||||
    const previouslyLoaded = Object.keys(loadingImages.value).filter((id) =>
 | 
			
		||||
      result.Search.some((m: MovieType) => m.imdbID === id),
 | 
			
		||||
    )
 | 
			
		||||
    movies.value = result.Search
 | 
			
		||||
 | 
			
		||||
    state.setState(searchPage.value, searchQuery.value, result.Search)
 | 
			
		||||
 | 
			
		||||
    for (const m of result.Search) {
 | 
			
		||||
      if (previouslyLoaded.includes(m.imdbID)) {
 | 
			
		||||
        continue
 | 
			
		||||
      }
 | 
			
		||||
      loadingImages.value[m.imdbID] = true
 | 
			
		||||
    }
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error(error)
 | 
			
		||||
    seachError.value = (error as Error).message
 | 
			
		||||
  } finally {
 | 
			
		||||
    isSearching.value = false
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async function loadMore() {
 | 
			
		||||
  try {
 | 
			
		||||
    isLoadingMore.value = true
 | 
			
		||||
    searchPage.value++
 | 
			
		||||
    const result = await loadMoreMovies(searchQuery.value, searchPage.value)
 | 
			
		||||
    movies.value.push(...result.Search)
 | 
			
		||||
    state.setState(searchPage.value, searchQuery.value, movies.value)
 | 
			
		||||
    for (const m of result.Search) {
 | 
			
		||||
      loadingImages.value[m.imdbID] = true
 | 
			
		||||
    }
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    searchPage.value = 1
 | 
			
		||||
    seachError.value = (error as Error).message
 | 
			
		||||
  } finally {
 | 
			
		||||
    isLoadingMore.value = false
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleLoaded(id: string) {
 | 
			
		||||
  loadingImages.value[id] = false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  if (state.searchQuery !== '' && state.searchPage !== 0 && state.movieList.length > 0) {
 | 
			
		||||
    searchQuery.value = state.searchQuery
 | 
			
		||||
    searchPage.value = state.searchPage
 | 
			
		||||
    movies.value = state.movieList
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="container mx-auto px-4 py-8">
 | 
			
		||||
    <h1 class="text-3xl font-bold text-center mb-8">Add a Movie</h1>
 | 
			
		||||
 | 
			
		||||
    <SearchBar v-model="searchQuery" @submit="searchMovie" />
 | 
			
		||||
 | 
			
		||||
    <div v-if="isSearching" class="flex justify-center my-12">
 | 
			
		||||
      <span class="loading loading-ring loading-lg"></span>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <MovieList
 | 
			
		||||
      v-else-if="movies && movies.length > 0"
 | 
			
		||||
      :movies="movies"
 | 
			
		||||
      :loading-images="loadingImages"
 | 
			
		||||
      :loading-more="isLoadingMore"
 | 
			
		||||
      @loaded="handleLoaded"
 | 
			
		||||
      @loadMore="loadMore"
 | 
			
		||||
      :is-search="true"
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <p v-else class="text-center text-gray-500">Search for a movie to add it to your list</p>
 | 
			
		||||
 | 
			
		||||
    <div class="flex justify-center pt-2">
 | 
			
		||||
      <ErrorAlert v-if="seachError" :message="seachError" />
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
							
								
								
									
										41
									
								
								src/views/DetailsView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/views/DetailsView.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import MovieDetails from '@/components/MovieDetails.vue'
 | 
			
		||||
import { getMovie } from '@/lib/api'
 | 
			
		||||
import type { MovieDetailsType } from '@/types/Movie'
 | 
			
		||||
import { onMounted, ref } from 'vue'
 | 
			
		||||
import { useRoute } from 'vue-router'
 | 
			
		||||
 | 
			
		||||
const route = useRoute()
 | 
			
		||||
 | 
			
		||||
const movie = ref<MovieDetailsType>()
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
  try {
 | 
			
		||||
    const response = await getMovie(route.params.id as string)
 | 
			
		||||
    movie.value = response
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error(error)
 | 
			
		||||
    movie.value = {
 | 
			
		||||
      Title: '',
 | 
			
		||||
      Year: '',
 | 
			
		||||
      imdbID: '',
 | 
			
		||||
      Type: '',
 | 
			
		||||
      Poster: '',
 | 
			
		||||
      Plot: '',
 | 
			
		||||
      Language: '',
 | 
			
		||||
      Country: '',
 | 
			
		||||
      imdbRating: '',
 | 
			
		||||
      Error: true,
 | 
			
		||||
      ErrorMessage: (error as Error).message,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="container mx-auto px-4 py-8">
 | 
			
		||||
    <MovieDetails :movie="movie" />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
							
								
								
									
										40
									
								
								src/views/HomeView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/views/HomeView.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
<script setup lang="ts"></script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="container mx-auto px-4 py-8">
 | 
			
		||||
    <div class="flex flex-col items-center justify-center">
 | 
			
		||||
      <h1 class="text-5xl font-bold text-center mb-12">
 | 
			
		||||
        Welcome to <br /><span class="text-6xl font-bold text-center gradient-text"
 | 
			
		||||
          >To Be Watched</span
 | 
			
		||||
        >
 | 
			
		||||
      </h1>
 | 
			
		||||
 | 
			
		||||
      <p class="text-xl text-center mb-12">
 | 
			
		||||
        A simple and beautiful movie list app built with Vue 3 and Tailwind CSS (daisyUI).
 | 
			
		||||
      </p>
 | 
			
		||||
      <RouterLink to="/add" class="btn btn-primary btn-lg">Get Started</RouterLink>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.gradient-text {
 | 
			
		||||
  background: linear-gradient(to right, black 0%, red 50%, blue 100%);
 | 
			
		||||
  -webkit-background-clip: text;
 | 
			
		||||
  -webkit-text-fill-color: transparent;
 | 
			
		||||
  background-size: 200% auto;
 | 
			
		||||
  animation: gradient 3s ease infinite alternate;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes gradient {
 | 
			
		||||
  0% {
 | 
			
		||||
    background-position: left center;
 | 
			
		||||
  }
 | 
			
		||||
  100% {
 | 
			
		||||
    background-position: right center;
 | 
			
		||||
  }
 | 
			
		||||
  50% {
 | 
			
		||||
    animation-play-state: paused;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										32
									
								
								src/views/ListView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/views/ListView.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { useMoviesStore } from '@/stores/movies'
 | 
			
		||||
import MovieList from '@/components/MovieList.vue'
 | 
			
		||||
import { onMounted, ref } from 'vue'
 | 
			
		||||
 | 
			
		||||
const loadingImages = ref<Record<string, boolean>>({})
 | 
			
		||||
 | 
			
		||||
const store = useMoviesStore()
 | 
			
		||||
 | 
			
		||||
function handleLoaded(id: string) {
 | 
			
		||||
  loadingImages.value[id] = false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  loadingImages.value = Object.fromEntries(store.movieList.map((movie) => [movie.imdbID, true]))
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="container mx-auto px-4 py-8">
 | 
			
		||||
    <MovieList
 | 
			
		||||
      v-if="store.movieList"
 | 
			
		||||
      :movies="store.movieList"
 | 
			
		||||
      :loading-images="loadingImages"
 | 
			
		||||
      :loading-more="false"
 | 
			
		||||
      @loaded="handleLoaded"
 | 
			
		||||
      :is-search="false"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
							
								
								
									
										11
									
								
								src/views/NotFoundView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/views/NotFoundView.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
<script setup lang="ts"></script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="container mx-auto px-4 py-8">
 | 
			
		||||
    <h1 class="text-center">Not found</h1>
 | 
			
		||||
    <router-link to="/">Home</router-link>
 | 
			
		||||
    <div class="container mx-auto px-4 py-8"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
							
								
								
									
										12
									
								
								tsconfig.app.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								tsconfig.app.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
{
 | 
			
		||||
  "extends": "@vue/tsconfig/tsconfig.dom.json",
 | 
			
		||||
  "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
 | 
			
		||||
  "exclude": ["src/**/__tests__/*"],
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
 | 
			
		||||
 | 
			
		||||
    "paths": {
 | 
			
		||||
      "@/*": ["./src/*"]
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
{
 | 
			
		||||
  "files": [],
 | 
			
		||||
  "references": [
 | 
			
		||||
    {
 | 
			
		||||
      "path": "./tsconfig.node.json"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "path": "./tsconfig.app.json"
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								tsconfig.node.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								tsconfig.node.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
{
 | 
			
		||||
  "extends": "@tsconfig/node22/tsconfig.json",
 | 
			
		||||
  "include": [
 | 
			
		||||
    "vite.config.*",
 | 
			
		||||
    "vitest.config.*",
 | 
			
		||||
    "cypress.config.*",
 | 
			
		||||
    "nightwatch.conf.*",
 | 
			
		||||
    "playwright.config.*",
 | 
			
		||||
    "eslint.config.*"
 | 
			
		||||
  ],
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "noEmit": true,
 | 
			
		||||
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
 | 
			
		||||
 | 
			
		||||
    "module": "ESNext",
 | 
			
		||||
    "moduleResolution": "Bundler",
 | 
			
		||||
    "types": ["node"]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								vite.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								vite.config.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
import { fileURLToPath, URL } from 'node:url'
 | 
			
		||||
 | 
			
		||||
import { defineConfig } from 'vite'
 | 
			
		||||
import vue from '@vitejs/plugin-vue'
 | 
			
		||||
import vueDevTools from 'vite-plugin-vue-devtools'
 | 
			
		||||
import tailwindcss from '@tailwindcss/vite'
 | 
			
		||||
 | 
			
		||||
// https://vite.dev/config/
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  plugins: [
 | 
			
		||||
    vue(),  
 | 
			
		||||
    vueDevTools(),
 | 
			
		||||
    tailwindcss()
 | 
			
		||||
  ],
 | 
			
		||||
  resolve: {
 | 
			
		||||
    alias: {
 | 
			
		||||
      '@': fileURLToPath(new URL('./src', import.meta.url))
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
		Reference in New Issue
	
	Block a user