Compare commits
10 Commits
37e0d00214
...
68fb45d6ef
| Author | SHA1 | Date |
|---|---|---|
|
|
68fb45d6ef | |
|
|
af4689d726 | |
|
|
01c80758bf | |
|
|
3ed7b14f1b | |
|
|
9079a52778 | |
|
|
cb0962f184 | |
|
|
137c620a48 | |
|
|
452be796f0 | |
|
|
d67e34c75c | |
|
|
50aa22ee6c |
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 80,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"arrowParens": "avoid"
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
"drizzle-orm": "^0.44.4",
|
"drizzle-orm": "^0.44.4",
|
||||||
"lightgallery": "^2.9.0-beta.1",
|
"lightgallery": "^2.9.0-beta.1",
|
||||||
|
"motion": "^12.23.12",
|
||||||
"next": "15.4.5",
|
"next": "15.4.5",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
|
|
@ -2459,6 +2460,33 @@
|
||||||
"node": ">=12.20.0"
|
"node": ">=12.20.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/framer-motion": {
|
||||||
|
"version": "12.23.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.12.tgz",
|
||||||
|
"integrity": "sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-dom": "^12.23.12",
|
||||||
|
"motion-utils": "^12.23.6",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/is-prop-valid": "*",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/is-prop-valid": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
|
@ -2855,6 +2883,47 @@
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/motion": {
|
||||||
|
"version": "12.23.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion/-/motion-12.23.12.tgz",
|
||||||
|
"integrity": "sha512-8jCD8uW5GD1csOoqh1WhH1A6j5APHVE15nuBkFeRiMzYBdRwyAHmSP/oXSuW0WJPZRXTFdBoG4hY9TFWNhhwng==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"framer-motion": "^12.23.12",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/is-prop-valid": "*",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/is-prop-valid": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/motion-dom": {
|
||||||
|
"version": "12.23.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.12.tgz",
|
||||||
|
"integrity": "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-utils": "^12.23.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/motion-utils": {
|
||||||
|
"version": "12.23.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
|
||||||
|
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
"drizzle-orm": "^0.44.4",
|
"drizzle-orm": "^0.44.4",
|
||||||
"lightgallery": "^2.9.0-beta.1",
|
"lightgallery": "^2.9.0-beta.1",
|
||||||
|
"motion": "^12.23.12",
|
||||||
"next": "15.4.5",
|
"next": "15.4.5",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export default async function RootLayout({
|
||||||
<GlobalStoreProvider initialMovies={movies}>
|
<GlobalStoreProvider initialMovies={movies}>
|
||||||
<AuroraBackground />
|
<AuroraBackground />
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<main className="relative pb-10">{children}</main>
|
<main className="relative [&>*:last-child]:pb-16">{children}</main>
|
||||||
</GlobalStoreProvider>
|
</GlobalStoreProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ export default async function GenrePage({ params }: PageProps) {
|
||||||
icon={<FaCalendar />}
|
icon={<FaCalendar />}
|
||||||
colors="green"
|
colors="green"
|
||||||
showFilters={false}
|
showFilters={false}
|
||||||
displayType="list"
|
overrideDisplayType="list"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -111,7 +111,7 @@ export default async function GenrePage({ params }: PageProps) {
|
||||||
colors="blue"
|
colors="blue"
|
||||||
showFilters={false}
|
showFilters={false}
|
||||||
sortDirection="desc"
|
sortDirection="desc"
|
||||||
displayType="list"
|
overrideDisplayType="list"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
@ -125,7 +125,7 @@ export default async function GenrePage({ params }: PageProps) {
|
||||||
icon={<FaFire />}
|
icon={<FaFire />}
|
||||||
colors="red"
|
colors="red"
|
||||||
showFilters={false}
|
showFilters={false}
|
||||||
displayType="list"
|
overrideDisplayType="list"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ export default async function Home() {
|
||||||
icon={<FaPlay />}
|
icon={<FaPlay />}
|
||||||
colors="blue"
|
colors="blue"
|
||||||
showFilters={false}
|
showFilters={false}
|
||||||
displayType="list"
|
overrideDisplayType="grid"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -61,7 +61,7 @@ export default async function Home() {
|
||||||
icon={<FaCalendar />}
|
icon={<FaCalendar />}
|
||||||
colors="blue"
|
colors="blue"
|
||||||
showFilters={false}
|
showFilters={false}
|
||||||
displayType="list"
|
overrideDisplayType="grid"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -73,7 +73,7 @@ export default async function Home() {
|
||||||
icon={<FaFire />}
|
icon={<FaFire />}
|
||||||
colors="red"
|
colors="red"
|
||||||
showFilters={false}
|
showFilters={false}
|
||||||
displayType="list"
|
overrideDisplayType="grid"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -85,7 +85,7 @@ export default async function Home() {
|
||||||
icon={<FaChartLine />}
|
icon={<FaChartLine />}
|
||||||
colors="green"
|
colors="green"
|
||||||
showFilters={false}
|
showFilters={false}
|
||||||
displayType="list"
|
overrideDisplayType="grid"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import { GenreList } from "@/components/molecules/GenreList";
|
import { GenreList } from '@/components/molecules/GenreList';
|
||||||
import { MovieList } from "@/components/molecules/MovieList";
|
import { MovieList } from '@/components/molecules/MovieList';
|
||||||
import { TrackedMovies } from "@/components/molecules/TrackedMovies";
|
import { RandomMovie } from '@/components/molecules/RandomMovie';
|
||||||
|
import { TrackedMovies } from '@/components/molecules/TrackedMovies';
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TrackedMovies />
|
<TrackedMovies />
|
||||||
<MovieList heading="Moja lista" />
|
<MovieList heading="Moja lista" />
|
||||||
|
<RandomMovie heading="Ciężko wybrać?" />
|
||||||
<GenreList heading="Odkrywaj nowe filmy według gatunku" />
|
<GenreList heading="Odkrywaj nowe filmy według gatunku" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,14 @@ export const Button: FC<Props> = ({
|
||||||
|
|
||||||
const buttonColor = gradient ?? colors[theme];
|
const buttonColor = gradient ?? colors[theme];
|
||||||
|
|
||||||
|
if (theme === "slate" && !className.includes("shadow-")) {
|
||||||
|
className += " shadow-cyan-500/20 hover:shadow-cyan-500/40";
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
className={`flex items-center justify-center gap-2 cursor-pointer text-white rounded-xl font-semibold shadow-2xl transition-colors duration-300
|
className={`flex items-center justify-center gap-2 cursor-pointer text-white rounded-xl font-semibold shadow-2xl transition-all duration-300
|
||||||
bg-gradient-to-r ${buttonColor?.from} ${buttonColor?.to} cursor-pointer ${sizes[size]} ${className}`}
|
bg-gradient-to-br ${buttonColor?.from} ${buttonColor?.to} cursor-pointer ${sizes[size]} ${className}`}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
{...(href && { href })}
|
{...(href && { href })}
|
||||||
>
|
>
|
||||||
|
|
@ -45,7 +49,7 @@ const sizes = {
|
||||||
small: "px-4 py-2 text-sm",
|
small: "px-4 py-2 text-sm",
|
||||||
medium: "px-8 py-4 text-lg",
|
medium: "px-8 py-4 text-lg",
|
||||||
large: "px-12 py-6 text-xl",
|
large: "px-12 py-6 text-xl",
|
||||||
icon: "p-3 [&>*]:w-5 [&>*]:h-5",
|
icon: "w-12 h-12 !rounded-full border border-white/20 hover:scale-105 [&>svg]:w-5 [&>svg]:h-5 shadow-lg ",
|
||||||
};
|
};
|
||||||
|
|
||||||
const colors = {
|
const colors = {
|
||||||
|
|
@ -55,30 +59,38 @@ const colors = {
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
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-cyan-600 hover:to-cyan-500",
|
||||||
},
|
},
|
||||||
glass: {
|
glass: {
|
||||||
from: "from-white/15 via-white/8 to-white/12 border border-white/20",
|
from: "from-white/15 border border-white/20",
|
||||||
to: "to-white/15 hover:to-white/10",
|
to: "to-white/5 hover:to-white/10",
|
||||||
},
|
},
|
||||||
rose: {
|
rosePink: {
|
||||||
from: "from-rose-600/90 hover:from-rose-500/90",
|
from: "from-rose-600/90 hover:from-rose-500/90",
|
||||||
to: "to-pink-600/90 hover:to-pink-500/90",
|
to: "to-pink-600/90 hover:to-pink-500/90",
|
||||||
},
|
},
|
||||||
emerald: {
|
emeraldTeal: {
|
||||||
from: "from-emerald-600/90 hover:from-emerald-500/90",
|
from: "from-emerald-600/90 hover:from-emerald-500/90",
|
||||||
to: "to-teal-600/90 hover:to-teal-500/90",
|
to: "to-teal-600/90 hover:to-teal-500/90",
|
||||||
},
|
},
|
||||||
purple: {
|
purplePink: {
|
||||||
from: "from-purple-600/90 hover:from-purple-500/90",
|
from: "from-purple-600/90 hover:from-purple-500/90",
|
||||||
to: "to-pink-600/90 hover:to-pink-500/90",
|
to: "to-pink-600/90 hover:to-pink-500/90",
|
||||||
},
|
},
|
||||||
pink: {
|
pinkEmerald: {
|
||||||
from: "from-pink-600/90 hover:from-pink-500/90",
|
from: "from-pink-600/90 hover:from-pink-500/90",
|
||||||
to: "to-emerald-600/90 hover:to-emerald-500/90",
|
to: "to-emerald-600/90 hover:to-emerald-500/90",
|
||||||
},
|
},
|
||||||
teal: {
|
tealEmerald: {
|
||||||
from: "from-teal-600/90 hover:from-teal-500/90",
|
from: "from-teal-600/90 hover:from-teal-500/90",
|
||||||
to: "to-emerald-600/90 hover:to-emerald-500/90",
|
to: "to-emerald-600/90 hover:to-emerald-500/90",
|
||||||
},
|
},
|
||||||
|
cyanPurple: {
|
||||||
|
from: "from-cyan-600/90 hover:from-cyan-500/90",
|
||||||
|
to: "to-purple-600/90 hover:to-purple-500/90",
|
||||||
|
},
|
||||||
|
slate: {
|
||||||
|
from: "from-slate-800/95",
|
||||||
|
to: "to-slate-900/95",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export const Dropdown: FC<Props> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className="relative inline-block">
|
<div ref={ref} className="relative inline-block">
|
||||||
<Button theme="glass" size="icon" onClick={() => setIsOpen(!isOpen)}>
|
<Button theme="slate" size="icon" onClick={() => setIsOpen(!isOpen)}>
|
||||||
{icon || <FaFilter />}
|
{icon || <FaFilter />}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,18 @@
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { useGlobalStore } from "@/app/store/globalStore";
|
import { useGlobalStore } from "@/app/store/globalStore";
|
||||||
import { Movie } from "@/types/global";
|
import { Movie } from "@/types/global";
|
||||||
import {
|
import { FaFire, FaPlusCircle, FaTrash } from "react-icons/fa";
|
||||||
AuroraLayout,
|
import Link from "next/link";
|
||||||
MinimalLayout,
|
import { RxEyeOpen } from "react-icons/rx";
|
||||||
ZeusLayout,
|
import { MdFavorite } from "react-icons/md";
|
||||||
DefaultLayout,
|
import { RiCalendarCheckLine, RiCalendarScheduleLine } from "react-icons/ri";
|
||||||
} from "./layouts";
|
|
||||||
|
|
||||||
type Props = Movie & {
|
type Props = Movie & {
|
||||||
layout?: "default" | "zeus" | "minimal" | "aurora";
|
|
||||||
showDayCounter?: boolean;
|
showDayCounter?: boolean;
|
||||||
simpleToggle?: boolean;
|
simpleToggle?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MovieCard: FC<Props> = ({
|
export const MovieCard: FC<Props> = ({
|
||||||
layout = "aurora",
|
|
||||||
showDayCounter = true,
|
showDayCounter = true,
|
||||||
simpleToggle = false,
|
simpleToggle = false,
|
||||||
...movie
|
...movie
|
||||||
|
|
@ -27,14 +24,21 @@ export const MovieCard: FC<Props> = ({
|
||||||
deleteMovie: deleteMovieFromStore,
|
deleteMovie: deleteMovieFromStore,
|
||||||
updateMovie: updateMovieInStore,
|
updateMovie: updateMovieInStore,
|
||||||
} = useGlobalStore();
|
} = useGlobalStore();
|
||||||
|
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 favorite = alreadyInStore?.favorite || movie.favorite;
|
||||||
|
|
||||||
const isReleased = new Date(movie.release_date) < new Date();
|
const isReleased = new Date(movie.release_date) < new Date();
|
||||||
const iconSize = 48;
|
const scoreColor =
|
||||||
const buttonClass =
|
vote_average >= 8
|
||||||
"p-4 text-sm transition-colors cursor-pointer text-center group/toggle";
|
? "from-emerald-400 to-teal-400"
|
||||||
|
: vote_average >= 6
|
||||||
|
? "from-yellow-400 to-orange-400"
|
||||||
|
: "from-red-400 to-pink-400";
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
addMovieToStore(movie);
|
addMovieToStore(movie);
|
||||||
|
|
@ -65,32 +69,161 @@ export const MovieCard: FC<Props> = ({
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const commonProps = {
|
return (
|
||||||
...movie,
|
<article className="group relative w-full overflow-hidden rounded-2xl max-w-[300px] mx-auto">
|
||||||
alreadyInStore,
|
{/* Main card container */}
|
||||||
isReleased,
|
<div className="grid relative h-full bg-gradient-to-br from-slate-800/95 via-slate-850/97 to-slate-900/95 border border-slate-700/50 shadow-2xl shadow-purple-500/10 group-hover:shadow-purple-500/20 transition-all duration-500">
|
||||||
handleAdd,
|
{/* Image section with sophisticated overlay */}
|
||||||
handleRemove,
|
<figure className="relative overflow-hidden aspect-[4/3] lg:aspect-[342/513]">
|
||||||
handleSeen,
|
<Link href={`/film/${id}`}>
|
||||||
handleFavorite,
|
<img
|
||||||
daysSinceRelease,
|
className="w-full h-full object-cover transition-all duration-700 hover:scale-110 hover:brightness-110 bg-gradient-to-b from-purple-600/50 to-emerald-600"
|
||||||
releaseDate,
|
src={`http://image.tmdb.org/t/p/w342${poster_path}`}
|
||||||
showDayCounter,
|
alt={title}
|
||||||
simpleToggle,
|
/>
|
||||||
buttonClass,
|
</Link>
|
||||||
iconSize,
|
|
||||||
favorite: alreadyInStore?.favorite || movie.favorite,
|
|
||||||
seen: alreadyInStore?.seen || movie.seen,
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (layout) {
|
{/* Gradient overlays for depth */}
|
||||||
case "aurora":
|
<div className="absolute inset-0 bg-gradient-to-t from-slate-900 via-slate-900/20 to-transparent pointer-events-none" />
|
||||||
return <AuroraLayout {...commonProps} />;
|
|
||||||
case "minimal":
|
{/* Floating rating badge */}
|
||||||
return <MinimalLayout {...commonProps} />;
|
{!!vote_average && (
|
||||||
case "zeus":
|
<div className="absolute top-4 right-4 transform rotate-3 group-hover:rotate-0 transition-transform duration-300">
|
||||||
return <ZeusLayout {...commonProps} />;
|
<div
|
||||||
default:
|
className={`bg-gradient-to-r ${scoreColor} p-2 rounded-xl shadow-lg border border-white/10`}
|
||||||
return <DefaultLayout {...commonProps} />;
|
>
|
||||||
}
|
<div className="flex items-center gap-2 text-white font-bold">
|
||||||
|
<span className="text-xl">★</span>
|
||||||
|
<span className="text-lg">{vote_average.toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Popularity indicator */}
|
||||||
|
<div className="absolute top-4 left-4 bg-gradient-to-br from-black/80 to-slate-900/85 px-3 py-2 rounded-xl border border-white/20 shadow-lg">
|
||||||
|
<div className="flex items-center gap-2 text-orange-400">
|
||||||
|
<FaFire className="animate-pulse" />
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{Math.round(popularity)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Days left to release */}
|
||||||
|
{(!isReleased || daysSinceRelease < 35) && (
|
||||||
|
<div className="absolute bottom-4 left-4 flex justify-center">
|
||||||
|
<p className="text-white bg-gradient-to-r from-black/75 to-slate-900/80 px-2.5 leading-[2] rounded-xl border border-white/20 text-xs shadow-lg">
|
||||||
|
{isReleased &&
|
||||||
|
daysSinceRelease < 35 &&
|
||||||
|
`od ${daysSinceRelease} dni`}
|
||||||
|
{!isReleased && `za ${daysSinceRelease} dni`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Status indicators */}
|
||||||
|
<div className="absolute bottom-4 right-4 flex gap-2">
|
||||||
|
{alreadyInStore && !simpleToggle && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
seen
|
||||||
|
? "bg-gradient-to-r from-emerald-500/95 to-emerald-600/90"
|
||||||
|
: "bg-gradient-to-r from-white/25 to-white/15"
|
||||||
|
} p-2 rounded-full cursor-pointer hover:bg-emerald-400 transition-colors border border-white/10 shadow-lg`}
|
||||||
|
onClick={handleSeen}
|
||||||
|
>
|
||||||
|
<RxEyeOpen size={16} className="text-white" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
favorite
|
||||||
|
? "bg-gradient-to-r from-rose-500/95 to-rose-600/90"
|
||||||
|
: "bg-gradient-to-r from-white/25 to-white/15"
|
||||||
|
} p-2 rounded-full cursor-pointer hover:bg-rose-400 transition-colors border border-white/10 shadow-lg`}
|
||||||
|
onClick={handleFavorite}
|
||||||
|
>
|
||||||
|
<MdFavorite size={16} className="text-white" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!alreadyInStore && (
|
||||||
|
<div
|
||||||
|
className={`bg-gradient-to-r from-emerald-500/50 to-emerald-600/50 p-2 rounded-full cursor-pointer hover:bg-emerald-400 transition-colors border border-white/10 shadow-lg`}
|
||||||
|
onClick={handleAdd}
|
||||||
|
>
|
||||||
|
<FaPlusCircle size={16} className="text-white" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{alreadyInStore && (
|
||||||
|
<div
|
||||||
|
className={`bg-gradient-to-r from-red-400/25 to-red-400/15 p-2 rounded-full cursor-pointer hover:bg-red-400 transition-colors border border-white/10 shadow-lg`}
|
||||||
|
onClick={handleRemove}
|
||||||
|
>
|
||||||
|
<FaTrash size={16} className="text-white" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
{/* Content section with glowing effects */}
|
||||||
|
<div className="relative p-6 flex flex-col justify-between">
|
||||||
|
<div className="relative z-10">
|
||||||
|
<Link href={`/film/${id}`}>
|
||||||
|
<h3 className="font-bold text-xl leading-tight mb-3 transition-colors duration-500 hover:text-secondary flex items-center gap-2">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-400 line-clamp-2 leading-relaxed opacity-80 transition-colors duration-300 hover:text-secondary">
|
||||||
|
{overview}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom section with enhanced styling */}
|
||||||
|
<div className="relative z-10 flex items-center justify-between pt-4 mt-4 border-t border-gradient-to-r border-slate-700/50">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-1 text-sm ${
|
||||||
|
isReleased ? "text-emerald-400" : "text-amber-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isReleased ? (
|
||||||
|
<RiCalendarCheckLine />
|
||||||
|
) : (
|
||||||
|
<RiCalendarScheduleLine />
|
||||||
|
)}
|
||||||
|
<span className="font-medium">
|
||||||
|
{releaseDate.toLocaleDateString("pl-PL", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "short",
|
||||||
|
year: "numeric",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{alreadyInStore && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{seen && (
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 bg-gradient-to-r from-emerald-400 to-teal-400 rounded-full shadow-lg shadow-emerald-400/50 animate-pulse"
|
||||||
|
title="Watched"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{favorite && (
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 bg-gradient-to-r from-rose-400 to-pink-400 rounded-full shadow-lg shadow-rose-400/50 animate-pulse"
|
||||||
|
title="Favorite"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,207 +0,0 @@
|
||||||
"use client";
|
|
||||||
import { FC, useState } from "react";
|
|
||||||
import { MdFavorite } from "react-icons/md";
|
|
||||||
import { RxEyeOpen } from "react-icons/rx";
|
|
||||||
import { FaFire, FaTrash, FaPlusCircle } from "react-icons/fa";
|
|
||||||
import { RiCalendarCheckLine, RiCalendarScheduleLine } from "react-icons/ri";
|
|
||||||
import { Movie } from "@/types/global";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
interface AuroraLayoutProps extends Movie {
|
|
||||||
showDayCounter?: boolean;
|
|
||||||
simpleToggle?: boolean;
|
|
||||||
alreadyInStore?: Movie | undefined;
|
|
||||||
isReleased: boolean;
|
|
||||||
handleAdd: () => void;
|
|
||||||
handleRemove: () => void;
|
|
||||||
handleSeen: () => void;
|
|
||||||
handleFavorite: () => void;
|
|
||||||
daysSinceRelease: number;
|
|
||||||
releaseDate: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AuroraLayout: FC<AuroraLayoutProps> = ({
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
overview,
|
|
||||||
popularity,
|
|
||||||
release_date,
|
|
||||||
poster_path,
|
|
||||||
vote_average,
|
|
||||||
seen,
|
|
||||||
favorite,
|
|
||||||
alreadyInStore,
|
|
||||||
isReleased,
|
|
||||||
handleAdd,
|
|
||||||
handleRemove,
|
|
||||||
handleSeen,
|
|
||||||
handleFavorite,
|
|
||||||
daysSinceRelease,
|
|
||||||
releaseDate,
|
|
||||||
simpleToggle,
|
|
||||||
}) => {
|
|
||||||
const scoreColor =
|
|
||||||
vote_average >= 8
|
|
||||||
? "from-emerald-400 to-teal-400"
|
|
||||||
: vote_average >= 6
|
|
||||||
? "from-yellow-400 to-orange-400"
|
|
||||||
: "from-red-400 to-pink-400";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<article className="group relative w-full overflow-hidden rounded-2xl max-w-[300px] mx-auto">
|
|
||||||
{/* Main card container */}
|
|
||||||
<div className="grid relative h-full bg-gradient-to-br from-slate-800/95 via-slate-850/97 to-slate-900/95 border border-slate-700/50 shadow-2xl shadow-purple-500/10 group-hover:shadow-purple-500/20 transition-all duration-500">
|
|
||||||
{/* Image section with sophisticated overlay */}
|
|
||||||
<figure className="relative overflow-hidden aspect-[4/3] lg:aspect-[342/513]">
|
|
||||||
<Link href={`/film/${id}`}>
|
|
||||||
<img
|
|
||||||
className="w-full h-full object-cover transition-all duration-700 hover:scale-110 hover:brightness-110 bg-gradient-to-b from-purple-600/50 to-emerald-600"
|
|
||||||
src={`http://image.tmdb.org/t/p/w342${poster_path}`}
|
|
||||||
alt={title}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{/* Gradient overlays for depth */}
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-slate-900 via-slate-900/20 to-transparent pointer-events-none" />
|
|
||||||
|
|
||||||
{/* Floating rating badge */}
|
|
||||||
{!!vote_average && (
|
|
||||||
<div className="absolute top-4 right-4 transform rotate-3 group-hover:rotate-0 transition-transform duration-300">
|
|
||||||
<div
|
|
||||||
className={`bg-gradient-to-r ${scoreColor} p-2 rounded-xl shadow-lg border border-white/10`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 text-white font-bold">
|
|
||||||
<span className="text-xl">★</span>
|
|
||||||
<span className="text-lg">{vote_average.toFixed(1)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Popularity indicator */}
|
|
||||||
<div className="absolute top-4 left-4 bg-gradient-to-br from-black/80 to-slate-900/85 px-3 py-2 rounded-xl border border-white/20 shadow-lg">
|
|
||||||
<div className="flex items-center gap-2 text-orange-400">
|
|
||||||
<FaFire className="animate-pulse" />
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{Math.round(popularity)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Days left to release */}
|
|
||||||
{(!isReleased || daysSinceRelease < 35) && (
|
|
||||||
<div className="absolute bottom-4 left-4 flex justify-center">
|
|
||||||
<p className="text-white bg-gradient-to-r from-black/75 to-slate-900/80 px-2.5 leading-[2] rounded-xl border border-white/20 text-xs shadow-lg">
|
|
||||||
{isReleased &&
|
|
||||||
daysSinceRelease < 35 &&
|
|
||||||
`od ${daysSinceRelease} dni`}
|
|
||||||
{!isReleased && `za ${daysSinceRelease} dni`}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Status indicators */}
|
|
||||||
<div className="absolute bottom-4 right-4 flex gap-2">
|
|
||||||
{alreadyInStore && !simpleToggle && (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={`${
|
|
||||||
seen
|
|
||||||
? "bg-gradient-to-r from-emerald-500/95 to-emerald-600/90"
|
|
||||||
: "bg-gradient-to-r from-white/25 to-white/15"
|
|
||||||
} p-2 rounded-full cursor-pointer hover:bg-emerald-400 transition-colors border border-white/10 shadow-lg`}
|
|
||||||
onClick={handleSeen}
|
|
||||||
>
|
|
||||||
<RxEyeOpen size={16} className="text-white" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`${
|
|
||||||
favorite
|
|
||||||
? "bg-gradient-to-r from-rose-500/95 to-rose-600/90"
|
|
||||||
: "bg-gradient-to-r from-white/25 to-white/15"
|
|
||||||
} p-2 rounded-full cursor-pointer hover:bg-rose-400 transition-colors border border-white/10 shadow-lg`}
|
|
||||||
onClick={handleFavorite}
|
|
||||||
>
|
|
||||||
<MdFavorite size={16} className="text-white" />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!alreadyInStore && (
|
|
||||||
<div
|
|
||||||
className={`bg-gradient-to-r from-emerald-500/50 to-emerald-600/50 p-2 rounded-full cursor-pointer hover:bg-emerald-400 transition-colors border border-white/10 shadow-lg`}
|
|
||||||
onClick={handleAdd}
|
|
||||||
>
|
|
||||||
<FaPlusCircle size={16} className="text-white" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{alreadyInStore && (
|
|
||||||
<div
|
|
||||||
className={`bg-gradient-to-r from-red-400/25 to-red-400/15 p-2 rounded-full cursor-pointer hover:bg-red-400 transition-colors border border-white/10 shadow-lg`}
|
|
||||||
onClick={handleRemove}
|
|
||||||
>
|
|
||||||
<FaTrash size={16} className="text-white" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
{/* Content section with glowing effects */}
|
|
||||||
<div className="relative p-6 flex flex-col justify-between">
|
|
||||||
<div className="relative z-10">
|
|
||||||
<Link href={`/film/${id}`}>
|
|
||||||
<h3 className="font-bold text-xl leading-tight mb-3 transition-colors duration-500 hover:text-secondary flex items-center gap-2">
|
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-400 line-clamp-2 leading-relaxed opacity-80 transition-colors duration-300 hover:text-secondary">
|
|
||||||
{overview}
|
|
||||||
</p>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bottom section with enhanced styling */}
|
|
||||||
<div className="relative z-10 flex items-center justify-between pt-4 mt-4 border-t border-gradient-to-r border-slate-700/50">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div
|
|
||||||
className={`flex items-center gap-1 text-sm ${
|
|
||||||
isReleased ? "text-emerald-400" : "text-amber-400"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isReleased ? (
|
|
||||||
<RiCalendarCheckLine />
|
|
||||||
) : (
|
|
||||||
<RiCalendarScheduleLine />
|
|
||||||
)}
|
|
||||||
<span className="font-medium">
|
|
||||||
{releaseDate.toLocaleDateString("pl-PL", {
|
|
||||||
day: "numeric",
|
|
||||||
month: "short",
|
|
||||||
year: "numeric",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{alreadyInStore && (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{seen && (
|
|
||||||
<div
|
|
||||||
className="w-3 h-3 bg-gradient-to-r from-emerald-400 to-teal-400 rounded-full shadow-lg shadow-emerald-400/50 animate-pulse"
|
|
||||||
title="Watched"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{favorite && (
|
|
||||||
<div
|
|
||||||
className="w-3 h-3 bg-gradient-to-r from-rose-400 to-pink-400 rounded-full shadow-lg shadow-rose-400/50 animate-pulse"
|
|
||||||
title="Favorite"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
"use client";
|
|
||||||
import { FC } from "react";
|
|
||||||
import { ReadMore } from "../../ReadMore";
|
|
||||||
import { Movie } from "@/types/global";
|
|
||||||
|
|
||||||
interface DefaultLayoutProps extends Movie {
|
|
||||||
showDayCounter?: boolean;
|
|
||||||
simpleToggle?: boolean;
|
|
||||||
alreadyInStore?: Movie | undefined;
|
|
||||||
isReleased: boolean;
|
|
||||||
handleAdd: () => void;
|
|
||||||
handleRemove: () => void;
|
|
||||||
handleSeen: () => void;
|
|
||||||
handleFavorite: () => void;
|
|
||||||
buttonClass: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DefaultLayout: FC<DefaultLayoutProps> = ({
|
|
||||||
title,
|
|
||||||
overview,
|
|
||||||
popularity,
|
|
||||||
release_date,
|
|
||||||
poster_path,
|
|
||||||
alreadyInStore,
|
|
||||||
isReleased,
|
|
||||||
handleAdd,
|
|
||||||
handleRemove,
|
|
||||||
handleSeen,
|
|
||||||
handleFavorite,
|
|
||||||
buttonClass,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="flex w-full shadow-md rounded-lg overflow-hidden mx-auto group/card">
|
|
||||||
<div className="overflow-hidden rounded-xl relative movie-item text-white movie-card">
|
|
||||||
<div className="absolute inset-0 z-10 bg-gradient-to-t from-black via-gray-900 to-transparent"></div>
|
|
||||||
<div className="relative group z-10 p-6 space-y-6 h-full">
|
|
||||||
<div className="align-self-end w-full h-full flex flex-col">
|
|
||||||
<div className="h-64"></div>
|
|
||||||
<div className="flex flex-col space-y-2 inner mb-4">
|
|
||||||
<h3
|
|
||||||
className="text-lg leading-[1.3] font-bold text-white line-clamp-1"
|
|
||||||
title={title}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
<div className="text-xs text-gray-400">
|
|
||||||
<ReadMore text={overview} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row justify-between mt-auto">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div className="text-sm text-gray-400">Popularity:</div>
|
|
||||||
<div className="popularity">{popularity}</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div className="text-sm text-gray-400">Release date:</div>
|
|
||||||
<div className="release">{release_date}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="absolute top-0 z-10 bg-transparent inset-0 group-hover/card:bg-black/50 transition-all opacity-0 group-hover/card:opacity-100 flex flex-col justify-center">
|
|
||||||
{!alreadyInStore && (
|
|
||||||
<button
|
|
||||||
className={`${buttonClass} bg-primary/70 text-white hover:bg-primary`}
|
|
||||||
onClick={handleAdd}
|
|
||||||
>
|
|
||||||
Add to list
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{alreadyInStore && (
|
|
||||||
<>
|
|
||||||
{isReleased && (
|
|
||||||
<button
|
|
||||||
className={`${buttonClass} bg-accent/70 text-white hover:bg-accent`}
|
|
||||||
onClick={handleSeen}
|
|
||||||
>
|
|
||||||
{alreadyInStore.seen ? "Mark as unseen" : "Mark as seen"}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
className={`${buttonClass} bg-amber-400/70 text-white hover:bg-amber-500`}
|
|
||||||
onClick={handleFavorite}
|
|
||||||
>
|
|
||||||
{alreadyInStore.favorite
|
|
||||||
? "Remove favorite"
|
|
||||||
: "Add to favorites"}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className={`${buttonClass} bg-primary/70 text-white hover:bg-primary`}
|
|
||||||
onClick={handleRemove}
|
|
||||||
>
|
|
||||||
Remove from list
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<figure className="absolute inset-0 w-full bottom-[20%]">
|
|
||||||
<img
|
|
||||||
className="w-full h-96 object-cover"
|
|
||||||
src={`http://image.tmdb.org/t/p/w342${poster_path}`}
|
|
||||||
/>
|
|
||||||
</figure>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
"use client";
|
|
||||||
import { FC } from "react";
|
|
||||||
import { ReadMore } from "../../ReadMore";
|
|
||||||
import { MdFavorite, MdFavoriteBorder } from "react-icons/md";
|
|
||||||
import { RxEyeOpen, RxEyeClosed } from "react-icons/rx";
|
|
||||||
import { IoMdRemoveCircleOutline } from "react-icons/io";
|
|
||||||
import { Movie } from "@/types/global";
|
|
||||||
|
|
||||||
interface MinimalLayoutProps extends Movie {
|
|
||||||
showDayCounter?: boolean;
|
|
||||||
simpleToggle?: boolean;
|
|
||||||
alreadyInStore?: Movie | undefined;
|
|
||||||
isReleased: boolean;
|
|
||||||
handleAdd: () => void;
|
|
||||||
handleRemove: () => void;
|
|
||||||
handleSeen: () => void;
|
|
||||||
handleFavorite: () => void;
|
|
||||||
releaseDate: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MinimalLayout: FC<MinimalLayoutProps> = ({
|
|
||||||
title,
|
|
||||||
overview,
|
|
||||||
poster_path,
|
|
||||||
vote_average,
|
|
||||||
seen,
|
|
||||||
favorite,
|
|
||||||
alreadyInStore,
|
|
||||||
isReleased,
|
|
||||||
handleAdd,
|
|
||||||
handleRemove,
|
|
||||||
handleSeen,
|
|
||||||
handleFavorite,
|
|
||||||
releaseDate,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<article className="group relative w-full h-[420px] bg-gradient-to-br from-white/8 via-slate-800/5 to-white/5 border border-white/10 rounded-xl overflow-hidden transition-all duration-300 hover:bg-gradient-to-br hover:from-white/15 hover:to-white/8 hover:border-white/20 hover:shadow-lg hover:shadow-black/20">
|
|
||||||
<figure className="relative h-[280px] overflow-hidden">
|
|
||||||
<img
|
|
||||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
|
|
||||||
src={`http://image.tmdb.org/t/p/w342${poster_path}`}
|
|
||||||
alt={title}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Rating badge */}
|
|
||||||
{!!vote_average && (
|
|
||||||
<div className="absolute top-3 right-3 bg-gradient-to-br from-black/75 to-slate-900/80 px-2 pr-3 pb-1 rounded-full border border-white/10 shadow-lg">
|
|
||||||
<span className="text-xs font-medium text-yellow-400">
|
|
||||||
★ {vote_average.toFixed(1)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Action overlay */}
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/85 via-slate-900/75 to-black/60 opacity-0 group-hover:opacity-100 transition-all duration-300 flex items-center justify-center">
|
|
||||||
{!alreadyInStore ? (
|
|
||||||
<button
|
|
||||||
onClick={handleAdd}
|
|
||||||
className="bg-white text-black px-4 py-2 rounded-lg font-medium text-sm transition-all duration-200 hover:bg-gray-100 hover:scale-105"
|
|
||||||
>
|
|
||||||
Add to List
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{isReleased && (
|
|
||||||
<button
|
|
||||||
onClick={handleSeen}
|
|
||||||
className={`p-2 rounded-lg transition-all duration-200 hover:scale-110 ${
|
|
||||||
seen
|
|
||||||
? "bg-green-500 text-white"
|
|
||||||
: "bg-white/20 text-white hover:bg-white/30"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{seen ? <RxEyeOpen size={20} /> : <RxEyeClosed size={20} />}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={handleFavorite}
|
|
||||||
className={`p-2 rounded-lg transition-all duration-200 hover:scale-110 ${
|
|
||||||
favorite
|
|
||||||
? "bg-red-500 text-white"
|
|
||||||
: "bg-white/20 text-white hover:bg-white/30"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{favorite ? (
|
|
||||||
<MdFavorite size={20} />
|
|
||||||
) : (
|
|
||||||
<MdFavoriteBorder size={20} />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleRemove}
|
|
||||||
className="p-2 rounded-lg bg-white/20 text-white hover:bg-red-500 transition-all duration-200 hover:scale-110"
|
|
||||||
>
|
|
||||||
<IoMdRemoveCircleOutline size={20} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
{/* Content section */}
|
|
||||||
<div className="p-4 flex flex-col justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-lg leading-tight line-clamp-2 mb-2 transition-colors duration-200 group-hover:text-white/90">
|
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
<div className="text-sm text-gray-400 leading-relaxed">
|
|
||||||
<ReadMore text={overview} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between mt-3 pt-3 border-t border-white/10">
|
|
||||||
<span className="text-xs text-gray-500 font-medium">
|
|
||||||
{releaseDate.toLocaleDateString("pl-PL", {
|
|
||||||
day: "numeric",
|
|
||||||
month: "long",
|
|
||||||
year: "numeric",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
{alreadyInStore && (
|
|
||||||
<div className="flex gap-1">
|
|
||||||
{seen && (
|
|
||||||
<div
|
|
||||||
className="w-2 h-2 bg-green-400 rounded-full"
|
|
||||||
title="Watched"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{favorite && (
|
|
||||||
<div
|
|
||||||
className="w-2 h-2 bg-red-400 rounded-full"
|
|
||||||
title="Favorite"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,166 +0,0 @@
|
||||||
"use client";
|
|
||||||
import { FC } from "react";
|
|
||||||
import { ReadMore } from "../../ReadMore";
|
|
||||||
import { MdFavorite, MdFavoriteBorder, MdOutlinePostAdd } from "react-icons/md";
|
|
||||||
import { RxEyeOpen, RxEyeClosed } from "react-icons/rx";
|
|
||||||
import { IoMdRemoveCircleOutline } from "react-icons/io";
|
|
||||||
import { FaFire } from "react-icons/fa";
|
|
||||||
import { RiCalendarCheckLine, RiCalendarScheduleLine } from "react-icons/ri";
|
|
||||||
import { Movie } from "@/types/global";
|
|
||||||
|
|
||||||
interface ZeusLayoutProps extends Movie {
|
|
||||||
showDayCounter?: boolean;
|
|
||||||
simpleToggle?: boolean;
|
|
||||||
alreadyInStore?: Movie | undefined;
|
|
||||||
isReleased: boolean;
|
|
||||||
handleAdd: () => void;
|
|
||||||
handleRemove: () => void;
|
|
||||||
handleSeen: () => void;
|
|
||||||
handleFavorite: () => void;
|
|
||||||
daysSinceRelease: number;
|
|
||||||
releaseDate: Date;
|
|
||||||
buttonClass: string;
|
|
||||||
iconSize: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ZeusLayout: FC<ZeusLayoutProps> = ({
|
|
||||||
title,
|
|
||||||
overview,
|
|
||||||
popularity,
|
|
||||||
poster_path,
|
|
||||||
vote_average,
|
|
||||||
seen,
|
|
||||||
favorite,
|
|
||||||
showDayCounter,
|
|
||||||
simpleToggle,
|
|
||||||
alreadyInStore,
|
|
||||||
isReleased,
|
|
||||||
handleAdd,
|
|
||||||
handleRemove,
|
|
||||||
handleSeen,
|
|
||||||
handleFavorite,
|
|
||||||
daysSinceRelease,
|
|
||||||
releaseDate,
|
|
||||||
buttonClass,
|
|
||||||
iconSize,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<article className="flex flex-col w-full shadow-lg rounded-t-lg overflow-hidden bg-black/50 shadow-white/5">
|
|
||||||
<figure className="relative ">
|
|
||||||
<img
|
|
||||||
style={{
|
|
||||||
aspectRatio: "342/513",
|
|
||||||
}}
|
|
||||||
className="w-full object-cover"
|
|
||||||
src={`http://image.tmdb.org/t/p/w342${poster_path}`}
|
|
||||||
/>
|
|
||||||
<span className="absolute inset-0 bg-gradient-to-t from-black/60 via-slate-900/40 to-black/30 opacity-0 hover-any:opacity-100 transition-opacity duration-300 flex items-center justify-center cursor-pointer">
|
|
||||||
{!alreadyInStore && (
|
|
||||||
<button className={buttonClass} onClick={handleAdd}>
|
|
||||||
<MdOutlinePostAdd size={64} color="white" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{alreadyInStore && (
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<>
|
|
||||||
{isReleased && !simpleToggle && (
|
|
||||||
<button
|
|
||||||
className={`${buttonClass} text-white`}
|
|
||||||
onClick={handleSeen}
|
|
||||||
>
|
|
||||||
<span className="group-hover/toggle:hidden">
|
|
||||||
{seen ? (
|
|
||||||
<RxEyeOpen size={iconSize} />
|
|
||||||
) : (
|
|
||||||
<RxEyeClosed size={iconSize} />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span className="hidden group-hover/toggle:block">
|
|
||||||
{seen ? (
|
|
||||||
<RxEyeClosed size={iconSize} />
|
|
||||||
) : (
|
|
||||||
<RxEyeOpen size={iconSize} />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!simpleToggle && (
|
|
||||||
<button
|
|
||||||
className={`${buttonClass} text-amber-400`}
|
|
||||||
onClick={handleFavorite}
|
|
||||||
>
|
|
||||||
<span className="group-hover/toggle:hidden">
|
|
||||||
{favorite ? (
|
|
||||||
<MdFavorite size={iconSize} />
|
|
||||||
) : (
|
|
||||||
<MdFavoriteBorder size={iconSize} />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span className="hidden group-hover/toggle:block">
|
|
||||||
{favorite ? (
|
|
||||||
<MdFavoriteBorder size={iconSize} />
|
|
||||||
) : (
|
|
||||||
<MdFavorite size={iconSize} />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
className={`${buttonClass} text-red-500`}
|
|
||||||
onClick={handleRemove}
|
|
||||||
>
|
|
||||||
<IoMdRemoveCircleOutline size={iconSize} />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span className="absolute top-0 right-0 bg-black/50 px-2 py-1 rounded-bl-lg">
|
|
||||||
<p className="text-sm text-white flex items-center gap-1">
|
|
||||||
<FaFire />
|
|
||||||
{popularity}
|
|
||||||
</p>
|
|
||||||
</span>
|
|
||||||
</figure>
|
|
||||||
<div className="p-4">
|
|
||||||
{!!vote_average && (
|
|
||||||
<p className="flex items-center gap-1 text-sm mb-2">
|
|
||||||
<span className="text-yellow-400">★</span>
|
|
||||||
<span>{vote_average.toFixed(1)}/10</span>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<h2 className="text-xl leading-[1.1] font-bold">{title}</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p
|
|
||||||
className={`text-sm mt-2 flex items-center gap-1 leading-[1.1] ${
|
|
||||||
isReleased ? "text-green-700" : "text-yellow-500"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isReleased ? <RiCalendarCheckLine /> : <RiCalendarScheduleLine />}
|
|
||||||
<span>
|
|
||||||
{releaseDate.toLocaleDateString("pl-PL", {
|
|
||||||
day: "numeric",
|
|
||||||
month: "long",
|
|
||||||
year: "numeric",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
{showDayCounter && (
|
|
||||||
<span className="text-xs text-gray-400">
|
|
||||||
{isReleased
|
|
||||||
? `${daysSinceRelease} dni od premiery`
|
|
||||||
: `${daysSinceRelease} dni do premiery`}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<div className="text-xs text-gray-400 mt-4">
|
|
||||||
<ReadMore text={overview} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export { AuroraLayout } from "./AuroraLayout";
|
|
||||||
export { MinimalLayout } from "./MinimalLayout";
|
|
||||||
export { ZeusLayout } from "./ZeusLayout";
|
|
||||||
export { DefaultLayout } from "./DefaultLayout";
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
|
"use client";
|
||||||
import { formatter } from "@/helpers/formater";
|
import { formatter } from "@/helpers/formater";
|
||||||
import { Movie } from "@/types/global";
|
import { Movie } from "@/types/global";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { FaCalendar, FaClock, FaStar } 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/globalStore";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
movie: Movie;
|
movie: Movie;
|
||||||
|
|
@ -15,6 +18,11 @@ export const MovieRow: FC<Props> = ({
|
||||||
isUpcoming = false,
|
isUpcoming = false,
|
||||||
compact = false,
|
compact = false,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { movies, addMovie, updateMovie } = useGlobalStore();
|
||||||
|
|
||||||
|
const dragControls = useAnimationControls();
|
||||||
|
const x = useMotionValue(0);
|
||||||
|
|
||||||
const daysSinceRelease = Math.abs(
|
const daysSinceRelease = Math.abs(
|
||||||
Math.floor(
|
Math.floor(
|
||||||
(new Date().getTime() - new Date(movie.release_date).getTime()) /
|
(new Date().getTime() - new Date(movie.release_date).getTime()) /
|
||||||
|
|
@ -22,60 +30,129 @@ export const MovieRow: FC<Props> = ({
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Check if movie is already in store.
|
||||||
|
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 (
|
return (
|
||||||
<Link
|
<div className="relative overflow-hidden rounded-xl">
|
||||||
href={`/film/${movie.id}`}
|
{/* Background actions */}
|
||||||
className="flex items-center gap-4 p-3 rounded-lg bg-gray-800/30 hover:bg-gray-800/50 transition-colors group"
|
<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">
|
||||||
<div className="relative w-12 h-16 rounded overflow-hidden flex-shrink-0">
|
<FaEye className="w-5 h-5 transition-colors text-green-500" />
|
||||||
<img
|
</div>
|
||||||
src={`https://image.tmdb.org/t/p/w154${movie.poster_path}`}
|
|
||||||
alt={movie.title}
|
<div className="absolute left-0 h-full w-24 bg-red-500/20 flex items-center justify-center cursor-pointer">
|
||||||
className="object-cover inset-0"
|
<FaHeart className="w-5 h-5 transition-colors text-red-500" />
|
||||||
sizes="48px"
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<motion.div
|
||||||
<h3 className="text-white font-medium text-sm truncate group-hover:text-blue-400 transition-colors">
|
drag="x"
|
||||||
{movie.title}
|
style={{ x }}
|
||||||
</h3>
|
animate={dragControls}
|
||||||
<div className="flex items-center gap-3 mt-1">
|
dragConstraints={{ left: -80, right: 80 }}
|
||||||
<div className="flex items-center gap-1 text-gray-400 text-xs">
|
dragElastic={0.01}
|
||||||
{isUpcoming ? (
|
dragMomentum={false}
|
||||||
<FaCalendar className="w-3 h-3" />
|
whileDrag={{ cursor: "grabbing" }}
|
||||||
) : (
|
onDragEnd={handleDragAction}
|
||||||
<FaClock className="w-3 h-3" />
|
className="relative z-10"
|
||||||
)}
|
>
|
||||||
<span>{formatter.formatDate(movie.release_date)}</span>
|
<Link
|
||||||
|
href={`/film/${movie.id}`}
|
||||||
|
draggable={false}
|
||||||
|
className="flex items-center gap-4 p-3 rounded-lg bg-gray-800 hover:bg-gray-800 transition-colors group"
|
||||||
|
>
|
||||||
|
<div className="relative w-12 h-16 rounded overflow-hidden flex-shrink-0">
|
||||||
|
<img
|
||||||
|
src={`https://image.tmdb.org/t/p/w154${movie.poster_path}`}
|
||||||
|
alt={movie.title}
|
||||||
|
className="object-cover inset-0"
|
||||||
|
sizes="48px"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!!movie.vote_average && (
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-1 text-yellow-400 text-xs">
|
<h3 className="text-white font-medium text-sm truncate group-hover:text-blue-400 transition-colors">
|
||||||
<FaStar className="w-3 h-3 fill-current" />
|
{movie.title}
|
||||||
<span>{movie.vote_average.toFixed(1)}</span>
|
</h3>
|
||||||
|
<div className="flex items-center gap-3 mt-1">
|
||||||
|
<div className="flex items-center gap-1 text-gray-400 text-xs">
|
||||||
|
{isUpcoming ? (
|
||||||
|
<FaCalendar className="w-3 h-3" />
|
||||||
|
) : (
|
||||||
|
<FaClock className="w-3 h-3" />
|
||||||
|
)}
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(isFavorite || movie.favorite) && (
|
||||||
|
<div
|
||||||
|
className="w-2 h-2 bg-red-500 rounded-full"
|
||||||
|
title="Ulubione"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(isWatched || movie.seen) && (
|
||||||
|
<div
|
||||||
|
className="w-2 h-2 bg-green-500 rounded-full"
|
||||||
|
title="Obejrzane"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!compact && (
|
||||||
|
<div
|
||||||
|
className={`text-xs px-2 py-1 rounded-full font-medium ${
|
||||||
|
isUpcoming
|
||||||
|
? "bg-blue-500/20 text-blue-400"
|
||||||
|
: "bg-green-500/20 text-green-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isUpcoming
|
||||||
|
? `za ${daysSinceRelease} dni`
|
||||||
|
: `od ${daysSinceRelease} dni`}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</Link>
|
||||||
{movie.favorite && (
|
</motion.div>
|
||||||
<div className="w-2 h-2 bg-red-500 rounded-full" title="Ulubione" />
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!compact && (
|
|
||||||
<div
|
|
||||||
className={`text-xs px-2 py-1 rounded-full font-medium ${
|
|
||||||
isUpcoming
|
|
||||||
? "bg-blue-500/20 text-blue-400"
|
|
||||||
: "bg-green-500/20 text-green-400"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isUpcoming
|
|
||||||
? `za ${daysSinceRelease} dni`
|
|
||||||
: `od ${daysSinceRelease} dni`}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,13 @@ export const Pagination: FC<Props> = ({
|
||||||
onPageChange,
|
onPageChange,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<ul className="flex justify-center gap-3 my-10">
|
<ul className="flex justify-center gap-3 my-10 items-center">
|
||||||
{currentPage > 1 && (
|
{currentPage > 1 && (
|
||||||
<li>
|
<li>
|
||||||
<Button
|
<Button
|
||||||
theme="glass"
|
size="icon"
|
||||||
|
theme="slate"
|
||||||
|
className="shadow-amber-400/20 hover:shadow-amber-400/40 "
|
||||||
aria-label="Previous page"
|
aria-label="Previous page"
|
||||||
onClick={() => onPageChange(currentPage - 1)}
|
onClick={() => onPageChange(currentPage - 1)}
|
||||||
>
|
>
|
||||||
|
|
@ -37,14 +39,16 @@ export const Pagination: FC<Props> = ({
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<li className="text-sm/8 font-medium tracking-widest leading-[2.8]">
|
<li className="text-sm/8 font-medium tracking-widest">
|
||||||
{currentPage}/{totalPages}
|
{currentPage}/{totalPages}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{currentPage < totalPages && (
|
{currentPage < totalPages && (
|
||||||
<li>
|
<li>
|
||||||
<Button
|
<Button
|
||||||
theme="glass"
|
theme="slate"
|
||||||
|
size="icon"
|
||||||
|
className="shadow-amber-400/20 hover:shadow-amber-400/40"
|
||||||
aria-label="Next page"
|
aria-label="Next page"
|
||||||
onClick={() => onPageChange(currentPage + 1)}
|
onClick={() => onPageChange(currentPage + 1)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { FC, useEffect, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
import { IoSearch } from "react-icons/io5";
|
import { IoSearch } from "react-icons/io5";
|
||||||
|
import { Button } from "../Button";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
@ -30,19 +31,21 @@ export const SearchInput: FC<Props> = ({
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="relative flex items-center gap-4">
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
name="search"
|
name="search"
|
||||||
value={value}
|
value={value}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
className={className}
|
className={
|
||||||
|
"w-full bg-gradient-to-br from-slate-800/95 to-slate-900/90 border border-white/20 rounded-full h-12 px-6 shadow-lg shadow-purple-500/15 outline-none focus:shadow-purple-500/20"
|
||||||
|
}
|
||||||
onChange={(e) => setValue(e.target.value)}
|
onChange={(e) => setValue(e.target.value)}
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
/>
|
/>
|
||||||
<button type="submit" className="absolute right-0 top-1 mt-3 mr-4">
|
<Button theme="slate" size="icon" className="shrink-0">
|
||||||
<IoSearch />
|
<IoSearch />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ export const Gallery: FC<Props> = ({
|
||||||
{limit < currentImages.length && (
|
{limit < currentImages.length && (
|
||||||
<div className="flex justify-center mt-6">
|
<div className="flex justify-center mt-6">
|
||||||
<Button
|
<Button
|
||||||
theme="teal"
|
theme="emeraldTeal"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => setLimit(currentImages.length)}
|
onClick={() => setLimit(currentImages.length)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -218,7 +218,7 @@ export const HeroMovie: FC<Props> = ({ movieDetails }) => {
|
||||||
{isInStore ? "Usuń z listy" : "Dodaj do listy"}
|
{isInStore ? "Usuń z listy" : "Dodaj do listy"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
theme={isFavorite ? "rose" : "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"
|
||||||
|
|
@ -234,7 +234,7 @@ export const HeroMovie: FC<Props> = ({ movieDetails }) => {
|
||||||
: "Dodaj do ulubionych"}
|
: "Dodaj do ulubionych"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
theme={isSeen ? "emerald" : "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" : ""
|
||||||
}`}
|
}`}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -63,7 +67,7 @@ export const MovieList: FC<Props> = ({
|
||||||
sortType
|
sortType
|
||||||
);
|
);
|
||||||
|
|
||||||
const [loaded, setLoaded] = useState(loadMore ? 8 : movies.length);
|
const [loaded, setLoaded] = useState(8);
|
||||||
const [parent] = useAutoAnimate();
|
const [parent] = useAutoAnimate();
|
||||||
|
|
||||||
const filteredMovies = movies.filter((movie) => {
|
const filteredMovies = movies.filter((movie) => {
|
||||||
|
|
@ -101,7 +105,7 @@ export const MovieList: FC<Props> = ({
|
||||||
if (sortDirection === "desc") {
|
if (sortDirection === "desc") {
|
||||||
sortedMovies = sortedMovies.reverse();
|
sortedMovies = sortedMovies.reverse();
|
||||||
}
|
}
|
||||||
sortedMovies = sortedMovies.slice(0, loaded);
|
sortedMovies = sortedMovies.slice(0, loadMore ? loaded : movies.length);
|
||||||
|
|
||||||
const handleFilter = (key?: keyof typeof filter) => {
|
const handleFilter = (key?: keyof typeof filter) => {
|
||||||
setFilter({
|
setFilter({
|
||||||
|
|
@ -152,19 +156,31 @@ export const MovieList: FC<Props> = ({
|
||||||
Ulubione ({movies.filter((movie) => movie.favorite).length})
|
Ulubione ({movies.filter((movie) => movie.favorite).length})
|
||||||
</Label>
|
</Label>
|
||||||
<Label
|
<Label
|
||||||
active={filter.seen !== undefined}
|
active={
|
||||||
|
filter.seen !== undefined && filter.released === undefined
|
||||||
|
}
|
||||||
onClick={() => handleFilter("seen")}
|
onClick={() => handleFilter("seen")}
|
||||||
>
|
>
|
||||||
Obejrzane ({movies.filter((movie) => movie.seen).length})
|
Obejrzane ({movies.filter((movie) => movie.seen).length})
|
||||||
</Label>
|
</Label>
|
||||||
<Label
|
<Label
|
||||||
active={filter.released !== undefined}
|
active={
|
||||||
onClick={() => handleFilter("released")}
|
filter.released !== undefined && filter.seen !== undefined
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
setFilter({
|
||||||
|
seen: 0,
|
||||||
|
released: 1,
|
||||||
|
favorites: filterFavoritesInitial,
|
||||||
|
upcoming: filterUpcomingInitial,
|
||||||
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
W kinach (
|
Do obejrzenia (
|
||||||
{
|
{
|
||||||
movies.filter(
|
movies.filter(
|
||||||
(movie) => new Date(movie.release_date) < new Date()
|
(movie) =>
|
||||||
|
new Date(movie.release_date) < new Date() && !movie.seen
|
||||||
).length
|
).length
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -184,6 +200,17 @@ export const MovieList: FC<Props> = ({
|
||||||
|
|
||||||
{showSorting && (
|
{showSorting && (
|
||||||
<div className="flex items-center gap-3 ml-auto">
|
<div className="flex items-center gap-3 ml-auto">
|
||||||
|
{!overrideDisplayType && (
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
theme="slate"
|
||||||
|
onClick={() =>
|
||||||
|
setDisplayType(displayType === "grid" ? "list" : "grid")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FaList />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Dropdown
|
<Dropdown
|
||||||
items={[
|
items={[
|
||||||
{ label: "Tytuł", value: "title" },
|
{ label: "Tytuł", value: "title" },
|
||||||
|
|
@ -193,15 +220,6 @@ export const MovieList: FC<Props> = ({
|
||||||
defaultValue={sort}
|
defaultValue={sort}
|
||||||
callback={(value) => setSort(value as "title")}
|
callback={(value) => setSort(value as "title")}
|
||||||
/>
|
/>
|
||||||
<Button
|
|
||||||
theme="glass"
|
|
||||||
size="icon"
|
|
||||||
onClick={() =>
|
|
||||||
setDisplay(display === "grid" ? "list" : "grid")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FaList />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -215,8 +233,8 @@ 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} {...movie} />
|
||||||
) : (
|
) : (
|
||||||
<MovieRow key={movie.id} movie={movie} compact />
|
<MovieRow key={movie.id} movie={movie} compact />
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
'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 { useRouter } from 'next/navigation';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Movie } from '@/types/global';
|
||||||
|
|
||||||
|
type StoreFilter = 'all' | 'not_seen' | 'released' | 'favorites' | 'to_watch';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
heading?: string;
|
||||||
|
storeFilter?: StoreFilter;
|
||||||
|
colors?: keyof typeof colorsMap;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RandomMovie: FC<Props> = ({
|
||||||
|
heading = 'Losowy film',
|
||||||
|
storeFilter = 'not_seen',
|
||||||
|
colors = 'purple',
|
||||||
|
className = '',
|
||||||
|
}) => {
|
||||||
|
const { movies } = useGlobalStore();
|
||||||
|
const [selectedMovie, setSelectedMovie] = useState<Movie | null>(null);
|
||||||
|
|
||||||
|
// Filter movies based on the selected store filter.
|
||||||
|
const filteredMovies = useMemo(() => {
|
||||||
|
const today = new Date();
|
||||||
|
|
||||||
|
return movies.filter(movie => {
|
||||||
|
switch (storeFilter) {
|
||||||
|
case 'not_seen':
|
||||||
|
return !movie.seen;
|
||||||
|
case 'released':
|
||||||
|
return new Date(movie.release_date) < today;
|
||||||
|
case 'favorites':
|
||||||
|
return movie.favorite;
|
||||||
|
case 'to_watch':
|
||||||
|
return !movie.seen && new Date(movie.release_date) < today;
|
||||||
|
case 'all':
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [movies, storeFilter]);
|
||||||
|
|
||||||
|
const handleRandomize = () => {
|
||||||
|
if (filteredMovies.length === 0) return;
|
||||||
|
|
||||||
|
const randomIndex = Math.floor(Math.random() * filteredMovies.length);
|
||||||
|
const randomMovie = filteredMovies[randomIndex];
|
||||||
|
|
||||||
|
setSelectedMovie(randomMovie);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (filteredMovies.length === 0) {
|
||||||
|
return (
|
||||||
|
<section className={`blocks ${className}`}>
|
||||||
|
<div className="container">
|
||||||
|
{heading && (
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<div className={`p-2 rounded-lg ${colorsMap[colors]}`}>
|
||||||
|
<FaDice className="text-white" />
|
||||||
|
</div>
|
||||||
|
<h2
|
||||||
|
className={`text-3xl font-bold ${colorsMap[colors]} bg-clip-text text-transparent`}
|
||||||
|
>
|
||||||
|
{heading}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<p className="text-text/60 text-lg">Brak filmów w kategorii</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={`blocks ${className}`}>
|
||||||
|
<div className="container">
|
||||||
|
{heading && (
|
||||||
|
<div className="flex justify-center mb-6">
|
||||||
|
<h2
|
||||||
|
className={`text-3xl font-bold ${colorsMap[colors]} bg-clip-text text-transparent`}
|
||||||
|
>
|
||||||
|
{heading}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="text-center mb-4">
|
||||||
|
<p className="text-text/70 text-sm">
|
||||||
|
Dostępnych {filteredMovies.length} filmów
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
theme="secondary"
|
||||||
|
onClick={handleRandomize}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<FaDice />
|
||||||
|
Losuj film
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedMovie && (
|
||||||
|
<div className="text-center mt-4">
|
||||||
|
<h3 className="text-2xl font-bold">
|
||||||
|
<Link href={`/film/${selectedMovie.id}`}>
|
||||||
|
{selectedMovie.title}
|
||||||
|
</Link>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const colorsMap = {
|
||||||
|
white: 'bg-gradient-to-r from-white to-gray-300',
|
||||||
|
yellow: 'bg-gradient-to-r from-yellow-400 to-orange-400',
|
||||||
|
blue: 'bg-gradient-to-r from-blue-400 to-purple-400',
|
||||||
|
green: 'bg-gradient-to-r from-green-400 to-teal-400',
|
||||||
|
red: 'bg-gradient-to-r from-red-400 to-pink-400',
|
||||||
|
purple: 'bg-gradient-to-r from-purple-400 to-pink-400',
|
||||||
|
orange: 'bg-gradient-to-r from-orange-400 to-yellow-400',
|
||||||
|
pink: 'bg-gradient-to-r from-pink-400 to-purple-400',
|
||||||
|
teal: 'bg-gradient-to-r from-teal-400 to-green-400',
|
||||||
|
gray: 'bg-gradient-to-r from-gray-400 to-gray-400',
|
||||||
|
};
|
||||||
|
|
@ -29,7 +29,6 @@ export const RecommendedMovies: FC<Props> = ({ movies }) => {
|
||||||
.map((movie) => (
|
.map((movie) => (
|
||||||
<MovieCard
|
<MovieCard
|
||||||
key={movie.id}
|
key={movie.id}
|
||||||
layout="aurora"
|
|
||||||
id={movie.id}
|
id={movie.id}
|
||||||
title={movie.title}
|
title={movie.title}
|
||||||
overview={movie.overview}
|
overview={movie.overview}
|
||||||
|
|
|
||||||
|
|
@ -13,15 +13,14 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SearchList: FC<Props> = ({ query }) => {
|
export const SearchList: FC<Props> = ({ query }) => {
|
||||||
const [response, setResponse] = useState<SearchResult | null>(null);
|
const [response, setResponse] = useState<SearchResult>({
|
||||||
const {
|
results: [],
|
||||||
results,
|
total_results: 0,
|
||||||
total_results = 0,
|
total_pages: 0,
|
||||||
total_pages = 0,
|
page: 1,
|
||||||
page = 1,
|
});
|
||||||
} = response ?? {};
|
const { results, total_results, total_pages, page } = response;
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const handleSearch = async (query: string, page: number) => {
|
const handleSearch = async (query: string, page: number) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
@ -44,6 +43,13 @@ export const SearchList: FC<Props> = ({ query }) => {
|
||||||
handleSearch(query, page);
|
handleSearch(query, page);
|
||||||
}, [query]);
|
}, [query]);
|
||||||
|
|
||||||
|
const movies = results.map((m) => ({
|
||||||
|
...m,
|
||||||
|
favorite: false,
|
||||||
|
seen: false,
|
||||||
|
genre_ids: JSON.stringify(m.genre_ids),
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="mb-4 md:mb-10">
|
<section className="mb-4 md:mb-10">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
|
|
@ -53,26 +59,25 @@ export const SearchList: FC<Props> = ({ query }) => {
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="absolute -inset-10 flex pt-60 justify-center bg-gradient-to-t from-slate-900/90 via-slate-800/50 to-transparent z-10">
|
<div className="absolute flex inset-0 items-center justify-center z-10 backdrop-blur">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<MovieList
|
<MovieList
|
||||||
overrideMovies={results?.map((m) => ({
|
showFilters={false}
|
||||||
...m,
|
overrideDisplayType="grid"
|
||||||
favorite: false,
|
overrideMovies={movies}
|
||||||
seen: false,
|
|
||||||
genre_ids: JSON.stringify(m.genre_ids),
|
|
||||||
}))}
|
|
||||||
fluid
|
fluid
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Pagination
|
{total_pages > 1 && (
|
||||||
totalPages={total_pages}
|
<Pagination
|
||||||
currentPage={page}
|
totalPages={total_pages}
|
||||||
onPageChange={handlePageChange}
|
currentPage={page}
|
||||||
/>
|
onPageChange={handlePageChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,6 @@ export const SimilarMovies: FC<Props> = ({ movies }) => {
|
||||||
{currentMovies.map((movie) => (
|
{currentMovies.map((movie) => (
|
||||||
<MovieCard
|
<MovieCard
|
||||||
key={movie.id}
|
key={movie.id}
|
||||||
layout="aurora"
|
|
||||||
id={movie.id}
|
id={movie.id}
|
||||||
title={movie.title}
|
title={movie.title}
|
||||||
overview={movie.overview}
|
overview={movie.overview}
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,17 @@ export const TrackedMovies: FC<Props> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const upcoming = movies.filter(
|
const upcoming = movies.filter((movie) => {
|
||||||
(movie) => new Date(movie.release_date) > today
|
const daysSinceRelease = Math.abs(
|
||||||
);
|
Math.floor(
|
||||||
|
(new Date().getTime() - new Date(movie.release_date).getTime()) /
|
||||||
|
(1000 * 60 * 60 * 24)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
new Date(movie.release_date) > today && daysSinceRelease <= daysLimit
|
||||||
|
);
|
||||||
|
});
|
||||||
const inCinema = movies.filter((movie) => {
|
const inCinema = movies.filter((movie) => {
|
||||||
const daysSinceRelease = Math.floor(
|
const daysSinceRelease = Math.floor(
|
||||||
(new Date().getTime() - new Date(movie.release_date).getTime()) /
|
(new Date().getTime() - new Date(movie.release_date).getTime()) /
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,9 @@ export const Search: FC<Props> = ({ onClose }) => {
|
||||||
<div className="fixed inset-0 z-[60] overflow-y-auto">
|
<div className="fixed inset-0 z-[60] overflow-y-auto">
|
||||||
{/* Close button */}
|
{/* Close button */}
|
||||||
<Button
|
<Button
|
||||||
theme="glass"
|
theme="slate"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="absolute top-6 right-6 z-10 group hover:!bg-red-500/50"
|
className="absolute top-6 right-6 z-10 group shadow-lg shadow-red-500/20 hover:shadow-red-500/40"
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
>
|
>
|
||||||
<IoClose className="text-white transition-transform duration-300 group-hover:rotate-90" />
|
<IoClose className="text-white transition-transform duration-300 group-hover:rotate-90" />
|
||||||
|
|
@ -71,15 +71,11 @@ export const Search: FC<Props> = ({ onClose }) => {
|
||||||
|
|
||||||
{/* Enhanced Search Input */}
|
{/* Enhanced Search Input */}
|
||||||
<div className="relative max-w-2xl mx-auto">
|
<div className="relative max-w-2xl mx-auto">
|
||||||
<div className="absolute inset-0 bg-gradient-to-r from-purple-500/30 to-cyan-500/30 rounded-2xl"></div>
|
<SearchInput
|
||||||
<div className="relative bg-gradient-to-br from-white/15 via-white/8 to-white/12 border border-white/20 rounded-2xl p-2 shadow-2xl shadow-purple-500/10">
|
onChange={handleSearch}
|
||||||
<SearchInput
|
placeholder="Wpisz tytuł filmu..."
|
||||||
className="w-full px-3 bg-transparent border-none text-lg lg:text-xl placeholder-gray-400 text-white focus:outline-none"
|
autoFocus={true}
|
||||||
onChange={handleSearch}
|
/>
|
||||||
placeholder="Wpisz tytuł filmu..."
|
|
||||||
autoFocus={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,20 @@
|
||||||
"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";
|
||||||
|
import { Button } from "@/components/atoms/Button";
|
||||||
|
|
||||||
const navigationItems = [
|
const navigationItems = [
|
||||||
|
{
|
||||||
|
label: "Odkrywaj",
|
||||||
|
href: "/odkrywaj",
|
||||||
|
icon: HiSparkles,
|
||||||
|
emoji: "🎬",
|
||||||
|
color: "from-purple-500 to-pink-600",
|
||||||
|
description: "Znajdź nowe filmy",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Strona Główna",
|
label: "Strona Główna",
|
||||||
href: "/",
|
href: "/",
|
||||||
|
|
@ -14,14 +23,6 @@ const navigationItems = [
|
||||||
color: "from-blue-500 to-purple-600",
|
color: "from-blue-500 to-purple-600",
|
||||||
description: "Twoja lista filmów",
|
description: "Twoja lista filmów",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "Odkrywaj",
|
|
||||||
href: "/odkrywaj",
|
|
||||||
icon: HiViewGrid,
|
|
||||||
emoji: "🎬",
|
|
||||||
color: "from-purple-500 to-pink-600",
|
|
||||||
description: "Znajdź nowe filmy",
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const Navbar = () => {
|
export const Navbar = () => {
|
||||||
|
|
@ -31,82 +32,36 @@ export const Navbar = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Elegant Floating Navigation */}
|
{/* Elegant Floating Navigation */}
|
||||||
<nav className="fixed bottom-0 left-0 right-0 z-50 pointer-events-none bg-black/90 lg:bg-transparent">
|
<nav className="fixed bottom-0 left-0 right-0 z-50 bg-gradient-to-t from-black to-transparent">
|
||||||
<div className="relative h-24 flex items-center justify-between px-6">
|
<div className="relative flex items-center justify-center px-6 py-4 gap-3">
|
||||||
{/* Brand Name - Floating Left */}
|
{/* Desktop Navigation Orbs */}
|
||||||
<div className="pointer-events-auto">
|
{navigationItems.map((item, index) => {
|
||||||
<Link href="/" className="group flex items-center gap-3">
|
const isActive = pathname === item.href;
|
||||||
<div className="relative">
|
return (
|
||||||
<h1 className="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-white via-purple-200 to-cyan-200 group-hover:from-purple-300 group-hover:to-cyan-300 transition-all duration-500">
|
<Link
|
||||||
MovieBox
|
key={item.href}
|
||||||
</h1>
|
href={item.href}
|
||||||
</div>
|
className="relative group cursor-pointer"
|
||||||
</Link>
|
>
|
||||||
</div>
|
{/* Main orb */}
|
||||||
|
<Button theme={isActive ? "secondary" : "slate"} size="icon">
|
||||||
{/* Navigation & Action Orbs - Right Side */}
|
{/* Icon */}
|
||||||
<div className="flex items-center gap-3 pointer-events-auto">
|
<item.icon
|
||||||
{/* Desktop Navigation Orbs */}
|
className={`transition-colors duration-300
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{navigationItems.map((item, index) => {
|
|
||||||
const isActive = pathname === item.href;
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={item.href}
|
|
||||||
href={item.href}
|
|
||||||
className="relative group cursor-pointer"
|
|
||||||
>
|
|
||||||
{/* Main orb */}
|
|
||||||
<div
|
|
||||||
className={`relative w-12 h-12 rounded-full border border-white/20 shadow-lg transition-all duration-300 hover:scale-105
|
|
||||||
${
|
|
||||||
isActive
|
|
||||||
? "bg-gradient-to-br from-purple-500/80 to-cyan-500/80 shadow-purple-500/40"
|
|
||||||
: "bg-gradient-to-br from-slate-800/95 to-slate-900/95 shadow-slate-500/20"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{/* Icon */}
|
|
||||||
<div className="w-full h-full flex items-center justify-center">
|
|
||||||
<item.icon
|
|
||||||
className={`w-5 h-5 transition-colors duration-300
|
|
||||||
${
|
${
|
||||||
isActive
|
isActive
|
||||||
? "text-white"
|
? "text-white"
|
||||||
: "text-gray-300 group-hover:text-white"
|
: "text-gray-300 group-hover:text-white"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Button>
|
||||||
</div>
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
{/* Tooltip */}
|
<Button theme="slate" size="icon" onClick={() => setSearchOpen(true)}>
|
||||||
<div className="absolute -bottom-12 left-1/2 transform -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none">
|
<HiSearch className="text-cyan-400 mx-auto" />
|
||||||
<div className="bg-black/90 text-white text-xs px-3 py-1 rounded-lg whitespace-nowrap border border-white/10">
|
</Button>
|
||||||
{item.label}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Search Orb */}
|
|
||||||
<div className="relative group">
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-r from-cyan-500/30 to-purple-500/30 rounded-full transition-all duration-300"></div>
|
|
||||||
<button
|
|
||||||
onClick={() => setSearchOpen(true)}
|
|
||||||
className="relative w-12 h-12 rounded-full bg-gradient-to-br from-slate-800/95 to-slate-900/95 border border-white/20 shadow-lg shadow-cyan-500/20 hover:shadow-cyan-500/40 transition-all duration-300 hover:scale-105"
|
|
||||||
>
|
|
||||||
<HiSearch className="w-5 h-5 text-cyan-400 mx-auto" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Tooltip */}
|
|
||||||
<div className="absolute -bottom-12 left-1/2 transform -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none">
|
|
||||||
<div className="bg-black/90 text-white text-xs px-3 py-1 rounded-lg whitespace-nowrap border border-white/10">
|
|
||||||
Szukaj filmów
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
|
||||||
73
todo.md
73
todo.md
|
|
@ -1,36 +1,51 @@
|
||||||
#
|
UI/UX Improvements
|
||||||
|
Dark/Light Mode Toggle - Obecnie tylko ciemny motyw
|
||||||
|
Responsywny design na urządzenia mobilne - Niektóre komponenty mogą wymagać poprawy
|
||||||
|
Loading states - Dodać skeletony zamiast spinnerów
|
||||||
|
Infinite scroll - Zamiast paginacji dla lepszego UX
|
||||||
|
Gesture support - Swipe na mobilnych dla akcji (dodaj/usuń film)
|
||||||
|
|
||||||
## ✅ `TODO.md` – Etapy rozwoju aplikacji
|
Zarządzanie filmami
|
||||||
|
Własne notatki do filmów - Pole w bazie danych już wspomniane w README
|
||||||
|
Tagi/kategorie użytkownika - Własne etykiety
|
||||||
|
Oceny użytkownika - Osobne od TMDB
|
||||||
|
Data obejrzenia - Kiedy użytkownik obejrzał film
|
||||||
|
Lista "Do obejrzenia" - Oddzielna od "Obejrzane"
|
||||||
|
Planowanie seansów - Kalendarz z datami
|
||||||
|
Eksport/import listy - JSON/CSV backup
|
||||||
|
|
||||||
```md
|
Funkcje społecznościowe
|
||||||
# TODO – MovieBox
|
Udostępnianie list - Link do publicznej listy
|
||||||
|
Rekomendacje na podstawie gustu - ML/AI sugestie
|
||||||
|
Porównanie list z znajomymi - Wspólne filmy
|
||||||
|
|
||||||
## 🔧 Faza 1 – MVP (funkcjonalna wersja lokalna)
|
Dodatkowe dane i integracje
|
||||||
|
Informacje o aktorach - Rozszerzone profile (już częściowo jest)
|
||||||
|
Gdzie obejrzeć - Streaming platforms API
|
||||||
|
Zwiastuny - YouTube API integration
|
||||||
|
Recenzje użytkowników - Własne mini-forum
|
||||||
|
Galeria zdjęć z filmu - Więcej materiałów wizualnych
|
||||||
|
|
||||||
- [ ] Integracja z TMDB API (wyszukiwanie filmów)
|
Performance i techniczne
|
||||||
- [ ] Utworzenie bazy danych (SQLite + Drizzle)
|
PWA - Offline support, push notifications o premierach
|
||||||
- [ ] Modele: Movie, WatchlistEntry
|
Lepsze caching - Redis/SWR optimizations
|
||||||
- [ ] Dodanie filmu do watchlisty (z podglądem szczegółów)
|
Lazy loading - Obrazy i komponenty
|
||||||
- [ ] Lista “Do obejrzenia” i “Obejrzane”
|
Search indexing - Full-text search w bazie
|
||||||
- [ ] Możliwość dodania tagu lub notatki do filmu
|
API rate limiting - Lepsze zarządzanie requestami do TMDB
|
||||||
- [ ] UI (Tailwind + ShadCN) – responsywna siatka filmów
|
|
||||||
|
|
||||||
## 🌐 Faza 2 – Rozszerzenie
|
Statystyki i analytics
|
||||||
|
Dashboard statystyk - Filmy obejrzane/miesiąc, ulubione gatunki
|
||||||
|
Streak tracking - Dni z rzędu oglądania filmów
|
||||||
|
Cele filmowe - X filmów do obejrzenia w roku
|
||||||
|
Porównanie z poprzednimi latami - Trendy
|
||||||
|
|
||||||
- [ ] Podgląd dat premier z TMDB
|
Powiadomienia
|
||||||
- [ ] Filtrowanie według daty premiery
|
Email notifications - O premierach z listy
|
||||||
- [ ] Sortowanie / filtrowanie po tagach/statusie
|
Push notifications - PWA alerts
|
||||||
|
Reminder system - Przypomnienia o filmach do obejrzenia
|
||||||
|
|
||||||
## 🔐 Faza 3 – Rozszerzenia prywatne
|
Baza danych i backend
|
||||||
|
Migracja na PostgreSQL - Jak wspomniano w README
|
||||||
- [ ] Dodanie Auth.js (logowanie)
|
User authentication - Currently brak systemu użytkowników
|
||||||
- [ ] Migracja bazy do PostgreSQL
|
API endpoints - Własne REST API
|
||||||
- [ ] Eksport listy filmów (np. JSON)
|
Backup system - Automatyczne kopie zapasowe
|
||||||
- [ ] Backup na GitHub (np. GitHub Actions)
|
|
||||||
|
|
||||||
## 💡 Pomysły na później
|
|
||||||
|
|
||||||
- [ ] System rekomendacji (podobne filmy)
|
|
||||||
- [ ] Powiadomienia o premierach
|
|
||||||
- [ ] Integracja z Letterboxd
|
|
||||||
```
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue