Implement loading spinner and Polish translations: add Spinner component for loading states in Search and SearchList, update UI text to Polish for improved localization, and enhance Pagination styles for better visibility.

This commit is contained in:
Norbert Maciaszek 2025-08-15 15:20:04 +02:00
parent 3a7669e26d
commit 7373d64123
6 changed files with 73 additions and 17 deletions

View File

@ -11,7 +11,7 @@ export default async function SearchPage({
<> <>
<section className="container"> <section className="container">
<h1 className="text-2xl"> <h1 className="text-2xl">
Search for: <strong>{s}</strong> Wyszukiwanie: <strong>{s}</strong>
</h1> </h1>
</section> </section>
<SearchList query={s} /> <SearchList query={s} />

View File

@ -1,4 +1,3 @@
import Link from "next/link";
import { FC } from "react"; import { FC } from "react";
type Props = { type Props = {
@ -13,11 +12,11 @@ export const Pagination: FC<Props> = ({
onPageChange, onPageChange,
}) => { }) => {
return ( return (
<ul className="flex justify-center gap-3 text-gray-900 my-10"> <ul className="flex justify-center gap-3 my-10">
{currentPage > 1 && ( {currentPage > 1 && (
<li> <li>
<button <button
className="grid size-8 place-content-center rounded border border-primary transition-colors hover:bg-primary hover:text-white cursor-pointer" className="grid size-8 place-content-center rounded border bg-white text-black border-primary transition-colors hover:bg-primary hover:text-white cursor-pointer"
aria-label="Previous page" aria-label="Previous page"
onClick={() => onPageChange(currentPage - 1)} onClick={() => onPageChange(currentPage - 1)}
> >
@ -44,7 +43,7 @@ export const Pagination: FC<Props> = ({
{currentPage < totalPages && ( {currentPage < totalPages && (
<li> <li>
<button <button
className="grid size-8 place-content-center rounded border border-primary transition-colors hover:bg-primary hover:text-white cursor-pointer" className="grid size-8 place-content-center rounded border bg-white text-black border-primary transition-colors hover:bg-primary hover:text-white cursor-pointer"
aria-label="Next page" aria-label="Next page"
onClick={() => onPageChange(currentPage + 1)} onClick={() => onPageChange(currentPage + 1)}
> >

View File

@ -0,0 +1,5 @@
import styles from "./styles.module.css";
export const Spinner = () => {
return <span className={styles.loader} />;
};

View File

@ -0,0 +1,32 @@
.loader {
width: 48px;
height: 48px;
background: #fff;
border-radius: 50%;
display: inline-block;
position: relative;
box-sizing: border-box;
animation: rotation 1s linear infinite;
}
.loader::after {
content: "";
box-sizing: border-box;
position: absolute;
left: 6px;
top: 10px;
width: 12px;
height: 12px;
color: #ff3d00;
background: currentColor;
border-radius: 50%;
box-shadow: 25px 2px, 10px 22px;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@ -1,12 +1,12 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { TMDB } from "@/lib/tmdb"; import { TMDB } from "@/lib/tmdb";
import { MovieCard } from "@/components/atoms/MovieCard";
import { SearchResult } from "@/lib/tmdb/types"; import { SearchResult } from "@/lib/tmdb/types";
import { FC } from "react"; import { FC } from "react";
import { useEffect } from "react"; import { useEffect } from "react";
import { Pagination } from "@/components/atoms/Pagination"; import { Pagination } from "@/components/atoms/Pagination";
import { MovieList } from "../MovieList"; import { MovieList } from "../MovieList";
import { Spinner } from "@/components/atoms/Spinner";
type Props = { type Props = {
query: string; query: string;
@ -21,12 +21,17 @@ export const SearchList: FC<Props> = ({ query }) => {
page = 1, page = 1,
} = response ?? {}; } = response ?? {};
const [isLoading, setIsLoading] = useState(false);
const handleSearch = async (query: string, page: number) => { const handleSearch = async (query: string, page: number) => {
setIsLoading(true);
const data = await TMDB.search({ const data = await TMDB.search({
query, query,
page, page,
}); });
setResponse(data); setResponse(data);
setIsLoading(false);
}; };
const handlePageChange = (page: number) => { const handlePageChange = (page: number) => {
@ -43,18 +48,26 @@ export const SearchList: FC<Props> = ({ query }) => {
<section className="mb-4 md:mb-10"> <section className="mb-4 md:mb-10">
<div className="container"> <div className="container">
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
{total_results} movies found for your search {total_results} filmów znaleziono dla Twojego wyszukiwania
</p> </p>
<MovieList <div className="relative">
overrideMovies={results?.map((m) => ({ {isLoading && (
...m, <div className="absolute -inset-10 flex pt-60 justify-center backdrop-blur-xs z-10">
favorite: false, <Spinner />
seen: false, </div>
genre_ids: JSON.stringify(m.genre_ids), )}
}))} <MovieList
fluid overrideMovies={results?.map((m) => ({
/> ...m,
favorite: false,
seen: false,
genre_ids: JSON.stringify(m.genre_ids),
}))}
fluid
/>
</div>
<Pagination <Pagination
totalPages={total_pages} totalPages={total_pages}
currentPage={page} currentPage={page}

View File

@ -9,18 +9,20 @@ import { useRef } from "react";
import { useOutsideClick } from "@/hooks/useOutsideClick"; import { useOutsideClick } from "@/hooks/useOutsideClick";
import { Button } from "@/components/atoms/Button"; import { Button } from "@/components/atoms/Button";
import { MovieList } from "@/components/molecules/MovieList"; import { MovieList } from "@/components/molecules/MovieList";
import { Spinner } from "@/components/atoms/Spinner";
export const Search = () => { export const Search = () => {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const [isSearchOpen, setIsSearchOpen] = useState(false); const [isSearchOpen, setIsSearchOpen] = useState(false);
const [response, setResponse] = useState<SearchResult | null>(null); const [response, setResponse] = useState<SearchResult | null>(null);
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const isLoading = query.length > 2 && !response;
const { results, total_pages, total_results = 0 } = response ?? {}; const { results, total_pages, total_results = 0 } = response ?? {};
const handleSearch = async (query: string) => { const handleSearch = async (query: string) => {
setQuery(query); setQuery(query);
setResponse(null);
if (query.length < 3) { if (query.length < 3) {
setResponse(null);
return; return;
} }
@ -65,6 +67,11 @@ export const Search = () => {
autoFocus={true} autoFocus={true}
/> />
</div> </div>
{isLoading && (
<div className="col-span-full mt-2 lg:mt-30 text-center ">
<Spinner />
</div>
)}
{results && ( {results && (
<div className="col-span-full mt-2 lg:mt-10 text-center "> <div className="col-span-full mt-2 lg:mt-10 text-center ">
<p className="text-white">{total_results} movies found</p> <p className="text-white">{total_results} movies found</p>