feat: integrate Zustand for global state management;
This commit is contained in:
36
package-lock.json
generated
36
package-lock.json
generated
@@ -18,7 +18,8 @@
|
|||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"swr": "^2.3.6"
|
"swr": "^2.3.6",
|
||||||
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
@@ -1593,7 +1594,7 @@
|
|||||||
"version": "19.1.9",
|
"version": "19.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz",
|
||||||
"integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
|
"integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
@@ -1703,7 +1704,7 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/data-uri-to-buffer": {
|
"node_modules/data-uri-to-buffer": {
|
||||||
@@ -2770,6 +2771,35 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zustand": {
|
||||||
|
"version": "5.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz",
|
||||||
|
"integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=18.0.0",
|
||||||
|
"immer": ">=9.0.6",
|
||||||
|
"react": ">=18.0.0",
|
||||||
|
"use-sync-external-store": ">=1.2.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"immer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"use-sync-external-store": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"swr": "^2.3.6"
|
"swr": "^2.3.6",
|
||||||
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { MovieCard } from "@/components/atoms/MovieCard";
|
import { MovieCard } from '@/components/atoms/MovieCard';
|
||||||
import { ActorHero } from "@/components/molecules/ActorHero";
|
import { ActorHero } from '@/components/molecules/ActorHero';
|
||||||
import { Carousel } from "@/components/molecules/Carousel";
|
import { Carousel } from '@/components/molecules/Carousel';
|
||||||
import { Gallery } from "@/components/molecules/Gallery";
|
import { Gallery } from '@/components/molecules/Gallery';
|
||||||
import { convertToMovie } from "@/helpers/convertToMovie";
|
import { convertToMovie } from '@/helpers/convertToMovie';
|
||||||
import { TMDB } from "@/lib/tmdb";
|
import { TMDB } from '@/lib/tmdb';
|
||||||
import { FaStar } from "react-icons/fa";
|
import { FaStar } from 'react-icons/fa';
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page({
|
||||||
params,
|
params,
|
||||||
@@ -32,7 +32,7 @@ export default async function Page({
|
|||||||
new Date(b.release_date).getTime() -
|
new Date(b.release_date).getTime() -
|
||||||
new Date(a.release_date).getTime()
|
new Date(a.release_date).getTime()
|
||||||
)
|
)
|
||||||
.map((movie) => {
|
.map(movie => {
|
||||||
const convertedMovie = convertToMovie(movie);
|
const convertedMovie = convertToMovie(movie);
|
||||||
if (!convertedMovie) return null;
|
if (!convertedMovie) return null;
|
||||||
return <MovieCard key={movie.id} {...convertedMovie} />;
|
return <MovieCard key={movie.id} {...convertedMovie} />;
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { DB_getMovies } from '@/lib/db/pb';
|
|
||||||
import { NextResponse } from 'next/server';
|
|
||||||
|
|
||||||
export const GET = async () => {
|
|
||||||
const movies = await DB_getMovies();
|
|
||||||
const res = NextResponse.json(movies);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
};
|
|
||||||
@@ -2,8 +2,8 @@ import './globals.css';
|
|||||||
|
|
||||||
import { Navbar } from '@/components/organisms/Navbar';
|
import { Navbar } from '@/components/organisms/Navbar';
|
||||||
import { AuroraBackground } from '@/components/effects';
|
import { AuroraBackground } from '@/components/effects';
|
||||||
import { GlobalStoreProvider } from './store/globalStore';
|
|
||||||
import { DB_getMovies } from '@/lib/db/pb';
|
import { DB_getMovies } from '@/lib/db/pb';
|
||||||
|
import { GlobalProvider } from './store/global';
|
||||||
|
|
||||||
export default async function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
@@ -23,11 +23,11 @@ export default async function RootLayout({
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
</head>
|
</head>
|
||||||
<body className={`antialiased`}>
|
<body className={`antialiased`}>
|
||||||
<GlobalStoreProvider initialMovies={movies}>
|
<GlobalProvider initialMovies={movies}>
|
||||||
<AuroraBackground />
|
<AuroraBackground />
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<main className="relative [&>*:last-child]:pb-16">{children}</main>
|
<main className="relative [&>*:last-child]:pb-16">{children}</main>
|
||||||
</GlobalStoreProvider>
|
</GlobalProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default async function Home() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TrackedMovies />
|
<TrackedMovies />
|
||||||
<MovieList heading="Moja lista" />
|
<MovieList heading="Moja lista" showSearch />
|
||||||
<RandomMovie heading="Ciężko wybrać?" />
|
<RandomMovie heading="Ciężko wybrać?" />
|
||||||
<GenreList heading="Odkrywaj nowe filmy według gatunku" />
|
<GenreList heading="Odkrywaj nowe filmy według gatunku" />
|
||||||
</>
|
</>
|
||||||
|
|||||||
107
src/app/store/global.tsx
Normal file
107
src/app/store/global.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
'use client';
|
||||||
|
import { Spinner } from '@/components/atoms/Spinner';
|
||||||
|
import { DB_addMovie, DB_deleteMovie, DB_updateMovie } from '@/lib/db/pb';
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
FC,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { create, useStore } from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
initialMovies: Movie[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
movies: Movie[];
|
||||||
|
setMovies: (movies: Movie[]) => void;
|
||||||
|
addMovie: (movie: Movie) => void;
|
||||||
|
deleteMovie: (id: number) => void;
|
||||||
|
updateMovie: (id: number, movie: Partial<Movie>) => void;
|
||||||
|
displayType: 'grid' | 'list';
|
||||||
|
setDisplayType: (type: 'grid' | 'list') => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Store = ReturnType<typeof createStore>;
|
||||||
|
|
||||||
|
const createStore = ({ initialMovies }: Props) => {
|
||||||
|
return create<State>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
movies: initialMovies,
|
||||||
|
setMovies: (movies: Movie[]) => set({ movies }),
|
||||||
|
|
||||||
|
addMovie: (movie: Movie) => {
|
||||||
|
if (get().movies.find(m => m.id == movie.id)) return;
|
||||||
|
|
||||||
|
DB_addMovie(movie);
|
||||||
|
set(state => ({ movies: [...state.movies, movie] }));
|
||||||
|
},
|
||||||
|
deleteMovie: (id: number) => {
|
||||||
|
DB_deleteMovie(id);
|
||||||
|
set(state => ({
|
||||||
|
movies: state.movies.filter(m => m.id != id),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
updateMovie: (id: number, movie: Partial<Movie>) => {
|
||||||
|
DB_updateMovie(id, movie);
|
||||||
|
set(state => ({
|
||||||
|
movies: state.movies.map(m =>
|
||||||
|
m.id == id ? { ...m, ...movie } : m
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
displayType: 'grid',
|
||||||
|
setDisplayType: (type: 'grid' | 'list') => set({ displayType: type }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'global',
|
||||||
|
partialize: state => ({ displayType: state.displayType }),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GlobalContext = createContext<Store | null>(null);
|
||||||
|
|
||||||
|
export const GlobalProvider: FC<
|
||||||
|
{
|
||||||
|
children: React.ReactNode;
|
||||||
|
} & Props
|
||||||
|
> = ({ children, initialMovies = [] }) => {
|
||||||
|
const store = useRef(createStore({ initialMovies }));
|
||||||
|
const [firstRender, setFirstRender] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (firstRender) {
|
||||||
|
setFirstRender(false);
|
||||||
|
}
|
||||||
|
}, [firstRender]);
|
||||||
|
|
||||||
|
if (firstRender) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center h-screen bg-black/80">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GlobalContext.Provider value={store.current}>
|
||||||
|
{children}
|
||||||
|
</GlobalContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGlobalStore = <T,>(selector: (state: State) => T): T => {
|
||||||
|
const store = useContext(GlobalContext);
|
||||||
|
if (!store) throw new Error('GlobalStore not found');
|
||||||
|
return useStore(store, selector);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGlobalZustandStore = useGlobalStore;
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
'use client';
|
|
||||||
import { Spinner } from '@/components/atoms/Spinner';
|
|
||||||
import { useLocalStorage } from '@/hooks/useLocalStorage';
|
|
||||||
import { DB_addMovie, DB_deleteMovie, DB_updateMovie } from '@/lib/db/pb';
|
|
||||||
import { createContext, FC, use, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
type GlobalStore = {
|
|
||||||
movies: Movie[];
|
|
||||||
addMovie: (movie: Movie) => void;
|
|
||||||
deleteMovie: (id: number) => void;
|
|
||||||
updateMovie: (id: number, movie: Partial<Movie>) => void;
|
|
||||||
displayType: 'grid' | 'list';
|
|
||||||
setDisplayType: (type: 'grid' | 'list') => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const globalStore = createContext<GlobalStore>({
|
|
||||||
movies: [],
|
|
||||||
addMovie: () => {},
|
|
||||||
deleteMovie: () => {},
|
|
||||||
updateMovie: () => {},
|
|
||||||
displayType: 'grid',
|
|
||||||
setDisplayType: () => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
children: React.ReactNode;
|
|
||||||
initialMovies?: Movie[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GlobalStoreProvider: FC<Props> = ({
|
|
||||||
children,
|
|
||||||
initialMovies = [],
|
|
||||||
}) => {
|
|
||||||
// Optimistic update
|
|
||||||
const [firstRender, setFirstRender] = useState(true);
|
|
||||||
const [movies, setMovies] = useState<GlobalStore['movies']>(initialMovies);
|
|
||||||
const [displayType, setDisplayType] = useLocalStorage<
|
|
||||||
GlobalStore['displayType']
|
|
||||||
>('displayType', 'grid');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (firstRender) {
|
|
||||||
setFirstRender(false);
|
|
||||||
}
|
|
||||||
}, [firstRender]);
|
|
||||||
|
|
||||||
const addMovie = async (movie: Movie) => {
|
|
||||||
if (movies.find(m => m.id === movie.id)) return;
|
|
||||||
|
|
||||||
DB_addMovie(movie);
|
|
||||||
setMovies(prev => [...prev, movie]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteMovie = async (id: number) => {
|
|
||||||
DB_deleteMovie(id);
|
|
||||||
setMovies(prev => prev.filter(m => m.id !== id));
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateMovie = async (id: number, movie: Partial<Movie>) => {
|
|
||||||
DB_updateMovie(id, movie);
|
|
||||||
setMovies(prev => prev.map(m => (m.id === id ? { ...m, ...movie } : m)));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<globalStore.Provider
|
|
||||||
value={{
|
|
||||||
movies,
|
|
||||||
addMovie,
|
|
||||||
deleteMovie,
|
|
||||||
updateMovie,
|
|
||||||
displayType,
|
|
||||||
setDisplayType,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{firstRender ? (
|
|
||||||
<div className="flex justify-center items-center h-screen bg-black/80">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
children
|
|
||||||
)}
|
|
||||||
</globalStore.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useGlobalStore = () => {
|
|
||||||
return use(globalStore);
|
|
||||||
};
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { useGlobalStore } from '@/app/store/globalStore';
|
|
||||||
import { FaFire, FaPlusCircle, FaTrash } from 'react-icons/fa';
|
import { FaFire, FaPlusCircle, FaTrash } from 'react-icons/fa';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { RxEyeOpen } from 'react-icons/rx';
|
import { RxEyeOpen } from 'react-icons/rx';
|
||||||
import { MdFavorite } from 'react-icons/md';
|
import { MdFavorite } from 'react-icons/md';
|
||||||
import { RiCalendarCheckLine, RiCalendarScheduleLine } from 'react-icons/ri';
|
import { RiCalendarCheckLine, RiCalendarScheduleLine } from 'react-icons/ri';
|
||||||
|
import { useGlobalStore } from '@/app/store/global';
|
||||||
|
|
||||||
type Props = Movie & {
|
type Props = Movie & {
|
||||||
showDayCounter?: boolean;
|
showDayCounter?: boolean;
|
||||||
@@ -17,16 +17,14 @@ export const MovieCard: FC<Props> = ({
|
|||||||
simpleToggle = false,
|
simpleToggle = false,
|
||||||
...movie
|
...movie
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const movies = useGlobalStore(state => state.movies);
|
||||||
movies,
|
const addMovie = useGlobalStore(state => state.addMovie);
|
||||||
addMovie: addMovieToStore,
|
const deleteMovie = useGlobalStore(state => state.deleteMovie);
|
||||||
deleteMovie: deleteMovieFromStore,
|
const updateMovie = useGlobalStore(state => state.updateMovie);
|
||||||
updateMovie: updateMovieInStore,
|
|
||||||
} = useGlobalStore();
|
|
||||||
const { vote_average, popularity, poster_path, title, overview } = movie;
|
const { vote_average, popularity, poster_path, title, overview } = movie;
|
||||||
|
|
||||||
const { id } = movie;
|
const { id } = movie;
|
||||||
const alreadyInStore = movies.find(m => m.id === id);
|
const alreadyInStore = movies.find(m => m.id == id);
|
||||||
|
|
||||||
const seen = alreadyInStore?.seen || movie.seen;
|
const seen = alreadyInStore?.seen || movie.seen;
|
||||||
const favorite = alreadyInStore?.favorite || movie.favorite;
|
const favorite = alreadyInStore?.favorite || movie.favorite;
|
||||||
@@ -40,22 +38,22 @@ export const MovieCard: FC<Props> = ({
|
|||||||
: 'from-red-400 to-pink-400';
|
: 'from-red-400 to-pink-400';
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
addMovieToStore(movie);
|
addMovie(movie);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemove = () => {
|
const handleRemove = () => {
|
||||||
deleteMovieFromStore(id);
|
deleteMovie(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSeen = () => {
|
const handleSeen = () => {
|
||||||
updateMovieInStore(id, {
|
updateMovie(id, {
|
||||||
seen: !movie.seen,
|
seen: !movie.seen,
|
||||||
favorite: false,
|
favorite: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFavorite = () => {
|
const handleFavorite = () => {
|
||||||
updateMovieInStore(id, {
|
updateMovie(id, {
|
||||||
favorite: !movie.favorite,
|
favorite: !movie.favorite,
|
||||||
seen: movie.seen || !movie.favorite,
|
seen: movie.seen || !movie.favorite,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { formatter } from '@/helpers/formater';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { FaCalendar, FaClock, FaStar, FaEye, FaHeart } from 'react-icons/fa';
|
import { FaCalendar, FaClock, FaStar, FaEye, FaHeart } from 'react-icons/fa';
|
||||||
import { motion, useAnimationControls, useMotionValue } from 'framer-motion';
|
import { useGlobalStore } from '@/app/store/global';
|
||||||
import { useGlobalStore } from '@/app/store/globalStore';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
movie: Movie;
|
movie: Movie;
|
||||||
@@ -17,10 +16,7 @@ export const MovieRow: FC<Props> = ({
|
|||||||
isUpcoming = false,
|
isUpcoming = false,
|
||||||
compact = false,
|
compact = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { movies, addMovie, updateMovie } = useGlobalStore();
|
const movies = useGlobalStore(state => state.movies);
|
||||||
|
|
||||||
const dragControls = useAnimationControls();
|
|
||||||
const x = useMotionValue(0);
|
|
||||||
|
|
||||||
const daysSinceRelease = Math.abs(
|
const daysSinceRelease = Math.abs(
|
||||||
Math.floor(
|
Math.floor(
|
||||||
@@ -30,128 +26,77 @@ export const MovieRow: FC<Props> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Check if movie is already in store.
|
// Check if movie is already in store.
|
||||||
const movieInStore = movies.find(m => m.id === movie.id);
|
const movieInStore = movies.find(m => m.id == movie.id);
|
||||||
const isWatched = movieInStore?.seen || false;
|
const isWatched = movieInStore?.seen || false;
|
||||||
const isFavorite = movieInStore?.favorite || false;
|
const isFavorite = movieInStore?.favorite || false;
|
||||||
|
|
||||||
const handleMarkAsWatched = () => {
|
|
||||||
if (movieInStore) {
|
|
||||||
updateMovie(movie.id, { seen: !isWatched });
|
|
||||||
} else {
|
|
||||||
addMovie({ ...movie, seen: true, favorite: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddToFavorites = () => {
|
|
||||||
if (movieInStore) {
|
|
||||||
updateMovie(movie.id, { favorite: !isFavorite, seen: true });
|
|
||||||
} else {
|
|
||||||
addMovie({ ...movie, seen: true, favorite: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragAction = () => {
|
|
||||||
const threshold = 70;
|
|
||||||
if (x.get() > threshold) {
|
|
||||||
handleAddToFavorites();
|
|
||||||
} else if (x.get() < -threshold) {
|
|
||||||
handleMarkAsWatched();
|
|
||||||
}
|
|
||||||
dragControls.start({
|
|
||||||
x: 0,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative overflow-hidden rounded-xl">
|
<div className="relative overflow-hidden rounded-xl">
|
||||||
{/* Background actions */}
|
<Link
|
||||||
<div className="absolute inset-0 flex">
|
href={`/film/${movie.id}`}
|
||||||
<div className="absolute right-0 h-full w-24 bg-green-500/20 flex items-center justify-center cursor-pointer">
|
draggable={false}
|
||||||
<FaEye className="w-5 h-5 transition-colors text-green-500" />
|
className="flex items-center gap-4 p-3 rounded-lg bg-gray-800 hover:bg-gray-800 transition-colors group"
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="absolute left-0 h-full w-24 bg-red-500/20 flex items-center justify-center cursor-pointer">
|
|
||||||
<FaHeart className="w-5 h-5 transition-colors text-red-500" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
drag="x"
|
|
||||||
style={{ x }}
|
|
||||||
animate={dragControls}
|
|
||||||
dragConstraints={{ left: -80, right: 80 }}
|
|
||||||
dragElastic={0.01}
|
|
||||||
dragMomentum={false}
|
|
||||||
whileDrag={{ cursor: 'grabbing' }}
|
|
||||||
onDragEnd={handleDragAction}
|
|
||||||
className="relative z-10"
|
|
||||||
>
|
>
|
||||||
<Link
|
<div className="relative w-12 h-16 rounded overflow-hidden flex-shrink-0">
|
||||||
href={`/film/${movie.id}`}
|
<img
|
||||||
draggable={false}
|
src={`https://image.tmdb.org/t/p/w154${movie.poster_path}`}
|
||||||
className="flex items-center gap-4 p-3 rounded-lg bg-gray-800 hover:bg-gray-800 transition-colors group"
|
alt={movie.title}
|
||||||
>
|
className="object-cover inset-0"
|
||||||
<div className="relative w-12 h-16 rounded overflow-hidden flex-shrink-0">
|
sizes="48px"
|
||||||
<img
|
/>
|
||||||
src={`https://image.tmdb.org/t/p/w154${movie.poster_path}`}
|
</div>
|
||||||
alt={movie.title}
|
|
||||||
className="object-cover inset-0"
|
|
||||||
sizes="48px"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h3 className="text-white font-medium text-sm truncate group-hover:text-blue-400 transition-colors">
|
<h3 className="text-white font-medium text-sm truncate group-hover:text-blue-400 transition-colors">
|
||||||
{movie.title}
|
{movie.title}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex items-center gap-3 mt-1">
|
<div className="flex items-center gap-3 mt-1">
|
||||||
<div className="flex items-center gap-1 text-gray-400 text-xs">
|
<div className="flex items-center gap-1 text-gray-400 text-xs">
|
||||||
{isUpcoming ? (
|
{isUpcoming ? (
|
||||||
<FaCalendar className="w-3 h-3" />
|
<FaCalendar className="w-3 h-3" />
|
||||||
) : (
|
) : (
|
||||||
<FaClock className="w-3 h-3" />
|
<FaClock className="w-3 h-3" />
|
||||||
)}
|
)}
|
||||||
<span>{formatter.formatDate(movie.release_date)}</span>
|
<span>{formatter.formatDate(movie.release_date)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!!movie.vote_average && (
|
||||||
|
<div className="flex items-center gap-1 text-yellow-400 text-xs">
|
||||||
|
<FaStar className="w-3 h-3 fill-current" />
|
||||||
|
<span>{movie.vote_average.toFixed(1)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{!!movie.vote_average && (
|
{(isFavorite || movie.favorite) && (
|
||||||
<div className="flex items-center gap-1 text-yellow-400 text-xs">
|
<div
|
||||||
<FaStar className="w-3 h-3 fill-current" />
|
className="w-2 h-2 bg-red-500 rounded-full"
|
||||||
<span>{movie.vote_average.toFixed(1)}</span>
|
title="Ulubione"
|
||||||
</div>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(isFavorite || movie.favorite) && (
|
{(isWatched || movie.seen) && (
|
||||||
<div
|
<div
|
||||||
className="w-2 h-2 bg-red-500 rounded-full"
|
className="w-2 h-2 bg-green-500 rounded-full"
|
||||||
title="Ulubione"
|
title="Obejrzane"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(isWatched || movie.seen) && (
|
|
||||||
<div
|
|
||||||
className="w-2 h-2 bg-green-500 rounded-full"
|
|
||||||
title="Obejrzane"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{!compact && (
|
{!compact && (
|
||||||
<div
|
<div
|
||||||
className={`text-xs px-2 py-1 rounded-full font-medium ${
|
className={`text-xs px-2 py-1 rounded-full font-medium ${
|
||||||
isUpcoming
|
isUpcoming
|
||||||
? 'bg-blue-500/20 text-blue-400'
|
? 'bg-blue-500/20 text-blue-400'
|
||||||
: 'bg-green-500/20 text-green-400'
|
: 'bg-green-500/20 text-green-400'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isUpcoming
|
{isUpcoming
|
||||||
? `za ${daysSinceRelease} dni`
|
? `za ${daysSinceRelease} dni`
|
||||||
: `od ${daysSinceRelease} dni`}
|
: `od ${daysSinceRelease} dni`}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"use client";
|
'use client';
|
||||||
|
|
||||||
import { BackButton } from "@/components/atoms/BackButton";
|
import { BackButton } from '@/components/atoms/BackButton';
|
||||||
import { formatter } from "@/helpers/formater";
|
import { formatter } from '@/helpers/formater';
|
||||||
import { PersonDetailsRich } from "@/lib/tmdb/types";
|
import { PersonDetailsRich } from '@/lib/tmdb/types';
|
||||||
import { FC } from "react";
|
import { FC } from 'react';
|
||||||
import {
|
import {
|
||||||
FaCalendarAlt,
|
FaCalendarAlt,
|
||||||
FaMapMarkerAlt,
|
FaMapMarkerAlt,
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
FaTwitter,
|
FaTwitter,
|
||||||
FaYoutube,
|
FaYoutube,
|
||||||
FaTiktok,
|
FaTiktok,
|
||||||
} from "react-icons/fa";
|
} from 'react-icons/fa';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
personDetails: PersonDetailsRich;
|
personDetails: PersonDetailsRich;
|
||||||
@@ -37,13 +37,13 @@ export const ActorHero: FC<Props> = ({ personDetails }) => {
|
|||||||
const getGenderText = (gender: number) => {
|
const getGenderText = (gender: number) => {
|
||||||
switch (gender) {
|
switch (gender) {
|
||||||
case 1:
|
case 1:
|
||||||
return "Kobieta";
|
return 'Kobieta';
|
||||||
case 2:
|
case 2:
|
||||||
return "Mężczyzna";
|
return 'Mężczyzna';
|
||||||
case 3:
|
case 3:
|
||||||
return "Niebinarne";
|
return 'Niebinarne';
|
||||||
default:
|
default:
|
||||||
return "Nie określono";
|
return 'Nie określono';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ export const ActorHero: FC<Props> = ({ personDetails }) => {
|
|||||||
src={
|
src={
|
||||||
personDetails.profile_path
|
personDetails.profile_path
|
||||||
? `https://image.tmdb.org/t/p/w500${personDetails.profile_path}`
|
? `https://image.tmdb.org/t/p/w500${personDetails.profile_path}`
|
||||||
: "/api/placeholder/400/600"
|
: '/api/placeholder/400/600'
|
||||||
}
|
}
|
||||||
alt={personDetails.name}
|
alt={personDetails.name}
|
||||||
className="w-80 h-auto rounded-2xl shadow-2xl shadow-purple-500/20 group-hover:shadow-purple-500/40 transition-all duration-500"
|
className="w-80 h-auto rounded-2xl shadow-2xl shadow-purple-500/20 group-hover:shadow-purple-500/40 transition-all duration-500"
|
||||||
@@ -88,9 +88,9 @@ export const ActorHero: FC<Props> = ({ personDetails }) => {
|
|||||||
{calculateAge(
|
{calculateAge(
|
||||||
personDetails.birthday,
|
personDetails.birthday,
|
||||||
personDetails.deathday
|
personDetails.deathday
|
||||||
)}{" "}
|
)}{' '}
|
||||||
lat
|
lat
|
||||||
{personDetails.deathday && " w chwili śmierci"})
|
{personDetails.deathday && ' w chwili śmierci'})
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -114,8 +114,8 @@ export const ActorHero: FC<Props> = ({ personDetails }) => {
|
|||||||
{personDetails.also_known_as.length > 0 && (
|
{personDetails.also_known_as.length > 0 && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<p className="text-gray-400 text-sm">
|
<p className="text-gray-400 text-sm">
|
||||||
Znany również jako:{" "}
|
Znany również jako:{' '}
|
||||||
{personDetails.also_known_as.slice(0, 3).join(", ")}
|
{personDetails.also_known_as.slice(0, 3).join(', ')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -171,7 +171,7 @@ export const ActorHero: FC<Props> = ({ personDetails }) => {
|
|||||||
</h3>
|
</h3>
|
||||||
<div className="text-gray-300 leading-relaxed text-lg space-y-4">
|
<div className="text-gray-300 leading-relaxed text-lg space-y-4">
|
||||||
{personDetails.biography
|
{personDetails.biography
|
||||||
.split("\n\n")
|
.split('\n\n')
|
||||||
.map((paragraph, index) => (
|
.map((paragraph, index) => (
|
||||||
<p key={index}>{paragraph}</p>
|
<p key={index}>{paragraph}</p>
|
||||||
))}
|
))}
|
||||||
@@ -185,7 +185,7 @@ export const ActorHero: FC<Props> = ({ personDetails }) => {
|
|||||||
<h3 className="text-lg font-semibold mb-3 text-purple-300">
|
<h3 className="text-lg font-semibold mb-3 text-purple-300">
|
||||||
Linki
|
Linki
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex gap-4">
|
<div className="flex flex-wrap gap-4">
|
||||||
{Object.entries(personDetails.external_ids).map(
|
{Object.entries(personDetails.external_ids).map(
|
||||||
([key, value]) => {
|
([key, value]) => {
|
||||||
if (!(key in externalIdsMap) || !value) {
|
if (!(key in externalIdsMap) || !value) {
|
||||||
@@ -222,32 +222,32 @@ export const ActorHero: FC<Props> = ({ personDetails }) => {
|
|||||||
|
|
||||||
const externalIdsMap = {
|
const externalIdsMap = {
|
||||||
facebook_id: {
|
facebook_id: {
|
||||||
label: "Facebook",
|
label: 'Facebook',
|
||||||
icon: <FaFacebook />,
|
icon: <FaFacebook />,
|
||||||
url: (id: string) => `https://www.facebook.com/${id}`,
|
url: (id: string) => `https://www.facebook.com/${id}`,
|
||||||
},
|
},
|
||||||
instagram_id: {
|
instagram_id: {
|
||||||
label: "Instagram",
|
label: 'Instagram',
|
||||||
icon: <FaInstagram />,
|
icon: <FaInstagram />,
|
||||||
url: (id: string) => `https://www.instagram.com/${id}`,
|
url: (id: string) => `https://www.instagram.com/${id}`,
|
||||||
},
|
},
|
||||||
twitter_id: {
|
twitter_id: {
|
||||||
label: "Twitter",
|
label: 'Twitter',
|
||||||
icon: <FaTwitter />,
|
icon: <FaTwitter />,
|
||||||
url: (id: string) => `https://www.twitter.com/${id}`,
|
url: (id: string) => `https://www.twitter.com/${id}`,
|
||||||
},
|
},
|
||||||
tiktok_id: {
|
tiktok_id: {
|
||||||
label: "TikTok",
|
label: 'TikTok',
|
||||||
icon: <FaTiktok />,
|
icon: <FaTiktok />,
|
||||||
url: (id: string) => `https://www.tiktok.com/${id}`,
|
url: (id: string) => `https://www.tiktok.com/${id}`,
|
||||||
},
|
},
|
||||||
youtube_id: {
|
youtube_id: {
|
||||||
label: "YouTube",
|
label: 'YouTube',
|
||||||
icon: <FaYoutube />,
|
icon: <FaYoutube />,
|
||||||
url: (id: string) => `https://www.youtube.com/${id}`,
|
url: (id: string) => `https://www.youtube.com/${id}`,
|
||||||
},
|
},
|
||||||
imdb_id: {
|
imdb_id: {
|
||||||
label: "IMDb",
|
label: 'IMDb',
|
||||||
icon: <FaImdb />,
|
icon: <FaImdb />,
|
||||||
url: (id: string) => `https://www.imdb.com/name/${id}`,
|
url: (id: string) => `https://www.imdb.com/name/${id}`,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
"use client";
|
'use client';
|
||||||
import { BackButton } from "@/components/atoms/BackButton";
|
import { BackButton } from '@/components/atoms/BackButton';
|
||||||
import { Button } from "@/components/atoms/Button";
|
import { Button } from '@/components/atoms/Button';
|
||||||
import { GenreLabel } from "@/components/atoms/GenreLabel";
|
import { GenreLabel } from '@/components/atoms/GenreLabel';
|
||||||
import { MovieDetailsRich } from "@/lib/tmdb/types";
|
import { MovieDetailsRich } from '@/lib/tmdb/types';
|
||||||
import { useGlobalStore } from "@/app/store/globalStore";
|
import { FC } from 'react';
|
||||||
import { FC } from "react";
|
|
||||||
import {
|
import {
|
||||||
FaHeart,
|
FaHeart,
|
||||||
FaBookmark,
|
FaBookmark,
|
||||||
@@ -12,19 +11,23 @@ import {
|
|||||||
FaCalendar,
|
FaCalendar,
|
||||||
FaGlobe,
|
FaGlobe,
|
||||||
FaEye,
|
FaEye,
|
||||||
} from "react-icons/fa";
|
} from 'react-icons/fa';
|
||||||
import { convertToMovie } from "@/helpers/convertToMovie";
|
import { convertToMovie } from '@/helpers/convertToMovie';
|
||||||
import { formatter } from "@/helpers/formater";
|
import { formatter } from '@/helpers/formater';
|
||||||
|
import { useGlobalStore } from '@/app/store/global';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
movieDetails: MovieDetailsRich;
|
movieDetails: MovieDetailsRich;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HeroMovie: FC<Props> = ({ movieDetails }) => {
|
export const HeroMovie: FC<Props> = ({ movieDetails }) => {
|
||||||
const { movies, addMovie, deleteMovie, updateMovie } = useGlobalStore();
|
const movies = useGlobalStore(state => state.movies);
|
||||||
|
const addMovie = useGlobalStore(state => state.addMovie);
|
||||||
|
const deleteMovie = useGlobalStore(state => state.deleteMovie);
|
||||||
|
const updateMovie = useGlobalStore(state => state.updateMovie);
|
||||||
|
|
||||||
// Check if movie is in store and get its state.
|
// Check if movie is in store and get its state.
|
||||||
const movieInStore = movies.find((m) => m.id === movieDetails.id);
|
const movieInStore = movies.find(m => m.id == movieDetails.id);
|
||||||
const isInStore = !!movieInStore;
|
const isInStore = !!movieInStore;
|
||||||
const isFavorite = movieInStore?.favorite || false;
|
const isFavorite = movieInStore?.favorite || false;
|
||||||
const isSeen = movieInStore?.seen || false;
|
const isSeen = movieInStore?.seen || false;
|
||||||
@@ -115,8 +118,8 @@ export const HeroMovie: FC<Props> = ({ movieDetails }) => {
|
|||||||
key={i}
|
key={i}
|
||||||
className={`text-2xl ${
|
className={`text-2xl ${
|
||||||
i < Math.round(movieDetails.vote_average / 2)
|
i < Math.round(movieDetails.vote_average / 2)
|
||||||
? "text-yellow-400"
|
? 'text-yellow-400'
|
||||||
: "text-gray-600"
|
: 'text-gray-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
★
|
★
|
||||||
@@ -173,7 +176,7 @@ export const HeroMovie: FC<Props> = ({ movieDetails }) => {
|
|||||||
Gatunki
|
Gatunki
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{movieDetails.genres.map((genre) => (
|
{movieDetails.genres.map(genre => (
|
||||||
<GenreLabel
|
<GenreLabel
|
||||||
key={genre.id}
|
key={genre.id}
|
||||||
genre={genre.name}
|
genre={genre.name}
|
||||||
@@ -203,47 +206,47 @@ export const HeroMovie: FC<Props> = ({ movieDetails }) => {
|
|||||||
gradient={
|
gradient={
|
||||||
isInStore
|
isInStore
|
||||||
? {
|
? {
|
||||||
from: "from-purple-600 hover:from-purple-500",
|
from: 'from-purple-600 hover:from-purple-500',
|
||||||
to: "to-emerald-600 hover:to-emerald-500",
|
to: 'to-emerald-600 hover:to-emerald-500',
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
from: "from-purple-600 hover:from-purple-500",
|
from: 'from-purple-600 hover:from-purple-500',
|
||||||
to: "to-pink-600 hover:to-pink-500",
|
to: 'to-pink-600 hover:to-pink-500',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
className={`flex items-center gap-3`}
|
className={`flex items-center gap-3`}
|
||||||
onClick={handleAddToList}
|
onClick={handleAddToList}
|
||||||
>
|
>
|
||||||
<FaBookmark />
|
<FaBookmark />
|
||||||
{isInStore ? "Usuń z listy" : "Dodaj do listy"}
|
{isInStore ? 'Usuń z listy' : 'Dodaj do listy'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
theme={isFavorite ? "rosePink" : "glass"}
|
theme={isFavorite ? 'rosePink' : 'glass'}
|
||||||
className={`flex items-center gap-3 ${
|
className={`flex items-center gap-3 ${
|
||||||
isFavorite
|
isFavorite
|
||||||
? "bg-gradient-to-r border-rose-400/30"
|
? 'bg-gradient-to-r border-rose-400/30'
|
||||||
: ""
|
: ''
|
||||||
}`}
|
}`}
|
||||||
onClick={handleToggleFavorite}
|
onClick={handleToggleFavorite}
|
||||||
>
|
>
|
||||||
<FaHeart
|
<FaHeart
|
||||||
className={isFavorite ? "text-rose-200" : ""}
|
className={isFavorite ? 'text-rose-200' : ''}
|
||||||
/>
|
/>
|
||||||
{isFavorite
|
{isFavorite
|
||||||
? "Usuń z ulubionych"
|
? 'Usuń z ulubionych'
|
||||||
: "Dodaj do ulubionych"}
|
: 'Dodaj do ulubionych'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
theme={isSeen ? "emeraldTeal" : "glass"}
|
theme={isSeen ? 'emeraldTeal' : 'glass'}
|
||||||
className={`flex items-center gap-3 ${
|
className={`flex items-center gap-3 ${
|
||||||
isSeen ? "bg-gradient-to-r border-emerald-400/30" : ""
|
isSeen ? 'bg-gradient-to-r border-emerald-400/30' : ''
|
||||||
}`}
|
}`}
|
||||||
onClick={handleToggleSeen}
|
onClick={handleToggleSeen}
|
||||||
>
|
>
|
||||||
<FaEye className={isSeen ? "text-emerald-200" : ""} />
|
<FaEye className={isSeen ? 'text-emerald-200' : ''} />
|
||||||
{isSeen
|
{isSeen
|
||||||
? "Oznacz jako nieobejrzany"
|
? 'Oznacz jako nieobejrzany'
|
||||||
: "Oznacz jako obejrzany"}
|
: 'Oznacz jako obejrzany'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"use client";
|
'use client';
|
||||||
import Link from "next/link";
|
import Link from 'next/link';
|
||||||
import { Button } from "@/components/atoms/Button";
|
import { Button } from '@/components/atoms/Button';
|
||||||
import { MovieDetailsRich } from "@/lib/tmdb/types";
|
import { MovieDetailsRich } from '@/lib/tmdb/types';
|
||||||
import { FC, useState } from "react";
|
import { FC, useState } from 'react';
|
||||||
import { FaDollarSign } from "react-icons/fa";
|
import { FaDollarSign } from 'react-icons/fa';
|
||||||
import { formatter } from "@/helpers/formater";
|
import { formatter } from '@/helpers/formater';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
movieDetails: MovieDetailsRich;
|
movieDetails: MovieDetailsRich;
|
||||||
@@ -13,16 +13,10 @@ type Props = {
|
|||||||
export const MovieCast: FC<Props> = ({ movieDetails }) => {
|
export const MovieCast: FC<Props> = ({ movieDetails }) => {
|
||||||
const [limit, setLimit] = useState(8);
|
const [limit, setLimit] = useState(8);
|
||||||
const director = movieDetails?.credits.crew.find(
|
const director = movieDetails?.credits.crew.find(
|
||||||
(member) => member.job === "Director"
|
member => member.job === 'Director'
|
||||||
);
|
);
|
||||||
const mainCast = movieDetails?.credits.cast.slice(0, limit) || [];
|
const mainCast = movieDetails?.credits.cast.slice(0, limit) || [];
|
||||||
|
|
||||||
const formatCurrency = (amount: number) =>
|
|
||||||
new Intl.NumberFormat("pl-PL", {
|
|
||||||
style: "currency",
|
|
||||||
currency: "USD",
|
|
||||||
}).format(amount);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="blocks">
|
<section className="blocks">
|
||||||
<div className="container mx-auto">
|
<div className="container mx-auto">
|
||||||
@@ -32,7 +26,7 @@ export const MovieCast: FC<Props> = ({ movieDetails }) => {
|
|||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<h2 className="text-2xl font-bold text-white mb-6">Obsada</h2>
|
<h2 className="text-2xl font-bold text-white mb-6">Obsada</h2>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-6">
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-6">
|
||||||
{mainCast.map((actor) => (
|
{mainCast.map(actor => (
|
||||||
<Link
|
<Link
|
||||||
key={actor.id}
|
key={actor.id}
|
||||||
href={`/aktor/${actor.id}`}
|
href={`/aktor/${actor.id}`}
|
||||||
@@ -41,12 +35,12 @@ export const MovieCast: FC<Props> = ({ movieDetails }) => {
|
|||||||
<div className="relative overflow-hidden rounded-xl mb-3">
|
<div className="relative overflow-hidden rounded-xl mb-3">
|
||||||
<img
|
<img
|
||||||
style={{
|
style={{
|
||||||
aspectRatio: "185/278",
|
aspectRatio: '185/278',
|
||||||
}}
|
}}
|
||||||
src={
|
src={
|
||||||
actor.profile_path
|
actor.profile_path
|
||||||
? `https://image.tmdb.org/t/p/w185${actor.profile_path}`
|
? `https://image.tmdb.org/t/p/w185${actor.profile_path}`
|
||||||
: "/api/placeholder/185/278"
|
: '/api/placeholder/185/278'
|
||||||
}
|
}
|
||||||
alt={actor.name}
|
alt={actor.name}
|
||||||
className="w-full object-cover group-hover:scale-110 transition-transform duration-500 bg-gradient-to-br from-purple-500/20 to-cyan-500/20"
|
className="w-full object-cover group-hover:scale-110 transition-transform duration-500 bg-gradient-to-br from-purple-500/20 to-cyan-500/20"
|
||||||
@@ -147,7 +141,7 @@ export const MovieCast: FC<Props> = ({ movieDetails }) => {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{movieDetails.production_companies
|
{movieDetails.production_companies
|
||||||
.slice(0, 3)
|
.slice(0, 3)
|
||||||
.map((company) => (
|
.map(company => (
|
||||||
<p key={company.id} className="text-gray-300">
|
<p key={company.id} className="text-gray-300">
|
||||||
{company.name}
|
{company.name}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { FC, ReactNode, useState } from 'react';
|
import { FC, ReactNode, useState } from 'react';
|
||||||
import { MovieCard } from '@/components/atoms/MovieCard';
|
import { MovieCard } from '@/components/atoms/MovieCard';
|
||||||
import { useGlobalStore } from '@/app/store/globalStore';
|
|
||||||
import { Dropdown } from '@/components/atoms/Dropdown';
|
import { Dropdown } from '@/components/atoms/Dropdown';
|
||||||
import { useAutoAnimate } from '@formkit/auto-animate/react';
|
import { useAutoAnimate } from '@formkit/auto-animate/react';
|
||||||
import { Button } from '@/components/atoms/Button';
|
import { Button } from '@/components/atoms/Button';
|
||||||
import { Label } from '@/components/atoms/Label';
|
import { Label } from '@/components/atoms/Label';
|
||||||
import { FaList } from 'react-icons/fa';
|
import { FaList } from 'react-icons/fa';
|
||||||
import { MovieRow } from '@/components/atoms/MovieRow';
|
import { MovieRow } from '@/components/atoms/MovieRow';
|
||||||
|
import { useGlobalStore } from '@/app/store/global';
|
||||||
|
import { SearchInput } from '@/components/atoms/SearchInput';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
heading?: string;
|
heading?: string;
|
||||||
@@ -17,6 +18,7 @@ type Props = {
|
|||||||
overrideMovies?: Movie[];
|
overrideMovies?: Movie[];
|
||||||
overrideDisplayType?: 'grid' | 'list';
|
overrideDisplayType?: 'grid' | 'list';
|
||||||
|
|
||||||
|
showSearch?: boolean;
|
||||||
showFilters?: boolean;
|
showFilters?: boolean;
|
||||||
filterSeen?: 0 | 1;
|
filterSeen?: 0 | 1;
|
||||||
filterFavorites?: 0 | 1;
|
filterFavorites?: 0 | 1;
|
||||||
@@ -25,7 +27,7 @@ type Props = {
|
|||||||
|
|
||||||
fluid?: boolean;
|
fluid?: boolean;
|
||||||
showSorting?: boolean;
|
showSorting?: boolean;
|
||||||
sort?: 'title' | 'releaseDate' | 'popularity';
|
sort?: 'title' | 'releaseDate' | 'popularity' | 'voteAverage';
|
||||||
sortDirection?: 'asc' | 'desc';
|
sortDirection?: 'asc' | 'desc';
|
||||||
|
|
||||||
loadMore?: boolean;
|
loadMore?: boolean;
|
||||||
@@ -37,6 +39,7 @@ export const MovieList: FC<Props> = ({
|
|||||||
colors = 'white',
|
colors = 'white',
|
||||||
overrideMovies,
|
overrideMovies,
|
||||||
showFilters = true,
|
showFilters = true,
|
||||||
|
showSearch = false,
|
||||||
filterSeen: filterSeenInitial,
|
filterSeen: filterSeenInitial,
|
||||||
filterFavorites: filterFavoritesInitial,
|
filterFavorites: filterFavoritesInitial,
|
||||||
filterUpcoming: filterUpcomingInitial,
|
filterUpcoming: filterUpcomingInitial,
|
||||||
@@ -48,23 +51,23 @@ export const MovieList: FC<Props> = ({
|
|||||||
loadMore = false,
|
loadMore = false,
|
||||||
overrideDisplayType,
|
overrideDisplayType,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const storeMovies = useGlobalStore(state => state.movies);
|
||||||
movies: storeMovies,
|
const displayTypeInitial = useGlobalStore(state => state.displayType);
|
||||||
displayType: displayTypeInitial,
|
const setDisplayType = useGlobalStore(state => state.setDisplayType);
|
||||||
setDisplayType,
|
|
||||||
} = useGlobalStore();
|
|
||||||
const movies = overrideMovies || storeMovies;
|
const movies = overrideMovies || storeMovies;
|
||||||
const displayType = overrideDisplayType || displayTypeInitial;
|
const displayType = overrideDisplayType || displayTypeInitial;
|
||||||
|
|
||||||
const [filter, setFilter] = useState({
|
const [filter, setFilter] = useState({
|
||||||
|
search: '',
|
||||||
seen: filterSeenInitial,
|
seen: filterSeenInitial,
|
||||||
favorites: filterFavoritesInitial,
|
favorites: filterFavoritesInitial,
|
||||||
upcoming: filterUpcomingInitial,
|
upcoming: filterUpcomingInitial,
|
||||||
released: filterReleasedInitial,
|
released: filterReleasedInitial,
|
||||||
});
|
});
|
||||||
const [sort, setSort] = useState<'title' | 'releaseDate' | 'popularity'>(
|
const [sort, setSort] = useState<
|
||||||
sortType
|
'title' | 'releaseDate' | 'popularity' | 'voteAverage'
|
||||||
);
|
>(sortType);
|
||||||
|
|
||||||
const [loaded, setLoaded] = useState(8);
|
const [loaded, setLoaded] = useState(8);
|
||||||
const [parent] = useAutoAnimate();
|
const [parent] = useAutoAnimate();
|
||||||
@@ -88,6 +91,9 @@ export const MovieList: FC<Props> = ({
|
|||||||
result =
|
result =
|
||||||
result && filter.released ? releaseDate < today : releaseDate > today;
|
result && filter.released ? releaseDate < today : releaseDate > today;
|
||||||
}
|
}
|
||||||
|
if (filter.search) {
|
||||||
|
result = movie.title.toLowerCase().includes(filter.search.toLowerCase());
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -98,6 +104,7 @@ export const MovieList: FC<Props> = ({
|
|||||||
new Date(b.release_date).getTime() - new Date(a.release_date).getTime()
|
new Date(b.release_date).getTime() - new Date(a.release_date).getTime()
|
||||||
);
|
);
|
||||||
if (sort === 'popularity') return b.popularity - a.popularity;
|
if (sort === 'popularity') return b.popularity - a.popularity;
|
||||||
|
if (sort === 'voteAverage') return b.vote_average - a.vote_average;
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -108,6 +115,7 @@ export const MovieList: FC<Props> = ({
|
|||||||
|
|
||||||
const handleFilter = (key?: keyof typeof filter) => {
|
const handleFilter = (key?: keyof typeof filter) => {
|
||||||
setFilter({
|
setFilter({
|
||||||
|
search: '',
|
||||||
seen: filterSeenInitial,
|
seen: filterSeenInitial,
|
||||||
favorites: filterFavoritesInitial,
|
favorites: filterFavoritesInitial,
|
||||||
upcoming: filterUpcomingInitial,
|
upcoming: filterUpcomingInitial,
|
||||||
@@ -168,6 +176,7 @@ export const MovieList: FC<Props> = ({
|
|||||||
}
|
}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setFilter({
|
setFilter({
|
||||||
|
search: '',
|
||||||
seen: 0,
|
seen: 0,
|
||||||
released: 1,
|
released: 1,
|
||||||
favorites: filterFavoritesInitial,
|
favorites: filterFavoritesInitial,
|
||||||
@@ -215,6 +224,7 @@ export const MovieList: FC<Props> = ({
|
|||||||
{ label: 'Tytuł', value: 'title' },
|
{ label: 'Tytuł', value: 'title' },
|
||||||
{ label: 'Data premiery', value: 'releaseDate' },
|
{ label: 'Data premiery', value: 'releaseDate' },
|
||||||
{ label: 'Popularność', value: 'popularity' },
|
{ label: 'Popularność', value: 'popularity' },
|
||||||
|
{ label: 'Ocena', value: 'voteAverage' },
|
||||||
]}
|
]}
|
||||||
defaultValue={sort}
|
defaultValue={sort}
|
||||||
callback={value => setSort(value as 'title')}
|
callback={value => setSort(value as 'title')}
|
||||||
@@ -223,6 +233,15 @@ export const MovieList: FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{showSearch && (
|
||||||
|
<div className="flex justify-end mt-6">
|
||||||
|
<SearchInput
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Wyszukaj film..."
|
||||||
|
onChange={value => setFilter({ ...filter, search: value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{filteredMovies.length === 0 && (
|
{filteredMovies.length === 0 && (
|
||||||
<p className="text-text/60 text-sm">Brak filmów</p>
|
<p className="text-text/60 text-sm">Brak filmów</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { FC, useMemo, useState } from 'react';
|
import { FC, useMemo, useState } from 'react';
|
||||||
import { useGlobalStore } from '@/app/store/globalStore';
|
|
||||||
import { Button } from '@/components/atoms/Button';
|
import { Button } from '@/components/atoms/Button';
|
||||||
import { FaDice } from 'react-icons/fa';
|
import { FaDice } from 'react-icons/fa';
|
||||||
|
import { useGlobalStore } from '@/app/store/global';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
type StoreFilter = 'all' | 'not_seen' | 'released' | 'favorites' | 'to_watch';
|
type StoreFilter = 'all' | 'not_seen' | 'released' | 'favorites' | 'to_watch';
|
||||||
@@ -20,7 +20,7 @@ export const RandomMovie: FC<Props> = ({
|
|||||||
colors = 'purple',
|
colors = 'purple',
|
||||||
className = '',
|
className = '',
|
||||||
}) => {
|
}) => {
|
||||||
const { movies } = useGlobalStore();
|
const movies = useGlobalStore(state => state.movies);
|
||||||
const [selectedMovie, setSelectedMovie] = useState<Movie | null>(null);
|
const [selectedMovie, setSelectedMovie] = useState<Movie | null>(null);
|
||||||
|
|
||||||
// Filter movies based on the selected store filter.
|
// Filter movies based on the selected store filter.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { useGlobalStore } from '@/app/store/globalStore';
|
|
||||||
import { FaCalendar, FaClock } from 'react-icons/fa';
|
import { FaCalendar, FaClock } from 'react-icons/fa';
|
||||||
import { MovieRow } from '@/components/atoms/MovieRow';
|
import { MovieRow } from '@/components/atoms/MovieRow';
|
||||||
|
import { useGlobalStore } from '@/app/store/global';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
overrideMovies?: Movie[];
|
overrideMovies?: Movie[];
|
||||||
@@ -17,7 +17,7 @@ export const TrackedMovies: FC<Props> = ({
|
|||||||
labelCurrent = 'Aktualnie w kinach',
|
labelCurrent = 'Aktualnie w kinach',
|
||||||
labelUpcoming = 'Nadchodzące premiery',
|
labelUpcoming = 'Nadchodzące premiery',
|
||||||
}) => {
|
}) => {
|
||||||
const { movies: storeMovies } = useGlobalStore();
|
const storeMovies = useGlobalStore(state => state.movies);
|
||||||
|
|
||||||
const movies = overrideMovies || storeMovies;
|
const movies = overrideMovies || storeMovies;
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import {
|
|||||||
FaMinus,
|
FaMinus,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
import { RiCalendarCheckLine, RiCalendarScheduleLine } from 'react-icons/ri';
|
import { RiCalendarCheckLine, RiCalendarScheduleLine } from 'react-icons/ri';
|
||||||
import { useGlobalStore } from '@/app/store/globalStore';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Button } from '@/components/atoms/Button';
|
import { Button } from '@/components/atoms/Button';
|
||||||
|
import { useGlobalStore } from '@/app/store/global';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
movies: Movie[];
|
movies: Movie[];
|
||||||
@@ -28,11 +28,9 @@ export const Hero: FC<Props> = ({
|
|||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||||
|
|
||||||
const {
|
const storedMovies = useGlobalStore(state => state.movies);
|
||||||
movies: storedMovies,
|
const addMovie = useGlobalStore(state => state.addMovie);
|
||||||
addMovie: addMovieToStore,
|
const deleteMovie = useGlobalStore(state => state.deleteMovie);
|
||||||
deleteMovie: deleteMovieInStore,
|
|
||||||
} = useGlobalStore();
|
|
||||||
|
|
||||||
const currentMovie = movies[currentIndex];
|
const currentMovie = movies[currentIndex];
|
||||||
|
|
||||||
@@ -92,11 +90,11 @@ export const Hero: FC<Props> = ({
|
|||||||
}, [autoRotate, rotateInterval, nextSlide, movies.length]);
|
}, [autoRotate, rotateInterval, nextSlide, movies.length]);
|
||||||
|
|
||||||
const handleAdd = async () => {
|
const handleAdd = async () => {
|
||||||
addMovieToStore(currentMovie);
|
addMovie(currentMovie);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemove = async () => {
|
const handleRemove = async () => {
|
||||||
deleteMovieInStore(id);
|
deleteMovie(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user