feat: enhance GlobalStoreProvider and MovieList components with display type management and loading spinner; integrate local storage for display preferences and improve user experience during initial render

This commit is contained in:
Norbert Maciaszek 2025-08-22 19:17:48 +02:00
parent 50aa22ee6c
commit d67e34c75c
3 changed files with 54 additions and 19 deletions

View File

@ -1,7 +1,9 @@
"use client"; "use client";
import { Spinner } from "@/components/atoms/Spinner";
import { useLocalStorage } from "@/hooks/useLocalStorage";
import { addMovieToDB, deleteMovieFromDB, updateMovieInDB } from "@/lib/db"; import { addMovieToDB, deleteMovieFromDB, updateMovieInDB } from "@/lib/db";
import { movies } from "@/lib/db/schema"; import { movies } from "@/lib/db/schema";
import { createContext, FC, use, useState } from "react"; import { createContext, FC, use, useEffect, useState } from "react";
type Movie = typeof movies.$inferSelect; type Movie = typeof movies.$inferSelect;
@ -10,6 +12,8 @@ type GlobalStore = {
addMovie: (movie: Movie) => void; addMovie: (movie: Movie) => void;
deleteMovie: (id: number) => void; deleteMovie: (id: number) => void;
updateMovie: (id: number, movie: Partial<Movie>) => void; updateMovie: (id: number, movie: Partial<Movie>) => void;
displayType: "grid" | "list";
setDisplayType: (type: "grid" | "list") => void;
}; };
const globalStore = createContext<GlobalStore>({ const globalStore = createContext<GlobalStore>({
@ -17,6 +21,8 @@ const globalStore = createContext<GlobalStore>({
addMovie: () => {}, addMovie: () => {},
deleteMovie: () => {}, deleteMovie: () => {},
updateMovie: () => {}, updateMovie: () => {},
displayType: "grid",
setDisplayType: () => {},
}); });
type Props = { type Props = {
@ -29,7 +35,17 @@ export const GlobalStoreProvider: FC<Props> = ({
initialMovies = [], initialMovies = [],
}) => { }) => {
// Optimistic update // Optimistic update
const [firstRender, setFirstRender] = useState(true);
const [movies, setMovies] = useState<GlobalStore["movies"]>(initialMovies); 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) => { const addMovie = async (movie: Movie) => {
if (movies.find((m) => m.id === movie.id)) return; if (movies.find((m) => m.id === movie.id)) return;
@ -52,9 +68,22 @@ export const GlobalStoreProvider: FC<Props> = ({
return ( return (
<globalStore.Provider <globalStore.Provider
value={{ movies, addMovie, deleteMovie, updateMovie }} value={{
movies,
addMovie,
deleteMovie,
updateMovie,
displayType,
setDisplayType,
}}
> >
{children} {firstRender ? (
<div className="flex justify-center items-center h-screen bg-black/80">
<Spinner />
</div>
) : (
children
)}
</globalStore.Provider> </globalStore.Provider>
); );
}; };

View File

@ -14,9 +14,9 @@ type Props = {
heading?: string; heading?: string;
icon?: ReactNode; icon?: ReactNode;
colors?: keyof typeof colorsMap; colors?: keyof typeof colorsMap;
displayType?: "grid" | "list";
overrideMovies?: Movie[]; overrideMovies?: Movie[];
overrideDisplayType?: "grid" | "list";
showFilters?: boolean; showFilters?: boolean;
filterSeen?: 0 | 1; filterSeen?: 0 | 1;
@ -47,12 +47,16 @@ export const MovieList: FC<Props> = ({
sort: sortType = "releaseDate", sort: sortType = "releaseDate",
sortDirection = "asc", sortDirection = "asc",
loadMore = false, loadMore = false,
displayType = "grid", overrideDisplayType,
}) => { }) => {
const { movies: storeMovies } = useGlobalStore(); const {
movies: storeMovies,
displayType: displayTypeInitial,
setDisplayType,
} = useGlobalStore();
const movies = overrideMovies || storeMovies; const movies = overrideMovies || storeMovies;
const displayType = overrideDisplayType || displayTypeInitial;
const [display, setDisplay] = useState<"grid" | "list">(displayType);
const [filter, setFilter] = useState({ const [filter, setFilter] = useState({
seen: filterSeenInitial, seen: filterSeenInitial,
favorites: filterFavoritesInitial, favorites: filterFavoritesInitial,
@ -205,15 +209,17 @@ export const MovieList: FC<Props> = ({
defaultValue={sort} defaultValue={sort}
callback={(value) => setSort(value as "title")} callback={(value) => setSort(value as "title")}
/> />
<Button {!overrideDisplayType && (
theme="glass" <Button
size="icon" theme="glass"
onClick={() => size="icon"
setDisplay(display === "grid" ? "list" : "grid") onClick={() =>
} setDisplayType(displayType === "grid" ? "list" : "grid")
> }
<FaList /> >
</Button> <FaList />
</Button>
)}
</div> </div>
)} )}
</div> </div>
@ -227,7 +233,7 @@ export const MovieList: FC<Props> = ({
ref={parent} ref={parent}
> >
{sortedMovies.map((movie) => {sortedMovies.map((movie) =>
display === "grid" ? ( displayType === "grid" ? (
<MovieCard key={movie.id} layout="aurora" {...movie} /> <MovieCard key={movie.id} layout="aurora" {...movie} />
) : ( ) : (
<MovieRow key={movie.id} movie={movie} compact /> <MovieRow key={movie.id} movie={movie} compact />

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react"; import { useState } from "react";
import { HiSearch, HiHome, HiViewGrid } from "react-icons/hi"; import { HiSearch, HiHome, HiSparkles } from "react-icons/hi";
import { Search } from "./components/Search"; import { Search } from "./components/Search";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
@ -17,7 +17,7 @@ const navigationItems = [
{ {
label: "Odkrywaj", label: "Odkrywaj",
href: "/odkrywaj", href: "/odkrywaj",
icon: HiViewGrid, icon: HiSparkles,
emoji: "🎬", emoji: "🎬",
color: "from-purple-500 to-pink-600", color: "from-purple-500 to-pink-600",
description: "Znajdź nowe filmy", description: "Znajdź nowe filmy",