UI improvements and code simplification

This commit is contained in:
2025-10-22 18:47:28 +03:30
parent 33d181b54e
commit 138e7c3248
8 changed files with 51 additions and 54 deletions

View File

@@ -0,0 +1,26 @@
<script setup lang="ts"></script>
<template>
<router-link
to="/add"
class="card relative w-full h-full flex items-center justify-center aspect-[2/3] bg-white/40 backdrop-blur-md border-2 border-dashed border-gray-300 rounded-lg shadow-sm hover:shadow-md hover:border-gray-400 transition-all duration-300"
v-motion-fade-visible-once
>
<!-- Plus icon -->
<div
class="flex flex-col items-center justify-center text-gray-400 hover:text-gray-600 transition"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-16 h-16 mb-2"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
</svg>
<span class="text-lg font-semibold">Add More</span>
</div>
</router-link>
</template>

View File

@@ -5,13 +5,11 @@ import type { MovieType } from '@/types/Movie'
const props = defineProps<{ const props = defineProps<{
movie: MovieType movie: MovieType
loading: boolean | undefined
}>() }>()
const emit = defineEmits<{ (e: 'loaded', id: string): void }>()
const store = useMoviesStore() const store = useMoviesStore()
const imageLoadFailed = ref(false) const imageLoadFailed = ref(false)
const loaded = ref(false)
const alreadyAdded = computed(() => const alreadyAdded = computed(() =>
store.movieList.some((movie) => movie.imdbID === props.movie.imdbID), store.movieList.some((movie) => movie.imdbID === props.movie.imdbID),
@@ -26,7 +24,7 @@ const alreadyAdded = computed(() =>
<!-- Poster --> <!-- Poster -->
<router-link :to="{ name: 'details', params: { id: props.movie.imdbID } }"> <router-link :to="{ name: 'details', params: { id: props.movie.imdbID } }">
<figure class="overflow-hidden flex items-center justify-center aspect-[2/3] bg-gray-50"> <figure class="overflow-hidden flex items-center justify-center aspect-[2/3] bg-gray-50">
<span v-if="props.loading" class="loading loading-ring loading-lg text-primary"></span> <span v-if="!loaded" class="loading loading-ring loading-lg text-primary"></span>
<div v-else-if="imageLoadFailed" class="flex items-center justify-center"> <div v-else-if="imageLoadFailed" class="flex items-center justify-center">
<svg <svg
@@ -46,15 +44,15 @@ const alreadyAdded = computed(() =>
</div> </div>
<img <img
v-show="!props.loading && !imageLoadFailed" v-show="loaded && !imageLoadFailed"
:src="props.movie.Poster" :src="props.movie.Poster"
:alt="props.movie.Title" :alt="props.movie.Title"
class="object-cover w-full h-full transform transition-transform duration-500 hover:scale-105" class="object-cover w-full h-full transform transition-transform duration-500 hover:scale-105"
@load="emit('loaded', props.movie.imdbID)" @load="loaded = true"
@error=" @error="
() => { () => {
imageLoadFailed = true imageLoadFailed = true
emit('loaded', props.movie.imdbID) loaded = true
} }
" "
/> />
@@ -73,14 +71,16 @@ const alreadyAdded = computed(() =>
</router-link> </router-link>
<div class="card-actions justify-end mt-auto"> <div class="card-actions justify-end mt-auto">
<button v-motion-fade-visible-once <button
v-motion-fade-visible-once
v-if="!alreadyAdded" v-if="!alreadyAdded"
class="btn btn-sm px-4 bg-gradient-to-r from-gray-100 to-gray-200 border border-gray-300 text-gray-700 hover:from-gray-200 hover:to-gray-300 hover:text-gray-900 transition" class="btn btn-sm px-4 bg-gradient-to-r from-gray-100 to-gray-200 border border-gray-300 text-gray-700 hover:from-gray-200 hover:to-gray-300 hover:text-gray-900 transition"
@click="store.addMovie(props.movie)" @click="store.addMovie(props.movie)"
> >
Add Add
</button> </button>
<button v-motion-fade-visible-once <button
v-motion-fade-visible-once
v-else v-else
class="btn btn-sm px-4 bg-gradient-to-r from-red-50 to-red-100 border border-red-200 text-red-600 hover:from-red-100 hover:to-red-200 hover:text-red-700 transition" class="btn btn-sm px-4 bg-gradient-to-r from-red-50 to-red-100 border border-red-200 text-red-600 hover:from-red-100 hover:to-red-200 hover:text-red-700 transition"
@click="store.removeMovie(props.movie.imdbID)" @click="store.removeMovie(props.movie.imdbID)"

View File

@@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import AddMoreCard from './AddMoreCard.vue'
import MovieCard from './MovieCard.vue' import MovieCard from './MovieCard.vue'
import type { MovieType } from '@/types/Movie' import type { MovieType } from '@/types/Movie'
const props = defineProps<{ const props = defineProps<{
movies: MovieType[] movies: MovieType[]
loadingImages: Record<string, boolean>
loadingMore: boolean loadingMore: boolean
isSearch: boolean isSearch: boolean
}>() }>()
@@ -30,11 +30,10 @@ const emit = defineEmits<{ (e: 'loaded', id: string): void; (e: 'loadMore'): voi
class="grid gap-4 grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5" class="grid gap-4 grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5"
> >
<li v-for="movie in props.movies" :key="movie.imdbID" v-auto-animate> <li v-for="movie in props.movies" :key="movie.imdbID" v-auto-animate>
<MovieCard <MovieCard :movie="movie" />
:movie="movie" </li>
:loading="props.loadingImages[movie.imdbID]" <li>
@loaded="emit('loaded', $event)" <AddMoreCard />
/>
</li> </li>
</ul> </ul>

View File

@@ -4,6 +4,7 @@ import { ref } from 'vue'
const props = defineProps<{ modelValue: string }>() const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ (e: 'update:modelValue', value: string): void; (e: 'submit'): void }>() 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) const localQuery = ref(props.modelValue)
function onSubmit() { function onSubmit() {

View File

@@ -1,9 +1,9 @@
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router' import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [ const routes: RouteRecordRaw[] = [
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
name: 'NotFound', name: 'NotFound',
component: () => import('@/views/NotFoundView.vue'), component: () => import('@/views/NotFoundView.vue'),
}, },
{ {

View File

@@ -1,5 +1,4 @@
@import 'tailwindcss'; @import 'tailwindcss';
@plugin "daisyui" { @plugin "daisyui" {
themes: themes: lofi --default;
lofi --default
} }

View File

@@ -10,7 +10,6 @@ import { useSearchPageStore } from '@/stores/movies'
const movies = ref<MovieType[]>() const movies = ref<MovieType[]>()
const seachError = ref<string>('') const seachError = ref<string>('')
const searchQuery = ref('') const searchQuery = ref('')
const loadingImages = ref<Record<string, boolean>>({})
const searchPage = ref(1) const searchPage = ref(1)
const isSearching = ref(false) const isSearching = ref(false)
const isLoadingMore = ref(false) const isLoadingMore = ref(false)
@@ -28,19 +27,9 @@ async function searchMovie() {
return return
} }
seachError.value = '' seachError.value = ''
const previouslyLoaded = Object.keys(loadingImages.value).filter((id) =>
result.Search.some((m: MovieType) => m.imdbID === id),
)
movies.value = result.Search movies.value = result.Search
state.setState(searchPage.value, searchQuery.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) { } catch (error) {
movies.value = [] movies.value = []
console.error(error) console.error(error)
@@ -57,9 +46,6 @@ async function loadMore() {
const result = await loadMoreMovies(searchQuery.value, searchPage.value) const result = await loadMoreMovies(searchQuery.value, searchPage.value)
movies.value?.push(...result.Search) movies.value?.push(...result.Search)
state.setState(searchPage.value, searchQuery.value, movies.value ? movies.value : []) state.setState(searchPage.value, searchQuery.value, movies.value ? movies.value : [])
for (const m of result.Search) {
loadingImages.value[m.imdbID] = true
}
} catch (error) { } catch (error) {
searchPage.value = 1 searchPage.value = 1
seachError.value = (error as Error).message seachError.value = (error as Error).message
@@ -68,10 +54,6 @@ async function loadMore() {
} }
} }
function handleLoaded(id: string) {
loadingImages.value[id] = false
}
onMounted(() => { onMounted(() => {
if ( if (
state.searchQuery !== '' && state.searchQuery !== '' &&
@@ -122,9 +104,7 @@ watch(searchQuery, () => {
<MovieList <MovieList
v-else-if="movies && movies.length > 0" v-else-if="movies && movies.length > 0"
:movies="movies" :movies="movies"
:loading-images="loadingImages"
:loading-more="isLoadingMore" :loading-more="isLoadingMore"
@loaded="handleLoaded"
@loadMore="loadMore" @loadMore="loadMore"
:is-search="true" :is-search="true"
/> />

View File

@@ -1,30 +1,22 @@
<script setup lang="ts"> <script setup lang="ts">
import { useMoviesStore } from '@/stores/movies' import { useMoviesStore } from '@/stores/movies'
import MovieList from '@/components/MovieList.vue' import MovieList from '@/components/MovieList.vue'
import { onMounted, ref } from 'vue'
const loadingImages = ref<Record<string, boolean>>({})
const store = useMoviesStore() const store = useMoviesStore()
function handleLoaded(id: string) {
loadingImages.value[id] = false
}
onMounted(() => {
loadingImages.value = Object.fromEntries(store.movieList.map((movie) => [movie.imdbID, true]))
})
</script> </script>
<template> <template>
<div class="container mx-auto px-4 py-8" v-motion-fade-visible-once> <div class="container mx-auto px-4 py-12" v-motion-fade-visible-once>
<h1
class="text-4xl font-extrabold text-center mb-10 bg-gradient-to-r from-gray-700 to-gray-500 bg-clip-text text-transparent"
>
Movie Library
</h1>
<MovieList <MovieList
v-auto-animate v-auto-animate
v-if="store.movieList" v-if="store.movieList"
:movies="store.movieList" :movies="store.movieList"
:loading-images="loadingImages"
:loading-more="false" :loading-more="false"
@loaded="handleLoaded"
:is-search="false" :is-search="false"
/> />
</div> </div>