feat: add movie favorites

This commit is contained in:
Sergey Krylov 2025-02-10 06:44:04 +03:00
parent 8739570261
commit 3da3f4769c
13 changed files with 316 additions and 140 deletions

14
.editorconfig Normal file
View File

@ -0,0 +1,14 @@
# editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false

View File

@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/vue.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
<title>Pinia Vue</title>
</head>
<body>
<div id="app"></div>

View File

@ -9,6 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
"pinia": "^2.3.1",
"vue": "^3.5.13"
},
"devDependencies": {

40
pnpm-lock.yaml generated
View File

@ -8,6 +8,9 @@ importers:
.:
dependencies:
pinia:
specifier: ^2.3.1
version: 2.3.1(vue@3.5.13)
vue:
specifier: ^3.5.13
version: 3.5.13
@ -308,6 +311,9 @@ packages:
'@vue/compiler-ssr@3.5.13':
resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==}
'@vue/devtools-api@6.6.4':
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
'@vue/reactivity@3.5.13':
resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==}
@ -356,6 +362,15 @@ packages:
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
pinia@2.3.1:
resolution: {integrity: sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==}
peerDependencies:
typescript: '>=4.4.4'
vue: ^2.7.0 || ^3.5.11
peerDependenciesMeta:
typescript:
optional: true
postcss@8.5.1:
resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==}
engines: {node: ^10 || ^12 || >=14}
@ -409,6 +424,17 @@ packages:
yaml:
optional: true
vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
engines: {node: '>=12'}
hasBin: true
peerDependencies:
'@vue/composition-api': ^1.0.0-rc.1
vue: ^3.0.0-0 || ^2.6.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
vue@3.5.13:
resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==}
peerDependencies:
@ -603,6 +629,8 @@ snapshots:
'@vue/compiler-dom': 3.5.13
'@vue/shared': 3.5.13
'@vue/devtools-api@6.6.4': {}
'@vue/reactivity@3.5.13':
dependencies:
'@vue/shared': 3.5.13
@ -672,6 +700,14 @@ snapshots:
picocolors@1.1.1: {}
pinia@2.3.1(vue@3.5.13):
dependencies:
'@vue/devtools-api': 6.6.4
vue: 3.5.13
vue-demi: 0.14.10(vue@3.5.13)
transitivePeerDependencies:
- '@vue/composition-api'
postcss@8.5.1:
dependencies:
nanoid: 3.3.8
@ -713,6 +749,10 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
vue-demi@0.14.10(vue@3.5.13):
dependencies:
vue: 3.5.13
vue@3.5.13:
dependencies:
'@vue/compiler-dom': 3.5.13

15
public/logo.svg Normal file
View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" width="512" height="512" viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve">
<g>
<circle style="fill:#4AB8A1;" cx="64" cy="64" r="64"/>
<g>
<path style="fill:#3F6073;" d="M64,98c-18.748,0-34-15.252-34-34s15.252-34,34-34s34,15.252,34,34S82.748,98,64,98z"/>
<circle style="fill:#2D4452;" cx="64" cy="64" r="30"/>
<path style="fill:#2D4452;" d="M89,111c-1.104,0-2-0.896-2-2c0-4.963,4.037-9,9-9h1c1.654,0,3-1.346,3-3s-1.346-3-3-3H64 c-1.104,0-2-0.896-2-2s0.896-2,2-2h33c3.859,0,7,3.141,7,7s-3.141,7-7,7h-1c-2.757,0-5,2.243-5,5C91,110.104,90.104,111,89,111z"/>
<circle style="fill:#233540;" cx="64" cy="64" r="25"/>
<path style="fill:#F5F5F5;" d="M64,19c-24.854,0-45,20.146-45,45s20.146,45,45,45s45-20.146,45-45S88.854,19,64,19z M77.436,36.423c3.904-3.905,10.236-3.905,14.143,0s3.904,10.237,0,14.144c-3.905,3.904-10.237,3.904-14.143,0 C73.529,46.66,73.529,40.328,77.436,36.423z M25,64c0-5.521,4.479-10,10-10s10,4.479,10,10s-4.479,10-10,10S25,69.521,25,64z M50.564,91.577c-3.904,3.905-10.236,3.905-14.143,0s-3.904-10.237,0-14.142c3.905-3.906,10.237-3.906,14.143,0 C54.471,81.34,54.471,87.672,50.564,91.577z M50.564,50.564c-3.904,3.904-10.236,3.904-14.143,0 c-3.904-3.904-3.904-10.236,0-14.143c3.905-3.906,10.237-3.904,14.143,0C54.471,40.328,54.471,46.66,50.564,50.564z M64,103 c-5.521,0-10-4.479-10-10s4.479-10,10-10s10,4.479,10,10S69.521,103,64,103z M64,45c-5.521,0-10-4.479-10-10s4.479-10,10-10 s10,4.479,10,10S69.521,45,64,45z M91.577,91.577c-3.905,3.905-10.237,3.905-14.144,0c-3.904-3.905-3.904-10.237,0-14.142 c3.905-3.906,10.237-3.906,14.144,0C95.482,81.34,95.482,87.672,91.577,91.577z M93,74c-5.521,0-10-4.479-10-10s4.479-10,10-10 s10,4.479,10,10S98.521,74,93,74z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 496 B

After

Width:  |  Height:  |  Size: 496 B

View File

@ -1,30 +1,96 @@
<script setup>
import HelloWorld from './components/HelloWorld.vue'
import { useMovieStore} from './stores/movie-store';
import Movie from './components/Movie.vue';
const movieStore = useMovieStore();
</script>
<template>
<div>
<a href="https://vite.dev" target="_blank">
<img src="/vite.svg" class="logo" alt="Vite logo" />
</a>
<a href="https://vuejs.org/" target="_blank">
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
</a>
</div>
<HelloWorld msg="Vite + Vue" />
<main>
<header class="header">
<img src="/logo.svg" alt="logo" class="header-logo">
<h2>Favorite Movies</h2>
</header>
<div class="tabs">
<button
class="btn"
:class="{btn_green: movieStore.activeTab === 1}"
@click="movieStore.setActiveTab(1)"
>
Favorites
</button>
<button
class="btn"
:class="{btn_green: movieStore.activeTab === 2}"
@click="movieStore.setActiveTab(2)"
>
Search
</button>
</div>
<div class="movies" v-if="movieStore.activeTab === 1">
<h3 v-if="movieStore.watchedMovies.length > 0">Watched movies ({{movieStore.watchedMovies.length}})</h3>
<Movie
v-for="movie in movieStore.watchedMovies"
:movie="movie"
:key="movie.id"
@toggleWatched="movieStore.toggleWatchedMovie(movie.id)"
@remove="movieStore.removeMovie(movie.id)"
/>
<h3 v-if="movieStore.totalCount > 0">All movies ({{movieStore.totalCount}})</h3>
<Movie
v-for="movie in movieStore.movies"
:movie="movie"
:key="movie.id"
@toggleWatched="movieStore.toggleWatchedMovie(movie.id)"
@remove="movieStore.removeMovie(movie.id)"
/>
<h3 v-if="movieStore.totalCount === 0">
No movies
</h3>
</div>
<div class="search" v-if="movieStore.activeTab === 2">
Search
</div>
</main>
</template>
<style scoped>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
<style lang="css">
.header {
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
.header-logo {
max-width: 50px;
margin-right: 10px;
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
.btn {
border: none;
width: 100px;
height: 40px;
font-size: 14px;
margin: 0 10px;
border-radius: 10px;
cursor: pointer;
background: #efefef;
}
.btn:hover {
opacity: 0.7;
}
.btn_green {
background: #37df5c;
}
.tabs {
display: flex;
justify-content: center;
margin-bottom: 30px;
}
</style>

View File

@ -1,43 +0,0 @@
<script setup>
import { ref } from 'vue'
defineProps({
msg: String,
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

98
src/components/Movie.vue Normal file
View File

@ -0,0 +1,98 @@
<template>
<div class="movie">
<img
:src="`https://image.tmdb.org/t/p/w300_and_h450_bestv2${movie.poster_path}`"
:alt="movie.original_title"
class="movie-img"
>
<div>
<div class="movie-name">
{{movie.original_title}} ({{ new Date(movie.release_date).toLocaleDateString() }})
</div>
<span class="movie-overview">{{ movie.overview }}</span>
<div class="movie-buttons">
<button
class="btn movie-buttons-watched"
@click="$emit('toggle-watched')"
>
{{ movie.isWatched ? 'Not watched' : 'Watched' }}
</button>
<button
class="btn movie-buttons-delete"
@click="$emit('remove')"
>
Delete
</button>
</div>
</div>
</div>
</template>
<script setup>
defineEmits(['toggle-watched', 'remove'])
defineProps({
movie: {
type: Object,
required: true,
default: () => ({})
}
})
</script>
<style lang="css" scoped>
.movie {
display: grid;
grid-template-columns: 200px 1fr;
column-gap: 30px;
margin-bottom: 20px;
border: 2px solid #efefef;
padding: 10px;
border-radius: 10px;
}
.movie-accept {
margin-right: 10px;
}
.movie-img {
width: 200px;
height: 200px;
object-fit: cover;
border-radius: 50%;
}
.movie-name {
display: flex;
align-items: center;
font-size: 20px;
margin-bottom: 20px;
}
.movie-overview {
display: block;
margin-bottom: 20px;
}
.movie-buttons {
display: flex;
align-items: center;
justify-content: center;
}
.movie-buttons-watched {
color: #fff;
background: #1eb4c3;
}
.movie-buttons-watched__icon {
width: 15px;
margin-left: 10px;
}
.movie-buttons-delete {
color: #fff;
background: #ff2a2a;
}
</style>

View File

@ -1,5 +1,11 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { createPinia } from 'pinia';
createApp(App).mount('#app')
const app = createApp(App);
const pinia = createPinia()
app
.use(pinia)
.mount('#app')

47
src/stores/movie-store.js Normal file
View File

@ -0,0 +1,47 @@
import { defineStore } from 'pinia';
export const useMovieStore = defineStore('movieStore', {
state: () => ({
movies: [
{
id: 1,
original_title: "Spider-Man",
overview:
"After being bitten by a genetically altered spider at Oscorp, nerdy but endearing high school student Peter Parker is endowed with amazing powers to become the superhero known as Spider-Man.",
poster_path: "/gh4cZbhZxyTbgxQPxD0dOudNPTn.jpg",
release_date: "2002-05-01",
isWatched: false,
},
{
id: 2,
original_title: "The Batman",
overview:
"In his second year of fighting crime, Batman uncovers corruption in Gotham City that connects to his own family while facing a serial killer known as the Riddler.",
poster_path: "/b0PlSFdDwbyK0cf5RxwDpaOJQvQ.jpg",
release_date: "2022-03-01",
isWatched: true,
},
],
activeTab: 1,
}),
getters: {
watchedMovies: (store) => {
return store.movies.filter((el) => el.isWatched)
},
totalCount: (store) => {
return store.movies.length
}
},
actions: {
setActiveTab(tab) {
this.activeTab = tab
},
toggleWatchedMovie(id) {
const movie = this.movies.find((el) => el.id === id);
movie.isWatched = !movie.isWatched
},
removeMovie(id) {
this.movies = this.movies.filter((el) => el.id !== id)
}
}
})

View File

@ -1,79 +1,12 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
@import url("https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@300;400&display=swap");
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
background: #f9f9f9;
font-family: "Roboto Condensed", sans-serif;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
main {
max-width: 600px;
margin: auto;
}