UI improvements

This commit is contained in:
2025-10-21 16:48:14 +03:30
parent 173d37b57d
commit 1ac22b60ca
10 changed files with 291 additions and 135 deletions
+38 -10
View File
@@ -19,12 +19,21 @@ const alreadyAdded = computed(() =>
</script> </script>
<template> <template>
<div class="card bg-base-100 shadow-sm hover:shadow-md transition rounded-sm w-full h-full"> <div
class="card relative w-full h-full overflow-hidden
bg-white/70 backdrop-blur-md border border-gray-200/60
shadow-md hover:shadow-xl transition-all duration-300"
v-motion-fade-visible-once
>
<!-- Poster -->
<router-link :to="{ name: 'details', params: { id: props.movie.imdbID } }"> <router-link :to="{ name: 'details', params: { id: props.movie.imdbID } }">
<figure v-motion-fade-visible-once <figure
class="overflow-hidden flex items-center justify-center bg-gray-100 aspect-[2/3] rounded-t-sm" class="overflow-hidden flex items-center justify-center aspect-[2/3] bg-gray-50"
> >
<span v-if="props.loading" class="loading loading-ring loading-xl text-primary"></span> <span
v-if="props.loading"
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
@@ -33,7 +42,7 @@ const alreadyAdded = computed(() =>
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
class="w-12 h-12 text-gray-400" class="w-12 h-12 text-gray-300"
> >
<path <path
stroke-linecap="round" stroke-linecap="round"
@@ -47,7 +56,7 @@ const alreadyAdded = computed(() =>
v-show="!props.loading && !imageLoadFailed" v-show="!props.loading && !imageLoadFailed"
:src="props.movie.Poster" :src="props.movie.Poster"
:alt="props.movie.Title" :alt="props.movie.Title"
class="object-cover w-full h-full" class="object-cover w-full h-full transform transition-transform duration-500 hover:scale-105"
@load="emit('loaded', props.movie.imdbID)" @load="emit('loaded', props.movie.imdbID)"
@error=" @error="
() => { () => {
@@ -59,21 +68,40 @@ const alreadyAdded = computed(() =>
</figure> </figure>
</router-link> </router-link>
<!-- Body -->
<div class="card-body p-4 flex flex-col"> <div class="card-body p-4 flex flex-col">
<router-link :to="{ name: 'details', params: { id: props.movie.imdbID } }"> <router-link :to="{ name: 'details', params: { id: props.movie.imdbID } }">
<h2 class="card-title text-lg">{{ props.movie.Title }}</h2> <h2
<p class="text-sm text-gray-500">{{ props.movie.Year }}</p> class="card-title text-base font-semibold
bg-gradient-to-r from-gray-700 to-gray-500
bg-clip-text text-transparent"
>
{{ props.movie.Title }}
</h2>
<p class="text-sm text-gray-400">{{ props.movie.Year }}</p>
</router-link> </router-link>
<div class="card-actions justify-end mt-auto"> <div class="card-actions justify-end mt-auto">
<button <button
v-if="!alreadyAdded" v-if="!alreadyAdded"
class="btn btn-sm btn-primary" 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-else class="btn btn-sm btn-error" @click="store.removeMovie(props.movie.imdbID)"> <button
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"
@click="store.removeMovie(props.movie.imdbID)"
>
Delete Delete
</button> </button>
</div> </div>
+76 -47
View File
@@ -7,7 +7,6 @@ import { useMoviesStore } from '@/stores/movies'
const props = defineProps<{ movie: MovieDetailsType | undefined }>() const props = defineProps<{ movie: MovieDetailsType | undefined }>()
const imageLoaded = ref(false) const imageLoaded = ref(false)
const store = useMoviesStore() const store = useMoviesStore()
const alreadyAdded = computed(() => const alreadyAdded = computed(() =>
@@ -16,57 +15,87 @@ const alreadyAdded = computed(() =>
</script> </script>
<template> <template>
<ErrorAlert class="" v-if="props.movie?.Error" :message="props.movie.ErrorMessage" /> <ErrorAlert v-if="props.movie?.Error" :message="props.movie.ErrorMessage" />
<div v-else class="hero bg-base-200 min-h-screen rounded-md"> <div
<!-- Loading state for hero content --> v-else
<div v-if="props.movie === undefined" class="hero-content"> class="min-h-screen flex items-center justify-center px-6 py-12
bg-gradient-to-b from-gray-50 to-white"
>
<!-- Loading state -->
<div v-if="props.movie === undefined" class="flex justify-center items-center">
<span class="loading loading-ring loading-lg text-primary"></span> <span class="loading loading-ring loading-lg text-primary"></span>
</div> </div>
<!-- Actual hero content --> <!-- Hero content -->
<div v-else class="hero bg-base-200 min-h-screen rounded-md" v-motion-fade-visible-once> <div
<div class="hero-content flex-col lg:flex-row gap-12"> v-else
<!-- Poster --> class="flex flex-col lg:flex-row gap-12 items-center max-w-6xl w-full
<figure class="flex-shrink-0"> bg-white/70 backdrop-blur-md border border-gray-200/60
<span v-if="!imageLoaded" class="loading loading-ring loading-lg text-primary"></span> shadow-md rounded-xl p-8 transition"
<img v-motion-fade-visible-once
v-show="imageLoaded" >
:src="props.movie.Poster" <!-- Poster -->
alt="Poster" <figure class="flex-shrink-0">
class="w-full max-w-lg rounded-lg shadow-2xl" <span v-if="!imageLoaded" class="loading loading-ring loading-lg text-primary"></span>
@load="imageLoaded = true" <img
/> v-show="imageLoaded"
</figure> :src="props.movie.Poster"
alt="Poster"
class="w-full max-w-sm rounded-lg shadow-lg transform transition-transform duration-500 hover:scale-105"
@load="imageLoaded = true"
/>
</figure>
<!-- Text --> <!-- Text -->
<div class="max-w-2xl"> <div class="flex-1">
<h1 class="text-5xl font-bold"> <h1 class="text-4xl font-bold text-gray-800 mb-4">
{{ props.movie.Title }} {{ props.movie.Title }}
<span class="text-gray-500 text-sm">({{ props.movie.Year }})</span> <span class="text-gray-400 text-lg font-normal">({{ props.movie.Year }})</span>
</h1> </h1>
<p class="py-6">{{ props.movie.Plot }}</p>
<div class="flex flex-wrap gap-2"> <p class="text-gray-600 leading-relaxed mb-6">{{ props.movie.Plot }}</p>
<span class="badge badge-dash badge-primary">
Language: {{ props.movie.Language }} <!-- Badges -->
</span> <div class="flex flex-wrap gap-2 mb-6">
<span class="badge badge-dash badge-secondary"> <span
Country: {{ props.movie.Country }} class="px-3 py-1 rounded-md text-sm
</span> bg-gradient-to-r from-gray-100 to-gray-200 text-gray-700 border border-gray-300"
<span class="badge badge-dash badge-info"> >
IMDB Rating: {{ props.movie.imdbRating }} Language: {{ props.movie.Language }}
</span> </span>
</div> <span
<div v-if="!alreadyAdded" class="card-actions mt-4"> class="px-3 py-1 rounded-md text-sm
<button class="btn -sm btn-primary" @click="store.addMovie(props.movie)"> bg-gradient-to-r from-gray-100 to-gray-200 text-gray-700 border border-gray-300"
Add to list >
</button> Country: {{ props.movie.Country }}
</div> </span>
<div v-else class="card-actions mt-4"> <span
<button class="btn btn-error" @click="store.removeMovie(props.movie.imdbID)"> class="px-3 py-1 rounded-md text-sm
Remove from list bg-gradient-to-r from-gray-100 to-gray-200 text-gray-700 border border-gray-300"
</button> >
</div> IMDB Rating: {{ props.movie.imdbRating }}
</span>
</div>
<!-- Actions -->
<div class="card-actions">
<button
v-if="!alreadyAdded"
class="btn px-6 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"
@click="store.addMovie(props.movie)"
>
Add to list
</button>
<button
v-else
class="btn px-6 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"
@click="store.removeMovie(props.movie.imdbID)"
>
Remove from list
</button>
</div> </div>
</div> </div>
</div> </div>
+13 -14
View File
@@ -10,19 +10,24 @@ const props = defineProps<{
}>() }>()
const emit = defineEmits<{ (e: 'loaded', id: string): void; (e: 'loadMore'): void }>() const emit = defineEmits<{ (e: 'loaded', id: string): void; (e: 'loadMore'): void }>()
</script> </script>
<template> <template>
<div> <div>
<p v-if="props.movies.length === 0" class="text-center text-gray-500 py-12"> <p
v-if="props.movies.length === 0"
class="text-center text-gray-500 py-12 bg-gray-50/60 rounded-lg border border-gray-200"
>
No movies found.<br /> No movies found.<br />
<RouterLink to="/add" class="text-black font-bold">Add a movie</RouterLink> <RouterLink to="/add" class="text-gray-700 font-semibold hover:text-gray-900 transition">
Add a movie
</RouterLink>
</p> </p>
<ul v-auto-animate <ul
v-auto-animate
v-else v-else
class="grid gap-6 grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5" class="grid gap-6 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 lg:gap-8"
> >
<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
@@ -33,19 +38,13 @@ const emit = defineEmits<{ (e: 'loaded', id: string): void; (e: 'loadMore'): voi
</li> </li>
</ul> </ul>
<div <div v-if="props.isSearch && props.movies.length > 0" class="flex justify-center mt-8">
v-if="props.isSearch && props.movies.length > 0"
class="flex justify-center mt-8"
>
<button <button
class="btn btn-outline btn-secondary" class="btn px-6 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 disabled:opacity-50"
@click="emit('loadMore')" @click="emit('loadMore')"
:disabled="props.loadingMore" :disabled="props.loadingMore"
> >
<span <span v-if="props.loadingMore" class="loading loading-spinner loading-xs mr-2"></span>
v-if="props.loadingMore"
class="loading loading-spinner loading-xs mr-2"
></span>
Load more Load more
</button> </button>
</div> </div>
+44 -13
View File
@@ -1,11 +1,22 @@
<template> <template>
<div class="navbar bg-base-100 shadow-sm sticky top-0 z-10"> <div
class="navbar sticky top-0 z-20
bg-white/70 backdrop-blur-md border-b border-gray-200/60
shadow-sm transition"
>
<!-- Left -->
<div class="navbar-start"> <div class="navbar-start">
<!-- Mobile dropdown -->
<div class="dropdown"> <div class="dropdown">
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden" aria-label="Menu"> <div
tabindex="0"
role="button"
class="btn btn-ghost lg:hidden"
aria-label="Menu"
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5" class="h-6 w-6 text-gray-600"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -20,24 +31,44 @@
</div> </div>
<ul <ul
tabindex="-1" tabindex="-1"
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow" class="menu menu-sm dropdown-content
mt-3 w-52 p-2 rounded-lg shadow-md
bg-white/80 backdrop-blur-md border border-gray-200/60"
> >
<li><RouterLink class="text-xl" to="/">Home</RouterLink></li> <li><RouterLink class="text-gray-700 hover:text-gray-900" to="/">Home</RouterLink></li>
<li><RouterLink class="text-xl" to="/list">List</RouterLink></li> <li><RouterLink class="text-gray-700 hover:text-gray-900" to="/list">List</RouterLink></li>
<li><RouterLink class="text-xl" to="/add">Add</RouterLink></li> <li><RouterLink class="text-gray-700 hover:text-gray-900" to="/add">Add</RouterLink></li>
</ul> </ul>
</div> </div>
<RouterLink class="btn btn-ghost text-xl" to="/">To Vue</RouterLink>
<!-- Logo -->
<RouterLink
class="btn btn-ghost normal-case text-2xl font-bold
bg-gradient-to-r from-gray-700 to-gray-500 bg-clip-text text-transparent"
to="/"
>
To Vue
</RouterLink>
</div> </div>
<!-- Center (desktop menu) -->
<div class="navbar-center hidden lg:flex"> <div class="navbar-center hidden lg:flex">
<ul class="menu menu-horizontal px-1"> <ul class="menu menu-horizontal px-1 space-x-4">
<li><RouterLink class="text-xl" to="/">Home</RouterLink></li> <li><RouterLink class="text-gray-700 hover:text-gray-900 transition" to="/">Home</RouterLink></li>
<li><RouterLink class="text-xl" to="/list">List</RouterLink></li> <li><RouterLink class="text-gray-700 hover:text-gray-900 transition" to="/list">List</RouterLink></li>
<li><RouterLink class="text-xl" to="/add">Add</RouterLink></li> <li><RouterLink class="text-gray-700 hover:text-gray-900 transition" to="/add">Add</RouterLink></li>
</ul> </ul>
</div> </div>
<!-- Right -->
<div class="navbar-end"> <div class="navbar-end">
<RouterLink class="btn" to="/add">Get Started</RouterLink> <RouterLink
class="btn px-5 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"
to="/add"
>
Get Started
</RouterLink>
</div> </div>
</div> </div>
</template> </template>
+44 -24
View File
@@ -4,7 +4,6 @@ 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() {
@@ -13,30 +12,51 @@ function onSubmit() {
</script> </script>
<template> <template>
<form @submit.prevent="onSubmit"> <form @submit.prevent="onSubmit" class="flex justify-center mb-8">
<div class="join flex justify-center mb-8"> <div
<label class="input join-item"> class="flex items-center w-full max-w-md
<svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> bg-white/70 backdrop-blur-md border border-gray-200/60
<g shadow-sm hover:shadow-md transition
stroke-linejoin="round" rounded-lg overflow-hidden"
stroke-linecap="round" >
stroke-width="2.5" <!-- Icon -->
fill="none" <span class="pl-3 text-gray-400">
stroke="currentColor" <svg
> class="h-5 w-5"
<circle cx="11" cy="11" r="8"></circle> xmlns="http://www.w3.org/2000/svg"
<path d="m21 21-4.3-4.3"></path> viewBox="0 0 24 24"
</g> fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg> </svg>
<input </span>
type="search"
placeholder="Search for a movie" <!-- Input -->
required <input
:value="props.modelValue" type="search"
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)" placeholder="Search for a movie"
/> required
</label> :value="props.modelValue"
<!-- <button class="btn btn-neutral join-item">Search</button> --> @input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
class="flex-1 px-3 py-2 bg-transparent outline-none text-gray-700 placeholder-gray-400"
/>
<!-- Optional button -->
<!--
<button
type="submit"
class="px-4 py-2 bg-gradient-to-r from-gray-100 to-gray-200
border-l border-gray-200 text-gray-600 hover:text-gray-800
transition"
>
Search
</button>
-->
</div> </div>
</form> </form>
</template> </template>
+24 -6
View File
@@ -102,15 +102,24 @@ watch(searchQuery, () => {
</script> </script>
<template> <template>
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-12" v-motion-fade-visible-once>
<h1 class="text-3xl font-bold text-center mb-8">Add a Movie</h1> <!-- Title -->
<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"
>
Add a Movie
</h1>
<!-- Search bar -->
<SearchBar v-model="searchQuery" @submit="searchMovie" /> <SearchBar v-model="searchQuery" @submit="searchMovie" />
<div v-if="isSearching" class="flex justify-center my-12"> <!-- Loading spinner -->
<span class="loading loading-ring loading-lg"></span> <div v-if="isSearching" class="flex justify-center my-16">
<span class="loading loading-ring loading-lg text-primary"></span>
</div> </div>
<!-- Movie list -->
<MovieList <MovieList
v-else-if="movies && movies.length > 0" v-else-if="movies && movies.length > 0"
:movies="movies" :movies="movies"
@@ -121,10 +130,19 @@ watch(searchQuery, () => {
:is-search="true" :is-search="true"
/> />
<p v-else class="text-center text-gray-500">Search for a movie to add it to your list</p> <!-- Empty state -->
<p
v-else
class="text-center text-gray-500 mt-16
bg-gray-50/60 border border-gray-200 rounded-lg py-12"
>
Search for a movie to add it to your list
</p>
<div class="flex justify-center pt-2"> <!-- Error alert -->
<div class="flex justify-center pt-4">
<ErrorAlert v-if="seachError" :message="seachError" /> <ErrorAlert v-if="seachError" :message="seachError" />
</div> </div>
</div> </div>
</template> </template>
+1 -1
View File
@@ -34,7 +34,7 @@ onMounted(async () => {
<template> <template>
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-8">
<MovieDetails :movie="movie" /> <MovieDetails :movie="movie" v-motion-fade-visible-once />
</div> </div>
</template> </template>
+28 -15
View File
@@ -1,40 +1,53 @@
<script setup lang="ts"></script> <script setup lang="ts"></script>
<template> <template>
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-16" v-motion-fade-visible-once>
<div class="flex flex-col items-center justify-center"> <div class="flex flex-col items-center justify-center text-center">
<h1 class="text-5xl font-bold text-center mb-12"> <h1 class="text-5xl font-bold mb-8 leading-tight">
Welcome to <br /><span class="text-6xl font-bold text-center gradient-text" Welcome to <br />
>To Be Watched</span <span class="text-6xl font-extrabold gradient-text">
> To Be Watched
</span>
</h1> </h1>
<p class="text-xl text-center mb-12"> <p class="text-lg text-gray-600 max-w-2xl mb-12">
A simple and beautiful movie list app built with Vue 3 and Tailwind CSS (daisyUI). A simple and beautiful movie list app built with Vue&nbsp;3 and Tailwind&nbsp;CSS (daisyUI).
</p> </p>
<RouterLink to="/add" class="btn btn-primary btn-lg">Get Started</RouterLink>
<RouterLink
to="/add"
class="btn px-8 py-3 text-lg font-medium
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"
>
Get Started
</RouterLink>
</div> </div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.gradient-text { .gradient-text {
background: linear-gradient(to right, black 0%, red 50%, blue 100%); background: linear-gradient(
90deg,
#6b7280 0%, /* gray-500 */
#818cf8 50%, /* indigo-400 */
#22d3ee 100% /* cyan-400 */
);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-size: 200% auto; background-size: 200% auto;
animation: gradient 3s ease infinite alternate; animation: gradientShift 6s ease infinite alternate;
} }
@keyframes gradient { @keyframes gradientShift {
0% { 0% {
background-position: left center; background-position: left center;
} }
100% { 100% {
background-position: right center; background-position: right center;
} }
50% {
animation-play-state: paused;
}
} }
</style> </style>
+1 -1
View File
@@ -17,7 +17,7 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-8" v-motion-fade-visible-once>
<MovieList <MovieList
v-auto-animate v-auto-animate
v-if="store.movieList" v-if="store.movieList"
+22 -4
View File
@@ -1,10 +1,28 @@
<script setup lang="ts"></script> <script setup lang="ts"></script>
<template> <template>
<div class="container mx-auto px-4 py-8"> <div
<h1 class="text-center">Not found</h1> class="min-h-screen flex flex-col items-center justify-center bg-gradient-to-b from-gray-50 to-white px-4"
<router-link to="/">Home</router-link> >
<div class="container mx-auto px-4 py-8"></div> <!-- Big 404 -->
<h1
class="text-8xl font-extrabold mb-4 bg-gradient-to-r from-gray-700 to-gray-500 bg-clip-text text-transparent"
>
404
</h1>
<!-- Message -->
<p class="text-xl text-gray-600 mb-8 text-center">
Oops! The page youre looking for doesnt exist.
</p>
<!-- Button back home -->
<RouterLink
to="/"
class="btn px-6 py-3 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"
>
Back to Home
</RouterLink>
</div> </div>
</template> </template>