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-dom": "19.1.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"swr": "^2.3.6"
|
||||
"swr": "^2.3.6",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
@@ -1593,7 +1594,7 @@
|
||||
"version": "19.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz",
|
||||
"integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
@@ -1703,7 +1704,7 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
@@ -2770,6 +2771,35 @@
|
||||
"engines": {
|
||||
"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-dom": "19.1.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"swr": "^2.3.6"
|
||||
"swr": "^2.3.6",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { MovieCard } from "@/components/atoms/MovieCard";
|
||||
import { ActorHero } from "@/components/molecules/ActorHero";
|
||||
import { Carousel } from "@/components/molecules/Carousel";
|
||||
import { Gallery } from "@/components/molecules/Gallery";
|
||||
import { convertToMovie } from "@/helpers/convertToMovie";
|
||||
import { TMDB } from "@/lib/tmdb";
|
||||
import { FaStar } from "react-icons/fa";
|
||||
import { MovieCard } from '@/components/atoms/MovieCard';
|
||||
import { ActorHero } from '@/components/molecules/ActorHero';
|
||||
import { Carousel } from '@/components/molecules/Carousel';
|
||||
import { Gallery } from '@/components/molecules/Gallery';
|
||||
import { convertToMovie } from '@/helpers/convertToMovie';
|
||||
import { TMDB } from '@/lib/tmdb';
|
||||
import { FaStar } from 'react-icons/fa';
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
@@ -32,7 +32,7 @@ export default async function Page({
|
||||
new Date(b.release_date).getTime() -
|
||||
new Date(a.release_date).getTime()
|
||||
)
|
||||
.map((movie) => {
|
||||
.map(movie => {
|
||||
const convertedMovie = convertToMovie(movie);
|
||||
if (!convertedMovie) return null;
|
||||
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 { AuroraBackground } from '@/components/effects';
|
||||
import { GlobalStoreProvider } from './store/globalStore';
|
||||
import { DB_getMovies } from '@/lib/db/pb';
|
||||
import { GlobalProvider } from './store/global';
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
@@ -23,11 +23,11 @@ export default async function RootLayout({
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</head>
|
||||
<body className={`antialiased`}>
|
||||
<GlobalStoreProvider initialMovies={movies}>
|
||||
<GlobalProvider initialMovies={movies}>
|
||||
<AuroraBackground />
|
||||
<Navbar />
|
||||
<main className="relative [&>*:last-child]:pb-16">{children}</main>
|
||||
</GlobalStoreProvider>
|
||||
</GlobalProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ export default async function Home() {
|
||||
return (
|
||||
<>
|
||||
<TrackedMovies />
|
||||
<MovieList heading="Moja lista" />
|
||||
<MovieList heading="Moja lista" showSearch />
|
||||
<RandomMovie heading="Ciężko wybrać?" />
|
||||
<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';
|
||||
import { FC } from 'react';
|
||||
import { useGlobalStore } from '@/app/store/globalStore';
|
||||
import { FaFire, FaPlusCircle, FaTrash } from 'react-icons/fa';
|
||||
import Link from 'next/link';
|
||||
import { RxEyeOpen } from 'react-icons/rx';
|
||||
import { MdFavorite } from 'react-icons/md';
|
||||
import { RiCalendarCheckLine, RiCalendarScheduleLine } from 'react-icons/ri';
|
||||
import { useGlobalStore } from '@/app/store/global';
|
||||
|
||||
type Props = Movie & {
|
||||
showDayCounter?: boolean;
|
||||
@@ -17,16 +17,14 @@ export const MovieCard: FC<Props> = ({
|
||||
simpleToggle = false,
|
||||
...movie
|
||||
}) => {
|
||||
const {
|
||||
movies,
|
||||
addMovie: addMovieToStore,
|
||||
deleteMovie: deleteMovieFromStore,
|
||||
updateMovie: updateMovieInStore,
|
||||
} = 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);
|
||||
const { vote_average, popularity, poster_path, title, overview } = 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 favorite = alreadyInStore?.favorite || movie.favorite;
|
||||
@@ -40,22 +38,22 @@ export const MovieCard: FC<Props> = ({
|
||||
: 'from-red-400 to-pink-400';
|
||||
|
||||
const handleAdd = () => {
|
||||
addMovieToStore(movie);
|
||||
addMovie(movie);
|
||||
};
|
||||
|
||||
const handleRemove = () => {
|
||||
deleteMovieFromStore(id);
|
||||
deleteMovie(id);
|
||||
};
|
||||
|
||||
const handleSeen = () => {
|
||||
updateMovieInStore(id, {
|
||||
updateMovie(id, {
|
||||
seen: !movie.seen,
|
||||
favorite: false,
|
||||
});
|
||||
};
|
||||
|
||||
const handleFavorite = () => {
|
||||
updateMovieInStore(id, {
|
||||
updateMovie(id, {
|
||||
favorite: !movie.favorite,
|
||||
seen: movie.seen || !movie.favorite,
|
||||
});
|
||||
|
||||
@@ -3,8 +3,7 @@ import { formatter } from '@/helpers/formater';
|
||||
import Link from 'next/link';
|
||||
import { FC } from 'react';
|
||||
import { FaCalendar, FaClock, FaStar, FaEye, FaHeart } from 'react-icons/fa';
|
||||
import { motion, useAnimationControls, useMotionValue } from 'framer-motion';
|
||||
import { useGlobalStore } from '@/app/store/globalStore';
|
||||
import { useGlobalStore } from '@/app/store/global';
|
||||
|
||||
type Props = {
|
||||
movie: Movie;
|
||||
@@ -17,10 +16,7 @@ export const MovieRow: FC<Props> = ({
|
||||
isUpcoming = false,
|
||||
compact = false,
|
||||
}) => {
|
||||
const { movies, addMovie, updateMovie } = useGlobalStore();
|
||||
|
||||
const dragControls = useAnimationControls();
|
||||
const x = useMotionValue(0);
|
||||
const movies = useGlobalStore(state => state.movies);
|
||||
|
||||
const daysSinceRelease = Math.abs(
|
||||
Math.floor(
|
||||
@@ -30,62 +26,12 @@ export const MovieRow: FC<Props> = ({
|
||||
);
|
||||
|
||||
// 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 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 (
|
||||
<div className="relative overflow-hidden rounded-xl">
|
||||
{/* Background actions */}
|
||||
<div className="absolute inset-0 flex">
|
||||
<div className="absolute right-0 h-full w-24 bg-green-500/20 flex items-center justify-center cursor-pointer">
|
||||
<FaEye className="w-5 h-5 transition-colors text-green-500" />
|
||||
</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
|
||||
href={`/film/${movie.id}`}
|
||||
draggable={false}
|
||||
@@ -151,7 +97,6 @@ export const MovieRow: FC<Props> = ({
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { BackButton } from "@/components/atoms/BackButton";
|
||||
import { formatter } from "@/helpers/formater";
|
||||
import { PersonDetailsRich } from "@/lib/tmdb/types";
|
||||
import { FC } from "react";
|
||||
import { BackButton } from '@/components/atoms/BackButton';
|
||||
import { formatter } from '@/helpers/formater';
|
||||
import { PersonDetailsRich } from '@/lib/tmdb/types';
|
||||
import { FC } from 'react';
|
||||
import {
|
||||
FaCalendarAlt,
|
||||
FaMapMarkerAlt,
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
FaTwitter,
|
||||
FaYoutube,
|
||||
FaTiktok,
|
||||
} from "react-icons/fa";
|
||||
} from 'react-icons/fa';
|
||||
|
||||
type Props = {
|
||||
personDetails: PersonDetailsRich;
|
||||
@@ -37,13 +37,13 @@ export const ActorHero: FC<Props> = ({ personDetails }) => {
|
||||
const getGenderText = (gender: number) => {
|
||||
switch (gender) {
|
||||
case 1:
|
||||
return "Kobieta";
|
||||
return 'Kobieta';
|
||||
case 2:
|
||||
return "Mężczyzna";
|
||||
return 'Mężczyzna';
|
||||
case 3:
|
||||
return "Niebinarne";
|
||||
return 'Niebinarne';
|
||||
default:
|
||||
return "Nie określono";
|
||||
return 'Nie określono';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -65,7 +65,7 @@ export const ActorHero: FC<Props> = ({ personDetails }) => {
|
||||
src={
|
||||
personDetails.profile_path
|
||||
? `https://image.tmdb.org/t/p/w500${personDetails.profile_path}`
|
||||
: "/api/placeholder/400/600"
|
||||
: '/api/placeholder/400/600'
|
||||
}
|
||||
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"
|
||||
@@ -88,9 +88,9 @@ export const ActorHero: FC<Props> = ({ personDetails }) => {
|
||||
{calculateAge(
|
||||
personDetails.birthday,
|
||||
personDetails.deathday
|
||||
)}{" "}
|
||||
)}{' '}
|
||||
lat
|
||||
{personDetails.deathday && " w chwili śmierci"})
|
||||
{personDetails.deathday && ' w chwili śmierci'})
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -114,8 +114,8 @@ export const ActorHero: FC<Props> = ({ personDetails }) => {
|
||||
{personDetails.also_known_as.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<p className="text-gray-400 text-sm">
|
||||
Znany również jako:{" "}
|
||||
{personDetails.also_known_as.slice(0, 3).join(", ")}
|
||||
Znany również jako:{' '}
|
||||
{personDetails.also_known_as.slice(0, 3).join(', ')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -171,7 +171,7 @@ export const ActorHero: FC<Props> = ({ personDetails }) => {
|
||||
</h3>
|
||||
<div className="text-gray-300 leading-relaxed text-lg space-y-4">
|
||||
{personDetails.biography
|
||||
.split("\n\n")
|
||||
.split('\n\n')
|
||||
.map((paragraph, index) => (
|
||||
<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">
|
||||
Linki
|
||||
</h3>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{Object.entries(personDetails.external_ids).map(
|
||||
([key, value]) => {
|
||||
if (!(key in externalIdsMap) || !value) {
|
||||
@@ -222,32 +222,32 @@ export const ActorHero: FC<Props> = ({ personDetails }) => {
|
||||
|
||||
const externalIdsMap = {
|
||||
facebook_id: {
|
||||
label: "Facebook",
|
||||
label: 'Facebook',
|
||||
icon: <FaFacebook />,
|
||||
url: (id: string) => `https://www.facebook.com/${id}`,
|
||||
},
|
||||
instagram_id: {
|
||||
label: "Instagram",
|
||||
label: 'Instagram',
|
||||
icon: <FaInstagram />,
|
||||
url: (id: string) => `https://www.instagram.com/${id}`,
|
||||
},
|
||||
twitter_id: {
|
||||
label: "Twitter",
|
||||
label: 'Twitter',
|
||||
icon: <FaTwitter />,
|
||||
url: (id: string) => `https://www.twitter.com/${id}`,
|
||||
},
|
||||
tiktok_id: {
|
||||
label: "TikTok",
|
||||
label: 'TikTok',
|
||||
icon: <FaTiktok />,
|
||||
url: (id: string) => `https://www.tiktok.com/${id}`,
|
||||
},
|
||||
youtube_id: {
|
||||
label: "YouTube",
|
||||
label: 'YouTube',
|
||||
icon: <FaYoutube />,
|
||||
url: (id: string) => `https://www.youtube.com/${id}`,
|
||||
},
|
||||
imdb_id: {
|
||||
label: "IMDb",
|
||||
label: 'IMDb',
|
||||
icon: <FaImdb />,
|
||||
url: (id: string) => `https://www.imdb.com/name/${id}`,
|
||||
},
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"use client";
|
||||
import { BackButton } from "@/components/atoms/BackButton";
|
||||
import { Button } from "@/components/atoms/Button";
|
||||
import { GenreLabel } from "@/components/atoms/GenreLabel";
|
||||
import { MovieDetailsRich } from "@/lib/tmdb/types";
|
||||
import { useGlobalStore } from "@/app/store/globalStore";
|
||||
import { FC } from "react";
|
||||
'use client';
|
||||
import { BackButton } from '@/components/atoms/BackButton';
|
||||
import { Button } from '@/components/atoms/Button';
|
||||
import { GenreLabel } from '@/components/atoms/GenreLabel';
|
||||
import { MovieDetailsRich } from '@/lib/tmdb/types';
|
||||
import { FC } from 'react';
|
||||
import {
|
||||
FaHeart,
|
||||
FaBookmark,
|
||||
@@ -12,19 +11,23 @@ import {
|
||||
FaCalendar,
|
||||
FaGlobe,
|
||||
FaEye,
|
||||
} from "react-icons/fa";
|
||||
import { convertToMovie } from "@/helpers/convertToMovie";
|
||||
import { formatter } from "@/helpers/formater";
|
||||
} from 'react-icons/fa';
|
||||
import { convertToMovie } from '@/helpers/convertToMovie';
|
||||
import { formatter } from '@/helpers/formater';
|
||||
import { useGlobalStore } from '@/app/store/global';
|
||||
|
||||
type Props = {
|
||||
movieDetails: MovieDetailsRich;
|
||||
};
|
||||
|
||||
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.
|
||||
const movieInStore = movies.find((m) => m.id === movieDetails.id);
|
||||
const movieInStore = movies.find(m => m.id == movieDetails.id);
|
||||
const isInStore = !!movieInStore;
|
||||
const isFavorite = movieInStore?.favorite || false;
|
||||
const isSeen = movieInStore?.seen || false;
|
||||
@@ -115,8 +118,8 @@ export const HeroMovie: FC<Props> = ({ movieDetails }) => {
|
||||
key={i}
|
||||
className={`text-2xl ${
|
||||
i < Math.round(movieDetails.vote_average / 2)
|
||||
? "text-yellow-400"
|
||||
: "text-gray-600"
|
||||
? 'text-yellow-400'
|
||||
: 'text-gray-600'
|
||||
}`}
|
||||
>
|
||||
★
|
||||
@@ -173,7 +176,7 @@ export const HeroMovie: FC<Props> = ({ movieDetails }) => {
|
||||
Gatunki
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{movieDetails.genres.map((genre) => (
|
||||
{movieDetails.genres.map(genre => (
|
||||
<GenreLabel
|
||||
key={genre.id}
|
||||
genre={genre.name}
|
||||
@@ -203,47 +206,47 @@ export const HeroMovie: FC<Props> = ({ movieDetails }) => {
|
||||
gradient={
|
||||
isInStore
|
||||
? {
|
||||
from: "from-purple-600 hover:from-purple-500",
|
||||
to: "to-emerald-600 hover:to-emerald-500",
|
||||
from: 'from-purple-600 hover:from-purple-500',
|
||||
to: 'to-emerald-600 hover:to-emerald-500',
|
||||
}
|
||||
: {
|
||||
from: "from-purple-600 hover:from-purple-500",
|
||||
to: "to-pink-600 hover:to-pink-500",
|
||||
from: 'from-purple-600 hover:from-purple-500',
|
||||
to: 'to-pink-600 hover:to-pink-500',
|
||||
}
|
||||
}
|
||||
className={`flex items-center gap-3`}
|
||||
onClick={handleAddToList}
|
||||
>
|
||||
<FaBookmark />
|
||||
{isInStore ? "Usuń z listy" : "Dodaj do listy"}
|
||||
{isInStore ? 'Usuń z listy' : 'Dodaj do listy'}
|
||||
</Button>
|
||||
<Button
|
||||
theme={isFavorite ? "rosePink" : "glass"}
|
||||
theme={isFavorite ? 'rosePink' : 'glass'}
|
||||
className={`flex items-center gap-3 ${
|
||||
isFavorite
|
||||
? "bg-gradient-to-r border-rose-400/30"
|
||||
: ""
|
||||
? 'bg-gradient-to-r border-rose-400/30'
|
||||
: ''
|
||||
}`}
|
||||
onClick={handleToggleFavorite}
|
||||
>
|
||||
<FaHeart
|
||||
className={isFavorite ? "text-rose-200" : ""}
|
||||
className={isFavorite ? 'text-rose-200' : ''}
|
||||
/>
|
||||
{isFavorite
|
||||
? "Usuń z ulubionych"
|
||||
: "Dodaj do ulubionych"}
|
||||
? 'Usuń z ulubionych'
|
||||
: 'Dodaj do ulubionych'}
|
||||
</Button>
|
||||
<Button
|
||||
theme={isSeen ? "emeraldTeal" : "glass"}
|
||||
theme={isSeen ? 'emeraldTeal' : 'glass'}
|
||||
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}
|
||||
>
|
||||
<FaEye className={isSeen ? "text-emerald-200" : ""} />
|
||||
<FaEye className={isSeen ? 'text-emerald-200' : ''} />
|
||||
{isSeen
|
||||
? "Oznacz jako nieobejrzany"
|
||||
: "Oznacz jako obejrzany"}
|
||||
? 'Oznacz jako nieobejrzany'
|
||||
: 'Oznacz jako obejrzany'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/atoms/Button";
|
||||
import { MovieDetailsRich } from "@/lib/tmdb/types";
|
||||
import { FC, useState } from "react";
|
||||
import { FaDollarSign } from "react-icons/fa";
|
||||
import { formatter } from "@/helpers/formater";
|
||||
'use client';
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/components/atoms/Button';
|
||||
import { MovieDetailsRich } from '@/lib/tmdb/types';
|
||||
import { FC, useState } from 'react';
|
||||
import { FaDollarSign } from 'react-icons/fa';
|
||||
import { formatter } from '@/helpers/formater';
|
||||
|
||||
type Props = {
|
||||
movieDetails: MovieDetailsRich;
|
||||
@@ -13,16 +13,10 @@ type Props = {
|
||||
export const MovieCast: FC<Props> = ({ movieDetails }) => {
|
||||
const [limit, setLimit] = useState(8);
|
||||
const director = movieDetails?.credits.crew.find(
|
||||
(member) => member.job === "Director"
|
||||
member => member.job === 'Director'
|
||||
);
|
||||
const mainCast = movieDetails?.credits.cast.slice(0, limit) || [];
|
||||
|
||||
const formatCurrency = (amount: number) =>
|
||||
new Intl.NumberFormat("pl-PL", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}).format(amount);
|
||||
|
||||
return (
|
||||
<section className="blocks">
|
||||
<div className="container mx-auto">
|
||||
@@ -32,7 +26,7 @@ export const MovieCast: FC<Props> = ({ movieDetails }) => {
|
||||
<div className="lg:col-span-2">
|
||||
<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">
|
||||
{mainCast.map((actor) => (
|
||||
{mainCast.map(actor => (
|
||||
<Link
|
||||
key={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">
|
||||
<img
|
||||
style={{
|
||||
aspectRatio: "185/278",
|
||||
aspectRatio: '185/278',
|
||||
}}
|
||||
src={
|
||||
actor.profile_path
|
||||
? `https://image.tmdb.org/t/p/w185${actor.profile_path}`
|
||||
: "/api/placeholder/185/278"
|
||||
: '/api/placeholder/185/278'
|
||||
}
|
||||
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"
|
||||
@@ -147,7 +141,7 @@ export const MovieCast: FC<Props> = ({ movieDetails }) => {
|
||||
<div className="space-y-2">
|
||||
{movieDetails.production_companies
|
||||
.slice(0, 3)
|
||||
.map((company) => (
|
||||
.map(company => (
|
||||
<p key={company.id} className="text-gray-300">
|
||||
{company.name}
|
||||
</p>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
'use client';
|
||||
import { FC, ReactNode, useState } from 'react';
|
||||
import { MovieCard } from '@/components/atoms/MovieCard';
|
||||
import { useGlobalStore } from '@/app/store/globalStore';
|
||||
import { Dropdown } from '@/components/atoms/Dropdown';
|
||||
import { useAutoAnimate } from '@formkit/auto-animate/react';
|
||||
import { Button } from '@/components/atoms/Button';
|
||||
import { Label } from '@/components/atoms/Label';
|
||||
import { FaList } from 'react-icons/fa';
|
||||
import { MovieRow } from '@/components/atoms/MovieRow';
|
||||
import { useGlobalStore } from '@/app/store/global';
|
||||
import { SearchInput } from '@/components/atoms/SearchInput';
|
||||
|
||||
type Props = {
|
||||
heading?: string;
|
||||
@@ -17,6 +18,7 @@ type Props = {
|
||||
overrideMovies?: Movie[];
|
||||
overrideDisplayType?: 'grid' | 'list';
|
||||
|
||||
showSearch?: boolean;
|
||||
showFilters?: boolean;
|
||||
filterSeen?: 0 | 1;
|
||||
filterFavorites?: 0 | 1;
|
||||
@@ -25,7 +27,7 @@ type Props = {
|
||||
|
||||
fluid?: boolean;
|
||||
showSorting?: boolean;
|
||||
sort?: 'title' | 'releaseDate' | 'popularity';
|
||||
sort?: 'title' | 'releaseDate' | 'popularity' | 'voteAverage';
|
||||
sortDirection?: 'asc' | 'desc';
|
||||
|
||||
loadMore?: boolean;
|
||||
@@ -37,6 +39,7 @@ export const MovieList: FC<Props> = ({
|
||||
colors = 'white',
|
||||
overrideMovies,
|
||||
showFilters = true,
|
||||
showSearch = false,
|
||||
filterSeen: filterSeenInitial,
|
||||
filterFavorites: filterFavoritesInitial,
|
||||
filterUpcoming: filterUpcomingInitial,
|
||||
@@ -48,23 +51,23 @@ export const MovieList: FC<Props> = ({
|
||||
loadMore = false,
|
||||
overrideDisplayType,
|
||||
}) => {
|
||||
const {
|
||||
movies: storeMovies,
|
||||
displayType: displayTypeInitial,
|
||||
setDisplayType,
|
||||
} = useGlobalStore();
|
||||
const storeMovies = useGlobalStore(state => state.movies);
|
||||
const displayTypeInitial = useGlobalStore(state => state.displayType);
|
||||
const setDisplayType = useGlobalStore(state => state.setDisplayType);
|
||||
|
||||
const movies = overrideMovies || storeMovies;
|
||||
const displayType = overrideDisplayType || displayTypeInitial;
|
||||
|
||||
const [filter, setFilter] = useState({
|
||||
search: '',
|
||||
seen: filterSeenInitial,
|
||||
favorites: filterFavoritesInitial,
|
||||
upcoming: filterUpcomingInitial,
|
||||
released: filterReleasedInitial,
|
||||
});
|
||||
const [sort, setSort] = useState<'title' | 'releaseDate' | 'popularity'>(
|
||||
sortType
|
||||
);
|
||||
const [sort, setSort] = useState<
|
||||
'title' | 'releaseDate' | 'popularity' | 'voteAverage'
|
||||
>(sortType);
|
||||
|
||||
const [loaded, setLoaded] = useState(8);
|
||||
const [parent] = useAutoAnimate();
|
||||
@@ -88,6 +91,9 @@ export const MovieList: FC<Props> = ({
|
||||
result =
|
||||
result && filter.released ? releaseDate < today : releaseDate > today;
|
||||
}
|
||||
if (filter.search) {
|
||||
result = movie.title.toLowerCase().includes(filter.search.toLowerCase());
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
@@ -98,6 +104,7 @@ export const MovieList: FC<Props> = ({
|
||||
new Date(b.release_date).getTime() - new Date(a.release_date).getTime()
|
||||
);
|
||||
if (sort === 'popularity') return b.popularity - a.popularity;
|
||||
if (sort === 'voteAverage') return b.vote_average - a.vote_average;
|
||||
return 0;
|
||||
});
|
||||
|
||||
@@ -108,6 +115,7 @@ export const MovieList: FC<Props> = ({
|
||||
|
||||
const handleFilter = (key?: keyof typeof filter) => {
|
||||
setFilter({
|
||||
search: '',
|
||||
seen: filterSeenInitial,
|
||||
favorites: filterFavoritesInitial,
|
||||
upcoming: filterUpcomingInitial,
|
||||
@@ -168,6 +176,7 @@ export const MovieList: FC<Props> = ({
|
||||
}
|
||||
onClick={() =>
|
||||
setFilter({
|
||||
search: '',
|
||||
seen: 0,
|
||||
released: 1,
|
||||
favorites: filterFavoritesInitial,
|
||||
@@ -215,6 +224,7 @@ export const MovieList: FC<Props> = ({
|
||||
{ label: 'Tytuł', value: 'title' },
|
||||
{ label: 'Data premiery', value: 'releaseDate' },
|
||||
{ label: 'Popularność', value: 'popularity' },
|
||||
{ label: 'Ocena', value: 'voteAverage' },
|
||||
]}
|
||||
defaultValue={sort}
|
||||
callback={value => setSort(value as 'title')}
|
||||
@@ -223,6 +233,15 @@ export const MovieList: FC<Props> = ({
|
||||
)}
|
||||
</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 && (
|
||||
<p className="text-text/60 text-sm">Brak filmów</p>
|
||||
)}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
import { FC, useMemo, useState } from 'react';
|
||||
import { useGlobalStore } from '@/app/store/globalStore';
|
||||
import { Button } from '@/components/atoms/Button';
|
||||
import { FaDice } from 'react-icons/fa';
|
||||
import { useGlobalStore } from '@/app/store/global';
|
||||
import Link from 'next/link';
|
||||
|
||||
type StoreFilter = 'all' | 'not_seen' | 'released' | 'favorites' | 'to_watch';
|
||||
@@ -20,7 +20,7 @@ export const RandomMovie: FC<Props> = ({
|
||||
colors = 'purple',
|
||||
className = '',
|
||||
}) => {
|
||||
const { movies } = useGlobalStore();
|
||||
const movies = useGlobalStore(state => state.movies);
|
||||
const [selectedMovie, setSelectedMovie] = useState<Movie | null>(null);
|
||||
|
||||
// Filter movies based on the selected store filter.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
import { FC } from 'react';
|
||||
import { useGlobalStore } from '@/app/store/globalStore';
|
||||
import { FaCalendar, FaClock } from 'react-icons/fa';
|
||||
import { MovieRow } from '@/components/atoms/MovieRow';
|
||||
import { useGlobalStore } from '@/app/store/global';
|
||||
|
||||
type Props = {
|
||||
overrideMovies?: Movie[];
|
||||
@@ -17,7 +17,7 @@ export const TrackedMovies: FC<Props> = ({
|
||||
labelCurrent = 'Aktualnie w kinach',
|
||||
labelUpcoming = 'Nadchodzące premiery',
|
||||
}) => {
|
||||
const { movies: storeMovies } = useGlobalStore();
|
||||
const storeMovies = useGlobalStore(state => state.movies);
|
||||
|
||||
const movies = overrideMovies || storeMovies;
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
FaMinus,
|
||||
} from 'react-icons/fa';
|
||||
import { RiCalendarCheckLine, RiCalendarScheduleLine } from 'react-icons/ri';
|
||||
import { useGlobalStore } from '@/app/store/globalStore';
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/components/atoms/Button';
|
||||
import { useGlobalStore } from '@/app/store/global';
|
||||
|
||||
type Props = {
|
||||
movies: Movie[];
|
||||
@@ -28,11 +28,9 @@ export const Hero: FC<Props> = ({
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||
|
||||
const {
|
||||
movies: storedMovies,
|
||||
addMovie: addMovieToStore,
|
||||
deleteMovie: deleteMovieInStore,
|
||||
} = useGlobalStore();
|
||||
const storedMovies = useGlobalStore(state => state.movies);
|
||||
const addMovie = useGlobalStore(state => state.addMovie);
|
||||
const deleteMovie = useGlobalStore(state => state.deleteMovie);
|
||||
|
||||
const currentMovie = movies[currentIndex];
|
||||
|
||||
@@ -92,11 +90,11 @@ export const Hero: FC<Props> = ({
|
||||
}, [autoRotate, rotateInterval, nextSlide, movies.length]);
|
||||
|
||||
const handleAdd = async () => {
|
||||
addMovieToStore(currentMovie);
|
||||
addMovie(currentMovie);
|
||||
};
|
||||
|
||||
const handleRemove = async () => {
|
||||
deleteMovieInStore(id);
|
||||
deleteMovie(id);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user