mirror of
				https://github.com/mmahdium/TBW.git
				synced 2025-11-04 09:09:24 +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