Compare commits

..

10 Commits

Author SHA1 Message Date
Norbert Maciaszek 9231e0642c refactor: migrate from drizzle to PocketBase 2025-10-27 20:29:49 +01:00
Norbert Maciaszek 68fb45d6ef feat: add RandomMovie component for enhanced user experience; implement movie filtering and random selection functionality, and update page layout to include new component 2025-10-25 23:02:25 +02:00
Norbert Maciaszek af4689d726 refactor: update Pagination and MovieList components for improved styling and functionality; adjust Button themes, enhance movie loading logic, and ensure consistent display of search results 2025-08-25 23:00:53 +02:00
Norbert Maciaszek 01c80758bf fix: update Button themes in Gallery and HeroMovie components for consistency and improved styling 2025-08-25 22:44:29 +02:00
Norbert Maciaszek 3ed7b14f1b feat: update Button component styles and themes; introduce new slate theme for consistent UI across Dropdown, SearchInput, and Navbar components, and enhance MovieList display type toggle functionality 2025-08-25 22:43:12 +02:00
Norbert Maciaszek 9079a52778 feat: enhance MovieRow component with drag-and-drop functionality for marking movies as watched or favorite; integrate framer-motion for improved animations and user interaction 2025-08-25 22:05:59 +02:00
Norbert Maciaszek cb0962f184 feat: add motion and framer-motion packages to enhance animation capabilities in the project 2025-08-24 22:57:31 +02:00
Norbert Maciaszek 137c620a48 refactor: remove layout prop from MovieCard in RecommendedMovies and SimilarMovies components for consistency 2025-08-22 19:42:27 +02:00
Norbert Maciaszek 452be796f0 feat: update layout and MovieCard components to enhance styling and functionality; replace layout system with a unified MovieCard design, adjust display types in Odkrywaj and GenrePage, and improve pagination button styles 2025-08-22 19:30:17 +02:00
Norbert Maciaszek d67e34c75c feat: enhance GlobalStoreProvider and MovieList components with display type management and loading spinner; integrate local storage for display preferences and improve user experience during initial render 2025-08-22 19:17:48 +02:00
43 changed files with 934 additions and 2068 deletions

9
.prettierrc Normal file
View File

@ -0,0 +1,9 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"arrowParens": "avoid"
}

View File

@ -1,10 +0,0 @@
import { defineConfig } from "drizzle-kit";
export default defineConfig({
out: "./drizzle",
schema: "./src/lib/db/schema.ts",
dialect: "sqlite",
dbCredentials: {
url: process.env.DB_FILE_NAME!,
},
});

View File

@ -1,11 +0,0 @@
CREATE TABLE `movies` (
`id` integer PRIMARY KEY NOT NULL,
`title` text NOT NULL,
`overview` text NOT NULL,
`popularity` real NOT NULL,
`release_date` text NOT NULL,
`poster_path` text NOT NULL,
`seen` integer DEFAULT 0 NOT NULL,
`favorite` integer DEFAULT 0 NOT NULL,
`notes` text DEFAULT '' NOT NULL
);

View File

@ -1,42 +0,0 @@
PRAGMA foreign_keys=OFF;--> statement-breakpoint
CREATE TABLE `__new_movies` (
`id` integer PRIMARY KEY NOT NULL,
`title` text NOT NULL,
`adult` integer NOT NULL,
`backdrop_path` text NOT NULL,
`genre_ids` text NOT NULL,
`original_language` text NOT NULL,
`original_title` text NOT NULL,
`overview` text NOT NULL,
`popularity` real NOT NULL,
`poster_path` text NOT NULL,
`release_date` text NOT NULL,
`video` integer NOT NULL,
`vote_average` real NOT NULL,
`vote_count` integer NOT NULL,
`seen` integer DEFAULT false,
`favorite` integer DEFAULT false
);
--> statement-breakpoint
INSERT INTO `__new_movies`("id", "title", "overview", "popularity", "poster_path", "release_date", "seen", "favorite", "adult", "backdrop_path", "genre_ids", "original_language", "original_title", "video", "vote_average", "vote_count")
SELECT
"id",
"title",
"overview",
"popularity",
"poster_path",
"release_date",
"seen",
"favorite",
0 as "adult", -- wartość domyślna
'' as "backdrop_path", -- wartość domyślna
'[]' as "genre_ids", -- wartość domyślna (pusta tablica JSON)
'pl-PL' as "original_language", -- wartość domyślna
"title" as "original_title", -- kopiuj z title
0 as "video", -- wartość domyślna
0.0 as "vote_average", -- wartość domyślna
0 as "vote_count" -- wartość domyślna
FROM `movies`;--> statement-breakpoint
DROP TABLE `movies`;--> statement-breakpoint
ALTER TABLE `__new_movies` RENAME TO `movies`;--> statement-breakpoint
PRAGMA foreign_keys=ON;

View File

@ -1,94 +0,0 @@
{
"version": "6",
"dialect": "sqlite",
"id": "19a2bad6-49be-485d-ac5e-291bd3d664ad",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"movies": {
"name": "movies",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"overview": {
"name": "overview",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"popularity": {
"name": "popularity",
"type": "real",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"release_date": {
"name": "release_date",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"poster_path": {
"name": "poster_path",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"seen": {
"name": "seen",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"favorite": {
"name": "favorite",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"notes": {
"name": "notes",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "''"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -1,142 +0,0 @@
{
"version": "6",
"dialect": "sqlite",
"id": "c3b4d292-f58b-4df8-844c-6e534034c832",
"prevId": "19a2bad6-49be-485d-ac5e-291bd3d664ad",
"tables": {
"movies": {
"name": "movies",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"adult": {
"name": "adult",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"backdrop_path": {
"name": "backdrop_path",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"genre_ids": {
"name": "genre_ids",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"original_language": {
"name": "original_language",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"original_title": {
"name": "original_title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"overview": {
"name": "overview",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"popularity": {
"name": "popularity",
"type": "real",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"poster_path": {
"name": "poster_path",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"release_date": {
"name": "release_date",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"video": {
"name": "video",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"vote_average": {
"name": "vote_average",
"type": "real",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"vote_count": {
"name": "vote_count",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"seen": {
"name": "seen",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": false
},
"favorite": {
"name": "favorite",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -1,20 +0,0 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1754676538678,
"tag": "0000_breezy_lester",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1754948246595,
"tag": "0001_elite_odin",
"breakpoints": true
}
]
}

724
package-lock.json generated
View File

@ -11,9 +11,10 @@
"@formkit/auto-animate": "^0.8.2", "@formkit/auto-animate": "^0.8.2",
"@libsql/client": "^0.15.10", "@libsql/client": "^0.15.10",
"dotenv": "^17.2.1", "dotenv": "^17.2.1",
"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",
"pocketbase": "^0.26.3",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
@ -24,7 +25,6 @@
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"drizzle-kit": "^0.31.4",
"tailwindcss": "^4", "tailwindcss": "^4",
"tsx": "^4.20.3", "tsx": "^4.20.3",
"typescript": "^5" "typescript": "^5"
@ -57,13 +57,6 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@drizzle-team/brocli": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
"integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@emnapi/runtime": { "node_modules/@emnapi/runtime": {
"version": "1.4.5", "version": "1.4.5",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz",
@ -74,442 +67,6 @@
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/@esbuild-kit/core-utils": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz",
"integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==",
"deprecated": "Merged into tsx: https://tsx.is",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.18.20",
"source-map-support": "^0.5.21"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/esbuild": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/android-arm": "0.18.20",
"@esbuild/android-arm64": "0.18.20",
"@esbuild/android-x64": "0.18.20",
"@esbuild/darwin-arm64": "0.18.20",
"@esbuild/darwin-x64": "0.18.20",
"@esbuild/freebsd-arm64": "0.18.20",
"@esbuild/freebsd-x64": "0.18.20",
"@esbuild/linux-arm": "0.18.20",
"@esbuild/linux-arm64": "0.18.20",
"@esbuild/linux-ia32": "0.18.20",
"@esbuild/linux-loong64": "0.18.20",
"@esbuild/linux-mips64el": "0.18.20",
"@esbuild/linux-ppc64": "0.18.20",
"@esbuild/linux-riscv64": "0.18.20",
"@esbuild/linux-s390x": "0.18.20",
"@esbuild/linux-x64": "0.18.20",
"@esbuild/netbsd-x64": "0.18.20",
"@esbuild/openbsd-x64": "0.18.20",
"@esbuild/sunos-x64": "0.18.20",
"@esbuild/win32-arm64": "0.18.20",
"@esbuild/win32-ia32": "0.18.20",
"@esbuild/win32-x64": "0.18.20"
}
},
"node_modules/@esbuild-kit/esm-loader": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz",
"integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==",
"deprecated": "Merged into tsx: https://tsx.is",
"dev": true,
"license": "MIT",
"dependencies": {
"@esbuild-kit/core-utils": "^3.3.2",
"get-tsconfig": "^4.7.0"
}
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.25.8", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
@ -2061,13 +1618,6 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"license": "MIT"
},
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001731", "version": "1.0.30001731",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz",
@ -2165,24 +1715,6 @@
"node": ">= 12" "node": ">= 12"
} }
}, },
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/dequal": { "node_modules/dequal": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@ -2214,147 +1746,6 @@
"url": "https://dotenvx.com" "url": "https://dotenvx.com"
} }
}, },
"node_modules/drizzle-kit": {
"version": "0.31.4",
"resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.4.tgz",
"integrity": "sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@drizzle-team/brocli": "^0.10.2",
"@esbuild-kit/esm-loader": "^2.5.5",
"esbuild": "^0.25.4",
"esbuild-register": "^3.5.0"
},
"bin": {
"drizzle-kit": "bin.cjs"
}
},
"node_modules/drizzle-orm": {
"version": "0.44.4",
"resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.4.tgz",
"integrity": "sha512-ZyzKFpTC/Ut3fIqc2c0dPZ6nhchQXriTsqTNs4ayRgl6sZcFlMs9QZKPSHXK4bdOf41GHGWf+FrpcDDYwW+W6Q==",
"license": "Apache-2.0",
"peerDependencies": {
"@aws-sdk/client-rds-data": ">=3",
"@cloudflare/workers-types": ">=4",
"@electric-sql/pglite": ">=0.2.0",
"@libsql/client": ">=0.10.0",
"@libsql/client-wasm": ">=0.10.0",
"@neondatabase/serverless": ">=0.10.0",
"@op-engineering/op-sqlite": ">=2",
"@opentelemetry/api": "^1.4.1",
"@planetscale/database": ">=1.13",
"@prisma/client": "*",
"@tidbcloud/serverless": "*",
"@types/better-sqlite3": "*",
"@types/pg": "*",
"@types/sql.js": "*",
"@upstash/redis": ">=1.34.7",
"@vercel/postgres": ">=0.8.0",
"@xata.io/client": "*",
"better-sqlite3": ">=7",
"bun-types": "*",
"expo-sqlite": ">=14.0.0",
"gel": ">=2",
"knex": "*",
"kysely": "*",
"mysql2": ">=2",
"pg": ">=8",
"postgres": ">=3",
"sql.js": ">=1",
"sqlite3": ">=5"
},
"peerDependenciesMeta": {
"@aws-sdk/client-rds-data": {
"optional": true
},
"@cloudflare/workers-types": {
"optional": true
},
"@electric-sql/pglite": {
"optional": true
},
"@libsql/client": {
"optional": true
},
"@libsql/client-wasm": {
"optional": true
},
"@neondatabase/serverless": {
"optional": true
},
"@op-engineering/op-sqlite": {
"optional": true
},
"@opentelemetry/api": {
"optional": true
},
"@planetscale/database": {
"optional": true
},
"@prisma/client": {
"optional": true
},
"@tidbcloud/serverless": {
"optional": true
},
"@types/better-sqlite3": {
"optional": true
},
"@types/pg": {
"optional": true
},
"@types/sql.js": {
"optional": true
},
"@upstash/redis": {
"optional": true
},
"@vercel/postgres": {
"optional": true
},
"@xata.io/client": {
"optional": true
},
"better-sqlite3": {
"optional": true
},
"bun-types": {
"optional": true
},
"expo-sqlite": {
"optional": true
},
"gel": {
"optional": true
},
"knex": {
"optional": true
},
"kysely": {
"optional": true
},
"mysql2": {
"optional": true
},
"pg": {
"optional": true
},
"postgres": {
"optional": true
},
"prisma": {
"optional": true
},
"sql.js": {
"optional": true
},
"sqlite3": {
"optional": true
}
}
},
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
"version": "5.18.2", "version": "5.18.2",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz",
@ -2411,19 +1802,6 @@
"@esbuild/win32-x64": "0.25.8" "@esbuild/win32-x64": "0.25.8"
} }
}, },
"node_modules/esbuild-register": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz",
"integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.3.4"
},
"peerDependencies": {
"esbuild": ">=0.12 <1"
}
},
"node_modules/fetch-blob": { "node_modules/fetch-blob": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
@ -2459,6 +1837,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,11 +2260,45 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/ms": { "node_modules/motion": {
"version": "2.1.3", "version": "12.23.12",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/motion/-/motion-12.23.12.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-8jCD8uW5GD1csOoqh1WhH1A6j5APHVE15nuBkFeRiMzYBdRwyAHmSP/oXSuW0WJPZRXTFdBoG4hY9TFWNhhwng==",
"dev": true, "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" "license": "MIT"
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
@ -3004,6 +2443,12 @@
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/pocketbase": {
"version": "0.26.3",
"resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.26.3.tgz",
"integrity": "sha512-5deUKRoEczpxxuHzwr6/DHVmgbggxylEVig8CKN+MjvtYxPUqX/C6puU0yaR2yhTi8zrh7J9s7Ty+qBGwVzWOQ==",
"license": "MIT"
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.6", "version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@ -3151,16 +2596,6 @@
"is-arrayish": "^0.3.1" "is-arrayish": "^0.3.1"
} }
}, },
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -3170,17 +2605,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"node_modules/styled-jsx": { "node_modules/styled-jsx": {
"version": "5.1.6", "version": "5.1.6",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",

View File

@ -12,9 +12,10 @@
"@formkit/auto-animate": "^0.8.2", "@formkit/auto-animate": "^0.8.2",
"@libsql/client": "^0.15.10", "@libsql/client": "^0.15.10",
"dotenv": "^17.2.1", "dotenv": "^17.2.1",
"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",
"pocketbase": "^0.26.3",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
@ -25,7 +26,6 @@
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"drizzle-kit": "^0.31.4",
"tailwindcss": "^4", "tailwindcss": "^4",
"tsx": "^4.20.3", "tsx": "^4.20.3",
"typescript": "^5" "typescript": "^5"

View File

@ -1,8 +1,8 @@
import { getMovies } from "@/lib/db"; import { DB_getMovies } from '@/lib/db/pb';
import { NextResponse } from "next/server"; import { NextResponse } from 'next/server';
export const GET = async () => { export const GET = async () => {
const movies = await getMovies(); const movies = await DB_getMovies();
const res = NextResponse.json(movies); const res = NextResponse.json(movies);
return res; return res;

View File

@ -1,16 +1,16 @@
import "./globals.css"; import './globals.css';
import { Navbar } from "@/components/organisms/Navbar"; import { Navbar } from '@/components/organisms/Navbar';
import { AuroraBackground } from "@/components/effects"; import { AuroraBackground } from '@/components/effects';
import { GlobalStoreProvider } from "./store/globalStore"; import { GlobalStoreProvider } from './store/globalStore';
import { getMovies } from "@/lib/db"; import { DB_getMovies } from '@/lib/db/pb';
export default async function RootLayout({ export default async function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
const movies = await getMovies(); const movies = await DB_getMovies();
return ( return (
<html lang="pl"> <html lang="pl">
@ -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>

View File

@ -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>
)} )}

View File

@ -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>

View File

@ -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" />
</> </>
); );

View File

@ -1,15 +1,16 @@
"use client"; 'use client';
import { addMovieToDB, deleteMovieFromDB, updateMovieInDB } from "@/lib/db"; import { Spinner } from '@/components/atoms/Spinner';
import { movies } from "@/lib/db/schema"; import { useLocalStorage } from '@/hooks/useLocalStorage';
import { createContext, FC, use, useState } from "react"; import { DB_addMovie, DB_deleteMovie, DB_updateMovie } from '@/lib/db/pb';
import { createContext, FC, use, useEffect, useState } from 'react';
type Movie = typeof movies.$inferSelect;
type GlobalStore = { type GlobalStore = {
movies: Movie[]; movies: Movie[];
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 +18,8 @@ const globalStore = createContext<GlobalStore>({
addMovie: () => {}, addMovie: () => {},
deleteMovie: () => {}, deleteMovie: () => {},
updateMovie: () => {}, updateMovie: () => {},
displayType: 'grid',
setDisplayType: () => {},
}); });
type Props = { type Props = {
@ -29,32 +32,53 @@ export const GlobalStoreProvider: FC<Props> = ({
initialMovies = [], initialMovies = [],
}) => { }) => {
// Optimistic update // Optimistic update
const [movies, setMovies] = useState<GlobalStore["movies"]>(initialMovies); const [firstRender, setFirstRender] = useState(true);
const [movies, setMovies] = useState<GlobalStore['movies']>(initialMovies);
const [displayType, setDisplayType] = useLocalStorage<
GlobalStore['displayType']
>('displayType', 'grid');
useEffect(() => {
if (firstRender) {
setFirstRender(false);
}
}, [firstRender]);
const addMovie = async (movie: Movie) => { const addMovie = async (movie: Movie) => {
if (movies.find((m) => m.id === movie.id)) return; if (movies.find(m => m.id === movie.id)) return;
addMovieToDB(movie); DB_addMovie(movie);
setMovies((prev) => [...prev, movie]); setMovies(prev => [...prev, movie]);
}; };
const deleteMovie = async (id: number) => { const deleteMovie = async (id: number) => {
deleteMovieFromDB(id); DB_deleteMovie(id);
setMovies((prev) => prev.filter((m) => m.id !== id)); setMovies(prev => prev.filter(m => m.id !== id));
}; };
const updateMovie = async (id: number, movie: Partial<Movie>) => { const updateMovie = async (id: number, movie: Partial<Movie>) => {
updateMovieInDB(id, movie); DB_updateMovie(id, movie);
setMovies((prev) => setMovies(prev => prev.map(m => (m.id === id ? { ...m, ...movie } : m)));
prev.map((m) => (m.id === id ? { ...m, ...movie } : m))
);
}; };
return ( return (
<globalStore.Provider <globalStore.Provider
value={{ movies, addMovie, deleteMovie, updateMovie }} value={{
movies,
addMovie,
deleteMovie,
updateMovie,
displayType,
setDisplayType,
}}
> >
{children} {firstRender ? (
<div className="flex justify-center items-center h-screen bg-black/80">
<Spinner />
</div>
) : (
children
)}
</globalStore.Provider> </globalStore.Provider>
); );
}; };

View File

@ -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",
},
}; };

View File

@ -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>

View File

@ -1,22 +1,18 @@
"use client"; 'use client';
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 { FaFire, FaPlusCircle, FaTrash } from 'react-icons/fa';
import { import Link from 'next/link';
AuroraLayout, import { RxEyeOpen } from 'react-icons/rx';
MinimalLayout, import { MdFavorite } from 'react-icons/md';
ZeusLayout, import { RiCalendarCheckLine, RiCalendarScheduleLine } from 'react-icons/ri';
DefaultLayout,
} 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 +23,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 +68,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>
);
}; };

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -1,4 +0,0 @@
export { AuroraLayout } from "./AuroraLayout";
export { MinimalLayout } from "./MinimalLayout";
export { ZeusLayout } from "./ZeusLayout";
export { DefaultLayout } from "./DefaultLayout";

View File

@ -1,8 +1,10 @@
import { formatter } from "@/helpers/formater"; 'use client';
import { Movie } from "@/types/global"; import { formatter } from '@/helpers/formater';
import Link from "next/link"; import Link from 'next/link';
import { FC } from "react"; import { FC } from 'react';
import { FaCalendar, FaClock, FaStar } 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 +17,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 +29,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>
); );
}; };

View File

@ -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)}
> >

View File

@ -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>
); );
}; };

View File

@ -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)}
> >

View File

@ -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" : ""
}`} }`}

View File

@ -1,22 +1,21 @@
"use client"; 'use client';
import { FC, ReactNode, useState } from "react"; import { FC, ReactNode, useState } from 'react';
import { MovieCard } from "@/components/atoms/MovieCard"; import { MovieCard } from '@/components/atoms/MovieCard';
import { Movie } from "@/types/global"; import { useGlobalStore } from '@/app/store/globalStore';
import { useGlobalStore } from "@/app/store/globalStore"; import { Dropdown } from '@/components/atoms/Dropdown';
import { Dropdown } from "@/components/atoms/Dropdown"; import { useAutoAnimate } from '@formkit/auto-animate/react';
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { Button } from '@/components/atoms/Button';
import { Button } from "@/components/atoms/Button"; import { Label } from '@/components/atoms/Label';
import { Label } from "@/components/atoms/Label"; import { FaList } from 'react-icons/fa';
import { FaList } from "react-icons/fa"; import { MovieRow } from '@/components/atoms/MovieRow';
import { MovieRow } from "@/components/atoms/MovieRow";
type Props = { 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;
@ -26,8 +25,8 @@ type Props = {
fluid?: boolean; fluid?: boolean;
showSorting?: boolean; showSorting?: boolean;
sort?: "title" | "releaseDate" | "popularity"; sort?: 'title' | 'releaseDate' | 'popularity';
sortDirection?: "asc" | "desc"; sortDirection?: 'asc' | 'desc';
loadMore?: boolean; loadMore?: boolean;
}; };
@ -35,7 +34,7 @@ type Props = {
export const MovieList: FC<Props> = ({ export const MovieList: FC<Props> = ({
heading, heading,
icon, icon,
colors = "white", colors = 'white',
overrideMovies, overrideMovies,
showFilters = true, showFilters = true,
filterSeen: filterSeenInitial, filterSeen: filterSeenInitial,
@ -44,29 +43,33 @@ export const MovieList: FC<Props> = ({
filterReleased: filterReleasedInitial, filterReleased: filterReleasedInitial,
fluid = false, fluid = false,
showSorting = true, showSorting = true,
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,
upcoming: filterUpcomingInitial, upcoming: filterUpcomingInitial,
released: filterReleasedInitial, released: filterReleasedInitial,
}); });
const [sort, setSort] = useState<"title" | "releaseDate" | "popularity">( const [sort, setSort] = useState<'title' | 'releaseDate' | 'popularity'>(
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 => {
let result = true; let result = true;
const today = new Date(); const today = new Date();
if (filter.seen !== undefined) { if (filter.seen !== undefined) {
@ -89,19 +92,19 @@ export const MovieList: FC<Props> = ({
}); });
let sortedMovies = filteredMovies.sort((a, b) => { let sortedMovies = filteredMovies.sort((a, b) => {
if (sort === "title") return a.title.localeCompare(b.title); if (sort === 'title') return a.title.localeCompare(b.title);
if (sort === "releaseDate") if (sort === 'releaseDate')
return ( return (
new Date(b.release_date).getTime() - new Date(a.release_date).getTime() new Date(b.release_date).getTime() - new Date(a.release_date).getTime()
); );
if (sort === "popularity") return b.popularity - a.popularity; if (sort === 'popularity') return b.popularity - a.popularity;
return 0; return 0;
}); });
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({
@ -115,7 +118,7 @@ export const MovieList: FC<Props> = ({
return ( return (
<section className="blocks"> <section className="blocks">
<div className={`${fluid ? "max-w-full px-4" : "container"}`}> <div className={`${fluid ? 'max-w-full px-4' : 'container'}`}>
{heading && ( {heading && (
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{icon && ( {icon && (
@ -147,17 +150,17 @@ export const MovieList: FC<Props> = ({
</Label> </Label>
<Label <Label
active={filter.favorites !== undefined} active={filter.favorites !== undefined}
onClick={() => handleFilter("favorites")} onClick={() => handleFilter('favorites')}
> >
Ulubione ({movies.filter((movie) => movie.favorite).length}) Ulubione ({movies.filter(movie => movie.favorite).length})
</Label> </Label>
<Label <Label
active={ active={
filter.seen !== undefined && filter.released === undefined 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={ active={
@ -175,7 +178,7 @@ export const MovieList: FC<Props> = ({
Do obejrzenia ( Do obejrzenia (
{ {
movies.filter( movies.filter(
(movie) => movie =>
new Date(movie.release_date) < new Date() && !movie.seen new Date(movie.release_date) < new Date() && !movie.seen
).length ).length
} }
@ -183,12 +186,12 @@ export const MovieList: FC<Props> = ({
</Label> </Label>
<Label <Label
active={filter.upcoming !== undefined} active={filter.upcoming !== undefined}
onClick={() => handleFilter("upcoming")} onClick={() => handleFilter('upcoming')}
> >
Nadchodzące ( Nadchodzące (
{ {
movies.filter( movies.filter(
(movie) => new Date(movie.release_date) > new Date() movie => new Date(movie.release_date) > new Date()
).length ).length
} }
) )
@ -196,24 +199,26 @@ 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' },
{ label: "Data premiery", value: "releaseDate" }, { label: 'Data premiery', value: 'releaseDate' },
{ label: "Popularność", value: "popularity" }, { label: 'Popularność', value: 'popularity' },
]} ]}
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>
@ -226,9 +231,9 @@ export const MovieList: FC<Props> = ({
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-y-6 gap-3 sm:gap-6 mt-8 justify-center" className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-y-6 gap-3 sm:gap-6 mt-8 justify-center"
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 />
) )
@ -252,14 +257,14 @@ export const MovieList: FC<Props> = ({
}; };
const colorsMap = { const colorsMap = {
white: "bg-gradient-to-r from-white to-gray-300", white: 'bg-gradient-to-r from-white to-gray-300',
yellow: "bg-gradient-to-r from-yellow-400 to-orange-400", yellow: 'bg-gradient-to-r from-yellow-400 to-orange-400',
blue: "bg-gradient-to-r from-blue-400 to-purple-400", blue: 'bg-gradient-to-r from-blue-400 to-purple-400',
green: "bg-gradient-to-r from-green-400 to-teal-400", green: 'bg-gradient-to-r from-green-400 to-teal-400',
red: "bg-gradient-to-r from-red-400 to-pink-400", red: 'bg-gradient-to-r from-red-400 to-pink-400',
purple: "bg-gradient-to-r from-purple-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", orange: 'bg-gradient-to-r from-orange-400 to-yellow-400',
pink: "bg-gradient-to-r from-pink-400 to-purple-400", pink: 'bg-gradient-to-r from-pink-400 to-purple-400',
teal: "bg-gradient-to-r from-teal-400 to-green-400", teal: 'bg-gradient-to-r from-teal-400 to-green-400',
gray: "bg-gradient-to-r from-gray-400 to-gray-400", gray: 'bg-gradient-to-r from-gray-400 to-gray-400',
}; };

View File

@ -0,0 +1,135 @@
'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 Link from 'next/link';
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',
};

View File

@ -1,9 +1,9 @@
"use client"; 'use client';
import { SearchResult } from "@/lib/tmdb/types"; import { SearchResult } from '@/lib/tmdb/types';
import { MovieCard } from "@/components/atoms/MovieCard"; import { MovieCard } from '@/components/atoms/MovieCard';
import { FC } from "react"; import { FC } from 'react';
import { FaStar } from "react-icons/fa"; import { FaStar } from 'react-icons/fa';
import { Carousel } from "../Carousel"; import { Carousel } from '../Carousel';
type Props = { type Props = {
movies: SearchResult; movies: SearchResult;
@ -26,10 +26,9 @@ export const RecommendedMovies: FC<Props> = ({ movies }) => {
new Date(b.release_date).getTime() - new Date(b.release_date).getTime() -
new Date(a.release_date).getTime() new Date(a.release_date).getTime()
) )
.map((movie) => ( .map(movie => (
<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}
@ -39,7 +38,7 @@ export const RecommendedMovies: FC<Props> = ({ movies }) => {
popularity={movie.popularity} popularity={movie.popularity}
adult={movie.adult} adult={movie.adult}
backdrop_path={movie.backdrop_path} backdrop_path={movie.backdrop_path}
genre_ids={movie.genre_ids.join(",")} genre_ids={movie.genre_ids.join(',')}
original_language={movie.original_language} original_language={movie.original_language}
original_title={movie.original_title} original_title={movie.original_title}
video={movie.video} video={movie.video}

View File

@ -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>
); );

View File

@ -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}

View File

@ -1,9 +1,8 @@
"use client"; 'use client';
import { FC } from "react"; import { FC } from 'react';
import { useGlobalStore } from "@/app/store/globalStore"; import { useGlobalStore } from '@/app/store/globalStore';
import { FaCalendar, FaClock } from "react-icons/fa"; import { FaCalendar, FaClock } from 'react-icons/fa';
import { MovieRow } from "@/components/atoms/MovieRow"; import { MovieRow } from '@/components/atoms/MovieRow';
import { Movie } from "@/types/global";
type Props = { type Props = {
overrideMovies?: Movie[]; overrideMovies?: Movie[];
@ -15,8 +14,8 @@ type Props = {
export const TrackedMovies: FC<Props> = ({ export const TrackedMovies: FC<Props> = ({
overrideMovies, overrideMovies,
daysLimit = 30, daysLimit = 30,
labelCurrent = "Aktualnie w kinach", labelCurrent = 'Aktualnie w kinach',
labelUpcoming = "Nadchodzące premiery", labelUpcoming = 'Nadchodzące premiery',
}) => { }) => {
const { movies: storeMovies } = useGlobalStore(); const { movies: storeMovies } = useGlobalStore();
@ -27,7 +26,7 @@ export const TrackedMovies: FC<Props> = ({
} }
const today = new Date(); const today = new Date();
const upcoming = movies.filter((movie) => { const upcoming = movies.filter(movie => {
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()) /
@ -38,7 +37,7 @@ export const TrackedMovies: FC<Props> = ({
new Date(movie.release_date) > today && daysSinceRelease <= daysLimit 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()) /
(1000 * 60 * 60 * 24) (1000 * 60 * 60 * 24)
@ -73,7 +72,7 @@ export const TrackedMovies: FC<Props> = ({
{labelCurrent} ({sortedInCinema.length}) {labelCurrent} ({sortedInCinema.length})
</h3> </h3>
<div className="space-y-2"> <div className="space-y-2">
{sortedInCinema.map((movie) => ( {sortedInCinema.map(movie => (
<MovieRow key={movie.id} movie={movie} isUpcoming={false} /> <MovieRow key={movie.id} movie={movie} isUpcoming={false} />
))} ))}
</div> </div>
@ -87,7 +86,7 @@ export const TrackedMovies: FC<Props> = ({
{labelUpcoming} ({sortedUpcoming.length}) {labelUpcoming} ({sortedUpcoming.length})
</h3> </h3>
<div className="space-y-2"> <div className="space-y-2">
{sortedUpcoming.map((movie) => ( {sortedUpcoming.map(movie => (
<MovieRow key={movie.id} movie={movie} isUpcoming /> <MovieRow key={movie.id} movie={movie} isUpcoming />
))} ))}
</div> </div>

View File

@ -1,17 +1,16 @@
"use client"; 'use client';
import { FC, useState, useEffect, useCallback } from "react"; import { FC, useState, useEffect, useCallback } from 'react';
import { Movie } from "@/types/global";
import { import {
FaPlus, FaPlus,
FaFire, FaFire,
FaChevronLeft, FaChevronLeft,
FaChevronRight, FaChevronRight,
FaMinus, FaMinus,
} from "react-icons/fa"; } from 'react-icons/fa';
import { RiCalendarCheckLine, RiCalendarScheduleLine } from "react-icons/ri"; import { RiCalendarCheckLine, RiCalendarScheduleLine } from 'react-icons/ri';
import { useGlobalStore } from "@/app/store/globalStore"; import { useGlobalStore } from '@/app/store/globalStore';
import Link from "next/link"; import Link from 'next/link';
import { Button } from "@/components/atoms/Button"; import { Button } from '@/components/atoms/Button';
type Props = { type Props = {
movies: Movie[]; movies: Movie[];
@ -50,7 +49,7 @@ export const Hero: FC<Props> = ({
vote_average, vote_average,
} = currentMovie; } = currentMovie;
const alreadyInStore = storedMovies.find((m) => m.id === id); const alreadyInStore = storedMovies.find(m => m.id === id);
const isReleased = new Date(release_date) < new Date(); const isReleased = new Date(release_date) < new Date();
const releaseDate = new Date(release_date); const releaseDate = new Date(release_date);
@ -58,7 +57,7 @@ export const Hero: FC<Props> = ({
if (isTransitioning) return; if (isTransitioning) return;
setIsTransitioning(true); setIsTransitioning(true);
setTimeout(() => { setTimeout(() => {
setCurrentIndex((prev) => (prev + 1) % movies.length); setCurrentIndex(prev => (prev + 1) % movies.length);
setIsTransitioning(false); setIsTransitioning(false);
}, 500); }, 500);
}, [movies.length, isTransitioning]); }, [movies.length, isTransitioning]);
@ -67,7 +66,7 @@ export const Hero: FC<Props> = ({
if (isTransitioning) return; if (isTransitioning) return;
setIsTransitioning(true); setIsTransitioning(true);
setTimeout(() => { setTimeout(() => {
setCurrentIndex((prev) => (prev - 1 + movies.length) % movies.length); setCurrentIndex(prev => (prev - 1 + movies.length) % movies.length);
setIsTransitioning(false); setIsTransitioning(false);
}, 500); }, 500);
}, [movies.length, isTransitioning]); }, [movies.length, isTransitioning]);
@ -108,7 +107,7 @@ export const Hero: FC<Props> = ({
<div <div
key={movie.id} key={movie.id}
className={`absolute inset-0 transition-opacity duration-500 ${ className={`absolute inset-0 transition-opacity duration-500 ${
index === currentIndex ? "opacity-100" : "opacity-0" index === currentIndex ? 'opacity-100' : 'opacity-0'
}`} }`}
> >
<img <img
@ -146,7 +145,7 @@ export const Hero: FC<Props> = ({
<div className="container relative z-10"> <div className="container relative z-10">
<div <div
className={`flex flex-col lg:flex-row items-center gap-8 lg:gap-12 transition-opacity duration-500 ${ className={`flex flex-col lg:flex-row items-center gap-8 lg:gap-12 transition-opacity duration-500 ${
isTransitioning ? "opacity-0" : "opacity-100" isTransitioning ? 'opacity-0' : 'opacity-100'
}`} }`}
> >
{/* Poster */} {/* Poster */}
@ -176,7 +175,7 @@ export const Hero: FC<Props> = ({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span <span
className={`flex items-center gap-1 text-sm ${ className={`flex items-center gap-1 text-sm ${
isReleased ? "text-green-400" : "text-yellow-400" isReleased ? 'text-green-400' : 'text-yellow-400'
}`} }`}
> >
{isReleased ? ( {isReleased ? (
@ -184,10 +183,10 @@ export const Hero: FC<Props> = ({
) : ( ) : (
<RiCalendarScheduleLine /> <RiCalendarScheduleLine />
)} )}
{releaseDate.toLocaleDateString("pl-PL", { {releaseDate.toLocaleDateString('pl-PL', {
day: "numeric", day: 'numeric',
month: "long", month: 'long',
year: "numeric", year: 'numeric',
})} })}
</span> </span>
</div> </div>
@ -211,12 +210,12 @@ export const Hero: FC<Props> = ({
{/* Action buttons */} {/* Action buttons */}
<div className="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start"> <div className="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start">
<Button <Button
theme={alreadyInStore ? "primary" : "secondary"} theme={alreadyInStore ? 'primary' : 'secondary'}
onClick={alreadyInStore ? handleRemove : handleAdd} onClick={alreadyInStore ? handleRemove : handleAdd}
> >
{alreadyInStore ? <FaMinus /> : <FaPlus />} {alreadyInStore ? <FaMinus /> : <FaPlus />}
<span> <span>
{alreadyInStore ? "Usuń z listy" : "Dodaj do listy"} {alreadyInStore ? 'Usuń z listy' : 'Dodaj do listy'}
</span> </span>
</Button> </Button>
</div> </div>
@ -234,8 +233,8 @@ export const Hero: FC<Props> = ({
disabled={isTransitioning} disabled={isTransitioning}
className={`w-3 h-3 rounded-full transition-all duration-300 disabled:cursor-not-allowed cursor-pointer ${ className={`w-3 h-3 rounded-full transition-all duration-300 disabled:cursor-not-allowed cursor-pointer ${
index === currentIndex index === currentIndex
? "bg-secondary scale-125" ? 'bg-secondary scale-125'
: "bg-white/50 hover:bg-secondary/70" : 'bg-white/50 hover:bg-secondary/70'
}`} }`}
/> />
))} ))}

View File

@ -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>

View File

@ -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>

View File

@ -1,5 +1,3 @@
import { Movie } from "@/types/global";
export const convertToMovie = ( export const convertToMovie = (
movie: any, movie: any,
override?: Partial<Movie> override?: Partial<Movie>
@ -12,13 +10,13 @@ export const convertToMovie = (
id: movie.id, id: movie.id,
title: movie.title, title: movie.title,
adult: movie.adult, adult: movie.adult,
backdrop_path: movie.backdrop_path || "", backdrop_path: movie.backdrop_path || '',
genre_ids: movie.genres?.join(",") || "", genre_ids: movie.genres?.join(',') || '',
original_language: movie.original_language, original_language: movie.original_language,
original_title: movie.original_title, original_title: movie.original_title,
overview: movie.overview || "", overview: movie.overview || '',
popularity: movie.popularity, popularity: movie.popularity,
poster_path: movie.poster_path || "", poster_path: movie.poster_path || '',
release_date: movie.release_date, release_date: movie.release_date,
video: movie.video, video: movie.video,
vote_average: movie.vote_average, vote_average: movie.vote_average,

View File

@ -1,37 +0,0 @@
"use server";
import { drizzle } from "drizzle-orm/libsql";
import { movies } from "./schema";
import { eq } from "drizzle-orm";
import { Movie } from "@/types/global";
import { revalidatePath } from "next/cache";
const db = drizzle(process.env.DB_FILE_NAME!);
export const getMovies = async () => {
return await db.select().from(movies).$withCache();
};
export const addMovieToDB = async (movie: Movie) => {
await db
.insert(movies)
.values({
...movie,
genre_ids: JSON.stringify(movie.genre_ids),
})
.onConflictDoNothing();
revalidatePath("/", "layout");
};
export const deleteMovieFromDB = async (id: number) => {
await db.delete(movies).where(eq(movies.id, id));
revalidatePath("/", "layout");
};
export const updateMovieInDB = async (
movieId: number,
movie: Partial<Movie>
) => {
await db.update(movies).set(movie).where(eq(movies.id, movieId));
revalidatePath("/", "layout");
};

64
src/lib/db/pb.ts Normal file
View File

@ -0,0 +1,64 @@
'use server';
import { revalidatePath } from 'next/cache';
import PocketBase from 'pocketbase';
const pb = new PocketBase(process.env.POCKET_URL!);
const collection = 'movies_prod';
type CollectionMovie = Movie & {
id_imdb: number;
};
export const DB_getMovies = async () => {
const records = await pb.collection<CollectionMovie>(collection).getFullList({
sort: '-created',
});
return records.map(record => ({
...record,
id: record.id_imdb,
})) as Movie[];
};
export const DB_addMovie = async (movie: Movie) => {
try {
await pb.collection<CollectionMovie>(collection).create({
...movie,
id: undefined,
id_imdb: movie.id,
genre_ids: JSON.stringify(movie.genre_ids),
});
revalidatePath('/', 'layout');
} catch (error) {
console.error(error);
}
};
export const DB_deleteMovie = async (id: number) => {
try {
const record = await pb
.collection<CollectionMovie>(collection)
.getFirstListItem(`id_imdb = "${id}"`);
await pb.collection(collection).delete(record.id.toString());
revalidatePath('/', 'layout');
} catch (error) {
console.error(error);
}
};
export const DB_updateMovie = async (id: number, movie: Partial<Movie>) => {
try {
const record = await pb
.collection<CollectionMovie>(collection)
.getFirstListItem(`id_imdb = "${id}"`);
await pb
.collection<CollectionMovie>(collection)
.update(record.id.toString(), {
...movie,
});
revalidatePath('/', 'layout');
} catch (error) {
console.error(error);
}
};

View File

@ -1,20 +0,0 @@
import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const movies = sqliteTable("movies", {
id: integer("id").primaryKey(),
title: text("title").notNull(),
adult: integer("adult", { mode: "boolean" }).notNull(),
backdrop_path: text("backdrop_path").notNull(),
genre_ids: text("genre_ids").notNull(),
original_language: text("original_language").notNull(),
original_title: text("original_title").notNull(),
overview: text("overview").notNull(),
popularity: real("popularity").notNull(),
poster_path: text("poster_path").notNull(),
release_date: text("release_date").notNull(),
video: integer("video", { mode: "boolean" }).notNull(),
vote_average: real("vote_average").notNull(),
vote_count: integer("vote_count").notNull(),
seen: integer("seen", { mode: "boolean" }).default(false),
favorite: integer("favorite", { mode: "boolean" }).default(false),
});

22
src/types/global.d.ts vendored
View File

@ -1,4 +1,18 @@
import { movies } from "@/lib/db/schema"; type Movie = {
import { SearchResult } from "@/lib/tmdb/types"; id: number;
title: string;
type Movie = typeof movies.$inferSelect; adult: boolean;
backdrop_path: string;
genre_ids: string;
original_language: string;
original_title: string;
overview: string;
popularity: number;
poster_path: string;
release_date: string;
video: boolean;
vote_average: number;
vote_count: number;
seen: boolean;
favorite: boolean;
};

73
todo.md
View File

@ -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
```