first commit

This commit is contained in:
2025-10-20 17:10:51 +03:30
commit 42b184d9e9
35 changed files with 4701 additions and 0 deletions

8
.editorconfig Normal file
View 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
View File

@@ -0,0 +1 @@
* text=auto eol=lf

30
.gitignore vendored Normal file
View 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
View File

@@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
}

8
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode"
]
}

48
README.md Normal file
View 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
View File

@@ -0,0 +1,2 @@
/// <reference types="vite/client" />
declare module '*.vue'

22
eslint.config.ts Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

2
public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Allow: /

12
src/App.vue Normal file
View 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>

View 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>

View 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>

View 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
View 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>

View 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>

View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,2 @@
@import 'tailwindcss';
@plugin "daisyui";

28
src/types/Movie.ts Normal file
View 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
View 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
View 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
View 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
View 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>

View 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
View 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
View File

@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

19
tsconfig.node.json Normal file
View 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
View 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))
},
},
})