update style and setup gh-pages

This commit is contained in:
2024-10-20 00:42:23 +02:00
parent 129fcb672c
commit ba881b1637
14 changed files with 1455 additions and 629 deletions

1
.env Normal file
View File

@@ -0,0 +1 @@
REACT_APP_API_URL=https://letterboxd-diary-posters-picker-backend.onrender.com

484
package-lock.json generated
View File

@@ -28,7 +28,11 @@
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"redux": "^5.0.1", "redux": "^5.0.1",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"sass": "^1.80.3",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
},
"devDependencies": {
"gh-pages": "^6.2.0"
} }
}, },
"node_modules/@adobe/css-tools": { "node_modules/@adobe/css-tools": {
@@ -4293,6 +4297,279 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@parcel/watcher": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz",
"integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==",
"license": "MIT",
"dependencies": {
"detect-libc": "^1.0.3",
"is-glob": "^4.0.3",
"micromatch": "^4.0.5",
"node-addon-api": "^7.0.0"
},
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.4.1",
"@parcel/watcher-darwin-arm64": "2.4.1",
"@parcel/watcher-darwin-x64": "2.4.1",
"@parcel/watcher-freebsd-x64": "2.4.1",
"@parcel/watcher-linux-arm-glibc": "2.4.1",
"@parcel/watcher-linux-arm64-glibc": "2.4.1",
"@parcel/watcher-linux-arm64-musl": "2.4.1",
"@parcel/watcher-linux-x64-glibc": "2.4.1",
"@parcel/watcher-linux-x64-musl": "2.4.1",
"@parcel/watcher-win32-arm64": "2.4.1",
"@parcel/watcher-win32-ia32": "2.4.1",
"@parcel/watcher-win32-x64": "2.4.1"
}
},
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz",
"integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz",
"integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-x64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz",
"integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz",
"integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz",
"integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz",
"integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz",
"integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz",
"integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz",
"integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-arm64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz",
"integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-ia32": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz",
"integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-x64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz",
"integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@pkgjs/parseargs": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -8271,6 +8548,18 @@
"npm": "1.2.8000 || >= 1.4.16" "npm": "1.2.8000 || >= 1.4.16"
} }
}, },
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"license": "Apache-2.0",
"bin": {
"detect-libc": "bin/detect-libc.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/detect-newline": { "node_modules/detect-newline": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@@ -8541,6 +8830,13 @@
"integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==", "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/email-addresses": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-5.0.0.tgz",
"integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==",
"dev": true,
"license": "MIT"
},
"node_modules/emittery": { "node_modules/emittery": {
"version": "0.8.1", "version": "0.8.1",
"resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz",
@@ -9879,6 +10175,34 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/filename-reserved-regex": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz",
"integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/filenamify": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz",
"integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==",
"dev": true,
"license": "MIT",
"dependencies": {
"filename-reserved-regex": "^2.0.0",
"strip-outer": "^1.0.1",
"trim-repeated": "^1.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/filesize": { "node_modules/filesize": {
"version": "8.0.7", "version": "8.0.7",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz",
@@ -10427,6 +10751,54 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/gh-pages": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.2.0.tgz",
"integrity": "sha512-HMXJ8th9u5wRXaZCnLcs/d3oVvCHiZkaP5KQExQljYGwJjQbSPyTdHe/Gc1IvYUR/rWiZLxNobIqfoMHKTKjHQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"async": "^3.2.4",
"commander": "^11.0.0",
"email-addresses": "^5.0.0",
"filenamify": "^4.3.0",
"find-cache-dir": "^3.3.1",
"fs-extra": "^11.1.1",
"globby": "^11.1.0"
},
"bin": {
"gh-pages": "bin/gh-pages.js",
"gh-pages-clean": "bin/gh-pages-clean.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/gh-pages/node_modules/commander": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=16"
}
},
"node_modules/gh-pages/node_modules/fs-extra": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
"integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/glob": { "node_modules/glob": {
"version": "7.2.3", "version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -11037,6 +11409,12 @@
"url": "https://opencollective.com/immer" "url": "https://opencollective.com/immer"
} }
}, },
"node_modules/immutable": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
"integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==",
"license": "MIT"
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -14748,6 +15126,12 @@
"tslib": "^2.0.3" "tslib": "^2.0.3"
} }
}, },
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"license": "MIT"
},
"node_modules/node-forge": { "node_modules/node-forge": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
@@ -15268,9 +15652,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.0.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
@@ -15389,9 +15773,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.41", "version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -15409,8 +15793,8 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.7", "nanoid": "^3.3.7",
"picocolors": "^1.0.1", "picocolors": "^1.1.0",
"source-map-js": "^1.2.0" "source-map-js": "^1.2.1"
}, },
"engines": { "engines": {
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
@@ -17914,6 +18298,24 @@
"integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==", "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==",
"license": "CC0-1.0" "license": "CC0-1.0"
}, },
"node_modules/sass": {
"version": "1.80.3",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.80.3.tgz",
"integrity": "sha512-ptDWyVmDMVielpz/oWy3YP3nfs7LpJTHIJZboMVs8GEC9eUmtZTZhMHlTW98wY4aEorDfjN38+Wr/XjskFWcfA==",
"license": "MIT",
"dependencies": {
"@parcel/watcher": "^2.4.1",
"chokidar": "^4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-loader": { "node_modules/sass-loader": {
"version": "12.6.0", "version": "12.6.0",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
@@ -17952,6 +18354,34 @@
} }
} }
}, },
"node_modules/sass/node_modules/chokidar": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/sass/node_modules/readdirp": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz",
"integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==",
"license": "MIT",
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/sax": { "node_modules/sax": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
@@ -18344,9 +18774,9 @@
} }
}, },
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.0", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@@ -18856,6 +19286,19 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/strip-outer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz",
"integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"escape-string-regexp": "^1.0.2"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/style-loader": { "node_modules/style-loader": {
"version": "3.3.4", "version": "3.3.4",
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz",
@@ -19127,9 +19570,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "3.4.10", "version": "3.4.14",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.14.tgz",
"integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==", "integrity": "sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@alloc/quick-lru": "^5.2.0", "@alloc/quick-lru": "^5.2.0",
@@ -19434,6 +19877,19 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/trim-repeated": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz",
"integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==",
"dev": true,
"license": "MIT",
"dependencies": {
"escape-string-regexp": "^1.0.2"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tryer": { "node_modules/tryer": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz",

View File

@@ -2,6 +2,7 @@
"name": "frontend", "name": "frontend",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"homepage": "https://hugyouu.github.io/Letterboxd-Diary-Posters-Picker",
"dependencies": { "dependencies": {
"@emotion/react": "^11.13.3", "@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0", "@emotion/styled": "^11.13.0",
@@ -23,13 +24,16 @@
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"redux": "^5.0.1", "redux": "^5.0.1",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"sass": "^1.80.3",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject" "eject": "react-scripts eject",
"predeploy": "npm run build",
"deploy": "gh-pages -d build"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
@@ -48,5 +52,8 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
},
"devDependencies": {
"gh-pages": "^6.2.0"
} }
} }

View File

@@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import "./styles/App.css"; import "./styles/App.scss";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react"; import { PersistGate } from "redux-persist/integration/react";

View File

@@ -1,239 +1,282 @@
import React, { useState } from 'react'; import React, { useState } from "react";
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from "react-redux";
import { import {
Container, Typography, Card, CardMedia, CardContent, Container,
Button, Checkbox, FormControlLabel, TextField, Dialog, DialogTitle, DialogContent, DialogActions, Typography,
Paper Card,
} from '@material-ui/core'; CardMedia,
import { makeStyles } from '@material-ui/core/styles'; CardContent,
import DeleteIcon from '@material-ui/icons/Delete'; Button,
import GetAppIcon from '@material-ui/icons/GetApp'; Checkbox,
import ShareIcon from '@material-ui/icons/Share'; FormControlLabel,
import EditIcon from '@material-ui/icons/Edit'; TextField,
import { removePoster } from '../services/action'; Dialog,
DialogTitle,
DialogContent,
DialogActions,
Paper,
} from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles";
import DeleteIcon from "@material-ui/icons/Delete";
import GetAppIcon from "@material-ui/icons/GetApp";
import ShareIcon from "@material-ui/icons/Share";
import EditIcon from "@material-ui/icons/Edit";
import { removePoster } from "../services/action";
import axios from "axios"; import axios from "axios";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
marginTop: theme.spacing(4), marginTop: theme.spacing(4),
marginBottom: theme.spacing(8), marginBottom: theme.spacing(8),
display: 'flex', display: "flex",
}, },
posterList: { posterList: {
flex: 2, flex: 2,
marginRight: theme.spacing(2), marginRight: theme.spacing(2),
maxHeight: 'calc(100vh - 200px)', maxHeight: "calc(100vh - 200px)",
overflowY: 'auto', overflowY: "auto",
}, },
actionPanel: { actionPanel: {
flex: 1, flex: 1,
position: 'sticky', position: "sticky",
top: theme.spacing(4), top: theme.spacing(4),
height: 'fit-content', height: "fit-content",
}, },
card: { card: {
display: 'flex', display: "flex",
marginBottom: theme.spacing(2), marginBottom: theme.spacing(2),
backgroundColor: '#2c3e50', backgroundColor: "#2c3e50",
}, },
cardMedia: { cardMedia: {
width: 100, width: 100,
}, },
cardContent: { cardContent: {
flex: '1 0 auto', flex: "1 0 auto",
}, },
actions: { actions: {
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
gap: theme.spacing(2), gap: theme.spacing(2),
}, },
slider: { slider: {
width: '100%', width: "100%",
marginTop: theme.spacing(2), marginTop: theme.spacing(2),
}, },
button: { button: {
width: '100%', width: "100%",
}, },
typography: { typography: {
color: '#ecf0f1', color: "#ecf0f1",
}, },
formControl: { formControl: {
marginBottom: theme.spacing(2), marginBottom: theme.spacing(2),
}, },
checkbox: { checkbox: {
color: '#3498db', color: "#3498db",
}, },
paper: { paper: {
padding: theme.spacing(3), padding: theme.spacing(3),
backgroundColor: '#34495e', backgroundColor: "#34495e",
}, },
})); }));
const Cart = () => { const Cart = () => {
const classes = useStyles(); const classes = useStyles();
const dispatch = useDispatch(); const dispatch = useDispatch();
const selectedPosters = useSelector((state) => { const selectedPosters = useSelector((state) => {
const selections = state.posterSelections; const selections = state.posterSelections;
return Object.entries(selections).flatMap(([movieId, posters]) => return Object.entries(selections).flatMap(([movieId, posters]) =>
posters.map(posterId => ({ movieId, posterId })) posters.map((posterId) => ({ movieId, posterId }))
);
});
const [selectedForDownload, setSelectedForDownload] = useState(selectedPosters.map(() => true));
const [downloadFormat, setDownloadFormat] = useState('zip');
const [renameDialogOpen, setRenameDialogOpen] = useState(false);
const [currentPosterIndex, setCurrentPosterIndex] = useState(null);
const [newPosterName, setNewPosterName] = useState('');
const handleRemovePoster = (index) => {
dispatch(removePoster(selectedPosters[index].movieId, selectedPosters[index].posterId));
};
const handleDownload = async () => {
const postersToDownload = selectedPosters
.filter((_, index) => selectedForDownload[index])
.map((poster) => ({
movieId: poster.movieId,
posterId: poster.posterId,
movieName: poster.movieName,
movieYear: poster.movieYear
}));
console.log('Downloading posters:', postersToDownload);
try {
const response = await axios.post('http://localhost:5000/api/download-posters', {
posters: postersToDownload,
format: downloadFormat,
}, {
responseType: 'blob',
});
const blob = new Blob([response.data], { type: response.headers['content-type'] });
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = `posters.${downloadFormat}`;
link.click();
} catch (error) {
console.error('Failed to download posters:', error);
}
};
const handleShare = () => {
console.log('Sharing selected posters');
};
const handleRename = (index) => {
setCurrentPosterIndex(index);
setNewPosterName(selectedPosters[index].posterId);
setRenameDialogOpen(true);
};
const handleRenameConfirm = () => {
console.log(`Renaming poster ${currentPosterIndex} to ${newPosterName}`);
setRenameDialogOpen(false);
};
return (
<Container className={classes.root}>
<div className={classes.posterList}>
<Typography variant="h4" gutterBottom className={classes.typography}>Your Cart</Typography>
{selectedPosters.map((poster, index) => (
<Card key={index} className={classes.card}>
<CardMedia
className={classes.cardMedia}
image={`https://image.tmdb.org/t/p/w500${poster.posterId}`}
title={`Poster ${index + 1}`}
/>
<CardContent className={classes.cardContent}>
<Typography variant="h6"
className={classes.typography}>{`Movie ID: ${poster.movieId}`}</Typography>
<Typography variant="body2"
className={classes.typography}>{poster.posterId}</Typography>
<FormControlLabel
control={
<Checkbox
checked={selectedForDownload[index]}
onChange={(e) => {
const newSelected = [...selectedForDownload];
newSelected[index] = e.target.checked;
setSelectedForDownload(newSelected);
}}
className={classes.checkbox}
/>
}
label="Select for download"
className={classes.typography}
/>
<Button startIcon={<DeleteIcon/>} onClick={() => handleRemovePoster(index)}
className={classes.typography}>Remove</Button>
<Button startIcon={<EditIcon/>} onClick={() => handleRename(index)}
className={classes.typography}>Rename</Button>
</CardContent>
</Card>
))}
</div>
<Paper className={classes.actionPanel} elevation={3}>
<div className={classes.actions}>
<Typography gutterBottom>Download Format</Typography>
<TextField
select
value={downloadFormat}
onChange={(e) => setDownloadFormat(e.target.value)}
SelectProps={{
native: true,
}}
className={classes.formControl}
>
<option value="zip">ZIP</option>
<option value="tar">TAR</option>
<option value="7z">7Z</option>
</TextField>
<Button
variant="contained"
color="primary"
startIcon={<GetAppIcon/>}
onClick={handleDownload}
disabled={selectedForDownload.every((selected) => !selected)}
className={classes.button}
>
Download Selected
</Button>
<Button
variant="contained"
color="secondary"
startIcon={<ShareIcon/>}
onClick={handleShare}
className={classes.button}
>
Share Selection
</Button>
</div>
</Paper>
<Dialog open={renameDialogOpen} onClose={() => setRenameDialogOpen(false)}>
<DialogTitle>Rename Poster</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
label="New Name"
type="text"
fullWidth
value={newPosterName}
onChange={(e) => setNewPosterName(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setRenameDialogOpen(false)} color="primary">
Cancel
</Button>
<Button onClick={handleRenameConfirm} color="primary">
Rename
</Button>
</DialogActions>
</Dialog>
</Container>
); );
});
const [selectedForDownload, setSelectedForDownload] = useState(
selectedPosters.map(() => true)
);
const [downloadFormat, setDownloadFormat] = useState("zip");
const [renameDialogOpen, setRenameDialogOpen] = useState(false);
const [currentPosterIndex, setCurrentPosterIndex] = useState(null);
const [newPosterName, setNewPosterName] = useState("");
const handleRemovePoster = (index) => {
dispatch(
removePoster(
selectedPosters[index].movieId,
selectedPosters[index].posterId
)
);
};
const handleDownload = async () => {
const postersToDownload = selectedPosters
.filter((_, index) => selectedForDownload[index])
.map((poster) => ({
movieId: poster.movieId,
posterId: poster.posterId,
movieName: poster.movieName,
movieYear: poster.movieYear,
}));
console.log("Downloading posters:", postersToDownload);
try {
const response = await axios.post(
"http://localhost:5000/api/download-posters",
{
posters: postersToDownload,
format: downloadFormat,
},
{
responseType: "blob",
}
);
const blob = new Blob([response.data], {
type: response.headers["content-type"],
});
const link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = `posters.${downloadFormat}`;
link.click();
} catch (error) {
console.error("Failed to download posters:", error);
}
};
const handleShare = () => {
console.log("Sharing selected posters");
};
const handleRename = (index) => {
setCurrentPosterIndex(index);
setNewPosterName(selectedPosters[index].posterId);
setRenameDialogOpen(true);
};
const handleRenameConfirm = () => {
console.log(`Renaming poster ${currentPosterIndex} to ${newPosterName}`);
setRenameDialogOpen(false);
};
return (
<Container className="cart-container">
<div className="poster-list">
<Typography variant="h4" gutterBottom className="cart-title">
Your Cart
</Typography>
{selectedPosters.map((poster, index) => (
<Card key={index} className="poster-card">
<CardMedia
className="poster-media"
image={`https://image.tmdb.org/t/p/w500${poster.posterId}`}
title={`Poster ${index + 1}`}
/>
<CardContent className="poster-content">
<Typography variant="h6" className="poster-title">
{`Movie ID: ${poster.movieId}`}
</Typography>
<Typography variant="body2" className="poster-id">
{poster.posterId}
</Typography>
<FormControlLabel
control={
<Checkbox
checked={selectedForDownload[index]}
onChange={(e) => {
const newSelected = [...selectedForDownload];
newSelected[index] = e.target.checked;
setSelectedForDownload(newSelected);
}}
className="checkbox"
/>
}
label="Select for download"
/>
<Button
startIcon={<DeleteIcon />}
onClick={() => handleRemovePoster(index)}
className="action-button"
>
Remove
</Button>
<Button
startIcon={<EditIcon />}
onClick={() => handleRename(index)}
className="action-button"
>
Rename
</Button>
</CardContent>
</Card>
))}
</div>
<Paper className="action-panel" elevation={3}>
<div className="action-buttons">
<Typography gutterBottom className="action-title">
Download Format
</Typography>
<TextField
select
value={downloadFormat}
onChange={(e) => setDownloadFormat(e.target.value)}
SelectProps={{
native: true,
}}
className="form-control"
>
<option value="zip">ZIP</option>
<option value="tar">TAR</option>
<option value="7z">7Z</option>
</TextField>
<Button
variant="contained"
color="primary"
startIcon={<GetAppIcon />}
onClick={handleDownload}
disabled={selectedForDownload.every((selected) => !selected)}
className="primary-button"
>
Download Selected
</Button>
<Button
variant="contained"
color="secondary"
startIcon={<ShareIcon />}
onClick={handleShare}
className="secondary-button"
>
Share Selection
</Button>
</div>
</Paper>
<Dialog
open={renameDialogOpen}
onClose={() => setRenameDialogOpen(false)}
className="rename-dialog"
>
<DialogTitle className="dialog-title">Rename Poster</DialogTitle>
<DialogContent className="dialog-content">
<TextField
autoFocus
margin="dense"
label="New Name"
type="text"
fullWidth
value={newPosterName}
onChange={(e) => setNewPosterName(e.target.value)}
/>
</DialogContent>
<DialogActions className="dialog-actions">
<Button onClick={() => setRenameDialogOpen(false)} color="primary">
Cancel
</Button>
<Button onClick={handleRenameConfirm} color="primary">
Rename
</Button>
</DialogActions>
</Dialog>
</Container>
);
}; };
export default Cart; export default Cart;

View File

@@ -17,6 +17,8 @@ import {
import CheckIcon from "@material-ui/icons/Check"; import CheckIcon from "@material-ui/icons/Check";
import NavBar from "./NavBar"; import NavBar from "./NavBar";
const apiUrl = process.env.REACT_APP_API_URL;
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
marginTop: theme.spacing(4), marginTop: theme.spacing(4),
@@ -85,14 +87,12 @@ const PosterGallery = ({ movieId }) => {
(state) => state.posterSelections[movieId] || [] (state) => state.posterSelections[movieId] || []
); );
console.log(movieId);
useEffect(() => { useEffect(() => {
const fetchPosters = async () => { const fetchPosters = async () => {
try { try {
setLoading(true); setLoading(true);
const response = await axios.get( const response = await axios.get(
`http://localhost:5000/api/posters/${movieName}/${movieYear}` `${apiUrl}/api/posters/${movieName}/${movieYear}`
); );
setPosters(response.data.posters); setPosters(response.data.posters);
} catch (error) { } catch (error) {
@@ -115,39 +115,39 @@ const PosterGallery = ({ movieId }) => {
return ( return (
<> <>
<Container className={classes.root}> <Container className="poster-gallery">
<Paper elevation={3} className={classes.paper}> <Paper elevation={3} className="content-paper">
<Typography variant="h4" gutterBottom className={classes.title}> <Typography variant="h4" gutterBottom className="title">
Posters for {movieName} ({movieYear}) Posters for {movieName} ({movieYear})
</Typography> </Typography>
{loading ? ( {loading ? (
<Box className={classes.loadingContainer}> <Box className="loading-container">
<CircularProgress /> <CircularProgress />
</Box> </Box>
) : ( ) : (
<Grid container spacing={3}> <Grid container spacing={3}>
{posters.map((poster, index) => ( {posters.map((poster, index) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={index}> <Grid item xs={12} sm={6} md={4} lg={3} key={index}>
<Card <Card
className={`${classes.posterCard} ${ className={`poster-card ${
selectedPosters.includes(poster.file_path) selectedPosters.includes(poster.file_path)
? classes.selectedPoster ? "selected-poster"
: "" : ""
}`} }`}
onClick={() => handlePosterSelect(poster.file_path)} onClick={() => handlePosterSelect(poster.file_path)}
> >
<CardMedia <CardMedia
className={classes.posterImage} className="poster-image"
image={`https://image.tmdb.org/t/p/w500${poster.file_path}`} image={`https://image.tmdb.org/t/p/w500${poster.file_path}`}
title={`${movieName} poster ${index + 1}`} title={`${movieName} poster ${index + 1}`}
/> />
{selectedPosters.includes(poster.file_path) && ( {selectedPosters.includes(poster.file_path) && (
<CheckIcon className={classes.checkIcon} /> <CheckIcon className="check-icon" />
)} )}
</Card> </Card>
</Grid> </Grid>
))} ))}
</Grid> </Grid>
)} )}
</Paper> </Paper>
</Container> </Container>

View File

@@ -10,7 +10,6 @@ import {
LinearProgress, LinearProgress,
Box, Box,
Paper, Paper,
makeStyles,
Button, Button,
} from "@material-ui/core"; } from "@material-ui/core";
import Pagination from "@mui/material/Pagination"; import Pagination from "@mui/material/Pagination";
@@ -19,127 +18,7 @@ import queryString from "query-string";
import pulpGif from "../static/images/pulp.gif"; import pulpGif from "../static/images/pulp.gif";
import NavBar from "./NavBar"; import NavBar from "./NavBar";
const useStyles = makeStyles((theme) => ({ const apiUrl = process.env.REACT_APP_API_URL;
root: {
marginTop: theme.spacing(4),
marginBottom: theme.spacing(8),
},
paper: {
padding: theme.spacing(3),
borderRadius: theme.shape.borderRadius,
backgroundColor: "#1c1f23",
color: "white",
},
title: {
marginBottom: theme.spacing(3),
},
username: {
color: "#00A346",
marginBottom: theme.spacing(2),
},
list: {
maxHeight: "800px",
minHeight: "900px",
},
listItem: {
marginBottom: theme.spacing(2),
"&:hover": {
backgroundColor: theme.palette.action.hover,
},
"&:not(:last-child)": {
borderBottom: `1px solid #667788`,
paddingTop: theme.spacing(0.5),
paddingBottom: theme.spacing(0.5),
},
},
poster: {
width: "50px",
height: "75px",
objectFit: "cover",
marginRight: theme.spacing(2),
borderRadius: theme.shape.borderRadius,
},
movieInfo: {
flexGrow: 1,
},
movieTitle: {
color: "white",
fontFamily: "TiemposTextWeb-Semibold, Georgia, serif",
fontSize: "1.38461538rem",
fontWeight: "400",
"&:hover": {
color: "var(--primary)",
},
},
movieYear: {
color: "#667788",
},
watchedDate: {
color: "#667788",
textAlign: "center",
},
watchedDay: {
fontSize: "2rem",
},
refreshButton: {
marginTop: theme.spacing(2),
},
paginationContainer: {
display: "flex",
justifyContent: "center",
marginTop: theme.spacing(3),
},
progressContainer: {
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
height: "200px",
},
progressLabel: {
marginTop: theme.spacing(2),
color: "white",
},
noMoviesContainer: {
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
height: "400px",
},
gifContainer: {
display: "flex",
justifyContent: "center",
position: "relative",
width: "200px",
height: "200px",
marginBottom: theme.spacing(2),
},
circleBackground: {
position: "absolute",
width: "120%",
height: "120%",
borderRadius: "50%",
backgroundColor: "#ffffff",
clipPath: "inset(0 0 25% 0)",
bottom: "-28%",
},
gif: {
position: "absolute",
width: "100%",
height: "100%",
objectFit: "contain",
},
backButton: {
marginTop: theme.spacing(2),
backgroundColor: "#00A346",
color: "white",
"&:hover": {
backgroundColor: "#008036",
},
},
}));
const paginationTheme = createTheme({ const paginationTheme = createTheme({
palette: { palette: {
@@ -173,7 +52,6 @@ const paginationTheme = createTheme({
}); });
const PosterSelector = () => { const PosterSelector = () => {
const classes = useStyles();
const [movies, setMovies] = useState([]); const [movies, setMovies] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
@@ -182,20 +60,19 @@ const PosterSelector = () => {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const moviesPerPage = 10; const moviesPerPage = 8;
useEffect(() => { useEffect(() => {
const parsed = queryString.parse(location.search); const parsed = queryString.parse(location.search);
const pageNumber = parsed.page ? parseInt(parsed.page, 10) : 1; const pageNumber = parsed.page ? parseInt(parsed.page, 10) : 1;
setPage(pageNumber); setPage(pageNumber);
fetchMovies(pageNumber).then(r => console.log(r)); fetchMovies(pageNumber).then((r) => console.log(r));
// fetchUsername();
}, [location.search]); }, [location.search]);
const fetchUsername = async () => { const fetchUsername = async () => {
try { try {
const response = await axios.get("http://localhost:5000/api/username"); const response = await axios.get(`${apiUrl}/api/username`);
setUsername(response.data.username); setUsername(response.data.username);
} catch (error) { } catch (error) {
console.error("Error fetching username:", error); console.error("Error fetching username:", error);
@@ -208,7 +85,7 @@ const PosterSelector = () => {
setProgress(0); setProgress(0);
const response = await axios.get( const response = await axios.get(
`http://localhost:5000/api/movies?page=${pageNumber}&limit=${moviesPerPage}`, `${apiUrl}/api/movies?page=${pageNumber}&limit=${moviesPerPage}`,
{ {
onDownloadProgress: (progressEvent) => { onDownloadProgress: (progressEvent) => {
const percentCompleted = Math.round( const percentCompleted = Math.round(
@@ -240,7 +117,7 @@ const PosterSelector = () => {
const handleRefreshFiles = async () => { const handleRefreshFiles = async () => {
try { try {
await axios.delete("http://localhost:5000/api/delete-csv"); await axios.delete(`${apiUrl}/api/delete-csv`);
navigate("/"); navigate("/");
} catch (error) { } catch (error) {
console.error("Error deleting CSV file:", error); console.error("Error deleting CSV file:", error);
@@ -267,121 +144,116 @@ const PosterSelector = () => {
return ( return (
<> <>
<Container className={classes.root}> <Container className="poster-selector">
<Paper elevation={3} className={classes.paper}> <Paper elevation={3} className="content-paper">
<Typography variant="h4" gutterBottom className={classes.title}> <Typography variant="h4" className="title">
Your diary Your diary
</Typography> </Typography>
{username && ( {username && (
<Typography variant="h6" className={classes.username}> <Typography variant="h6" className="username">
Letterboxd User: {username} Letterboxd User: {username}
</Typography> </Typography>
)} )}
{loading ? ( {loading ? (
<Box className={classes.progressContainer}> <Box className="progress-container">
<LinearProgress variant="determinate" value={progress} /> <LinearProgress />
<Typography className={classes.progressLabel}> <Typography className="progress-label">
Downloading movies... Downloading movies...
</Typography> </Typography>
</Box> </Box>
) : movies.length > 0 ? ( ) : movies.length > 0 ? (
<> <>
<List className={classes.list}> <List className="movies-list">
{movies.map((movie, index) => ( {movies.map((movie, index) => (
<ListItem <ListItem
button button
key={index} key={index}
onClick={() => handleMovieClick(movie.Name, movie.Year)} onClick={() => handleMovieClick(movie.Name, movie.Year)}
className={classes.listItem} className="movie-item"
> >
<Grid container alignItems="center"> <Grid container alignItems="center">
<Grid item> <Grid item>
<img <img
src={ src={
`https://image.tmdb.org/t/p/w500${movie.Poster.file_path}` || `https://image.tmdb.org/t/p/w500${movie.Poster.file_path}` ||
`/api/placeholder/50/75` `/api/placeholder/50/75`
} }
alt={movie.Name} alt={movie.Name}
className={classes.poster} className="movie-poster"
/> />
</Grid> </Grid>
<Grid item className={classes.movieInfo}> <Grid item className="movie-info">
<Typography <Typography variant="subtitle1" className="movie-title">
variant="subtitle1" {movie.Name}
className={classes.movieTitle} </Typography>
> <Typography variant="body2" className="movie-year">
{movie.Name} {movie.Year}
</Typography> </Typography>
<Typography </Grid>
variant="body2" <Grid item>
color="textSecondary" <Box className="watched-date">
className={classes.movieYear} {(() => {
> const { day, month } = formatWatchedDate(
{movie.Year} movie["Watched Date"]
</Typography> );
</Grid> return (
<Grid item> <>
<Box className={classes.watchedDate}> <Typography
{(() => { variant="body2"
const { day, month } = formatWatchedDate( className="watched-day"
movie["Watched Date"] >
); {day}
return ( </Typography>
<> <Typography
<Typography variant="body2"
variant="body2" className="watched-month"
className={classes.watchedDay} >
> {month}
{day} </Typography>
</Typography> </>
<Typography );
variant="body2" })()}
className={classes.watchedMonth} </Box>
> </Grid>
{month} </Grid>
</Typography> </ListItem>
</> ))}
); </List>
})()} <Box className="pagination-container">
</Box> <ThemeProvider theme={paginationTheme}>
</Grid> <Pagination
</Grid> count={totalPages}
</ListItem> page={page}
))} onChange={handlePageChange}
</List> color="primary"
<Box className={classes.paginationContainer}>
<ThemeProvider theme={paginationTheme}>
<Pagination
count={totalPages}
page={page}
onChange={handlePageChange}
color="primary"
/>
</ThemeProvider>
</Box>
</>
) : (
<Box className={classes.noMoviesContainer}>
<div className={classes.gifContainer}>
<div className={classes.circleBackground}></div>
<img
src={pulpGif}
alt="Confused reaction"
className={classes.gif}
/> />
</div> </ThemeProvider>
<Typography variant="body1" gutterBottom>
No movies found in your diary. Please check your Letterboxd
username or CSV file.
</Typography>
<Button
variant="contained"
className={classes.backButton}
onClick={handleGoBack}
>
Go Back
</Button>
</Box> </Box>
</>
) : (
<Box className="no-movies-container">
<div className="gif-container">
<div className="circle-background"></div>
<img
src={pulpGif}
alt="Confused reaction"
className="reaction-gif"
/>
</div>
<Typography variant="body1" gutterBottom>
No movies found in your diary. Please check your Letterboxd
username or CSV file.
</Typography>
<Button
variant="contained"
className="back-button"
onClick={handleGoBack}
>
Go Back
</Button>
</Box>
)} )}
</Paper> </Paper>
</Container> </Container>

View File

@@ -7,103 +7,15 @@ import {
Button, Button,
CircularProgress, CircularProgress,
TextField, TextField,
makeStyles,
} from "@material-ui/core"; } from "@material-ui/core";
import { CloudUploadOutlined } from "@material-ui/icons"; import { CloudUploadOutlined } from "@material-ui/icons";
import CloudDoneOutlinedIcon from "@mui/icons-material/CloudDoneOutlined"; import CloudDoneOutlinedIcon from "@mui/icons-material/CloudDoneOutlined";
import { useDropzone } from "react-dropzone"; import { useDropzone } from "react-dropzone";
import axios from "axios"; import axios from "axios";
const useStyles = makeStyles((theme) => ({ const apiUrl = process.env.REACT_APP_API_URL;
root: {
padding: theme.spacing(4),
backgroundColor: "#14181c",
minHeight: "100vh",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
},
card: {
width: "100%",
maxWidth: "600px",
backgroundColor: "#1c1f23",
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shadows[3],
padding: theme.spacing(4),
},
title: {
marginBottom: theme.spacing(3),
fontWeight: "bold",
color: "#ffffff",
},
dropzone: {
border: `2px dashed #00A346`,
borderRadius: theme.shape.borderRadius,
padding: theme.spacing(4),
textAlign: "center",
cursor: "pointer",
"&:hover": {
backgroundColor: "#1f252a",
},
},
dropzoneIcon: {
marginBottom: theme.spacing(2),
color: "#00A346",
},
dropzoneText: {
marginBottom: theme.spacing(2),
color: "#ffffff",
},
progressContainer: {
marginTop: theme.spacing(2),
},
submitButton: {
color: "#fff",
fontSize: "0.8rem",
fontWeight: "900",
textTransform: "uppercase",
letterSpacing: "0.04em",
lineHeight: "2.8rem",
display: "inline-block",
cursor: "pointer",
padding: "0 1rem",
border: "0",
borderRadius: "4px",
outline: "none",
background: "#526e89",
transition: "background-color 0.3s ease",
"&:hover": {
backgroundColor: "#1caff2",
},
},
overviewText: {
color: "#456",
marginTop: theme.spacing(4),
padding: theme.spacing(3),
},
usernameField: {
marginBottom: theme.spacing(3),
"& .MuiOutlinedInput-root": {
color: "#ffffff",
"& fieldset": {
borderColor: "#00A346",
},
"&:hover fieldset": {
borderColor: "#00A346",
},
"&.Mui-focused fieldset": {
borderColor: "#1caff2",
},
},
"& .MuiInputLabel-root": {
color: "#ffffff",
},
},
}));
const UploadDiary = () => { const UploadDiary = () => {
const classes = useStyles();
const [file, setFile] = useState(null); const [file, setFile] = useState(null);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
@@ -112,7 +24,7 @@ const UploadDiary = () => {
useEffect(() => { useEffect(() => {
const checkCSVFile = async () => { const checkCSVFile = async () => {
try { try {
const response = await axios.get("http://localhost:5000/api/check-csv"); const response = await axios.get(`${apiUrl}/api/check-csv`);
if (response.data.fileExists) { if (response.data.fileExists) {
navigate("/PosterSelector"); navigate("/PosterSelector");
} }
@@ -136,16 +48,14 @@ const UploadDiary = () => {
setUploading(true); setUploading(true);
try { try {
if (username) { if (username) {
// Here you would implement the logic to fetch the diary from Letterboxd
// For now, we'll just simulate a successful fetch
console.log(`Fetching diary for user: ${username}`); console.log(`Fetching diary for user: ${username}`);
await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate API call await new Promise((resolve) => setTimeout(resolve, 1000));
navigate("/PosterSelector"); navigate("/PosterSelector");
} else if (file) { } else if (file) {
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
await axios.post("http://localhost:5000/api/upload-csv", formData, { await axios.post(`${apiUrl}/api/upload-csv`, formData, {
headers: { headers: {
"Content-Type": "multipart/form-data", "Content-Type": "multipart/form-data",
}, },
@@ -162,13 +72,13 @@ const UploadDiary = () => {
}; };
return ( return (
<Container className={classes.root}> <Container className="upload-diary">
<div className={classes.card}> <div className="upload-card">
<Typography variant="h4" className={classes.title}> <Typography variant="h4" className="title">
Upload Letterboxd Diary Upload Letterboxd Diary
</Typography> </Typography>
<TextField <TextField
className={classes.usernameField} className="username-field"
label="Letterboxd Username" label="Letterboxd Username"
variant="outlined" variant="outlined"
fullWidth fullWidth
@@ -176,42 +86,33 @@ const UploadDiary = () => {
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
placeholder="Enter your Letterboxd username" placeholder="Enter your Letterboxd username"
/> />
<Typography <Typography variant="body2" className="or-text">
variant="body2"
style={{ color: "#ffffff", marginBottom: "1rem" }}
>
Or upload a CSV file: Or upload a CSV file:
</Typography> </Typography>
<div {...getRootProps()} className={classes.dropzone}> <div {...getRootProps()} className="dropzone">
<input {...getInputProps()} /> <input {...getInputProps()} />
{file ? ( {file ? (
<> <>
<CloudDoneOutlinedIcon <CloudDoneOutlinedIcon className="dropzone-icon" />
className={classes.dropzoneIcon} <Typography variant="h6" className="dropzone-text">
style={{ fontSize: "4rem" }}
/>
<Typography variant="h6" className={classes.dropzoneText}>
Your file has been uploaded: {file.name} Your file has been uploaded: {file.name}
</Typography> </Typography>
</> </>
) : ( ) : (
<> <>
<CloudUploadOutlined <CloudUploadOutlined className="dropzone-icon" />
className={classes.dropzoneIcon} <Typography variant="h6" className="dropzone-text">
style={{ fontSize: "4rem" }}
/>
<Typography variant="h6" className={classes.dropzoneText}>
Drag and drop a CSV file here or click to select Drag and drop a CSV file here or click to select
</Typography> </Typography>
</> </>
)} )}
</div> </div>
<Grid container justify="center" className={classes.progressContainer}> <Grid container justify="center" className="progress-container">
{uploading ? ( {uploading ? (
<CircularProgress /> <CircularProgress />
) : ( ) : (
<Button <Button
className={classes.submitButton} className="submit-button"
variant="contained" variant="contained"
onClick={handleUpload} onClick={handleUpload}
disabled={!username && !file} disabled={!username && !file}
@@ -221,7 +122,7 @@ const UploadDiary = () => {
)} )}
</Grid> </Grid>
</div> </div>
<Typography variant="body1" className={classes.overviewText}> <Typography variant="body1" className="overview-text">
This tool allows you to easily import your Letterboxd diary and select This tool allows you to easily import your Letterboxd diary and select
your favorite movie poster. Follow these simple steps: your favorite movie poster. Follow these simple steps:
<br /> <br />

View File

@@ -1,3 +1,8 @@
@use "./scss/uploadDiary";
@use "./scss/posterSelector";
@use "./scss/posterGallery";
@use "./scss/cart";
.App { .App {
text-align: center; text-align: center;
} }

View File

@@ -16,6 +16,7 @@ code {
:root { :root {
--background: #fff; --background: #fff;
--bgComp: #1c1f23;
--foreground: #2c3e50; --foreground: #2c3e50;
--primary: #1caff2; --primary: #1caff2;
--secondary: #000; --secondary: #000;

166
src/styles/scss/_cart.scss Normal file
View File

@@ -0,0 +1,166 @@
// Variables
$primary-color: #00a346;
$secondary-color: #3498db;
$background-dark: #2c3e50;
$background-light: #34495e;
$text-white: #ecf0f1;
$text-muted: #95a5a6;
$hover-scale: 1.05;
$gap-spacing: 1rem;
.cart-container {
margin-top: 2rem;
margin-bottom: 4rem;
display: flex;
gap: $gap-spacing;
.poster-list {
flex: 2;
margin-right: 2rem;
max-height: calc(100vh - 200px);
overflow-y: auto;
.cart-title {
color: $text-white;
margin-bottom: 1rem;
font-size: 2rem;
font-weight: 400;
}
.poster-card {
display: flex;
margin-bottom: 1rem;
background-color: $background-dark;
border-radius: 4px;
overflow: hidden;
.poster-media {
width: 120px;
height: 180px;
object-fit: cover;
border-radius: 4px 0 0 4px;
}
.poster-content {
flex-grow: 1;
padding: 1rem;
display: flex;
flex-direction: column;
justify-content: space-between;
.poster-title {
color: $text-white;
font-size: 1.25rem;
margin-bottom: 0.5rem;
}
.poster-id {
color: $text-muted;
font-size: 1rem;
margin-bottom: 1rem;
}
.poster-actions {
display: flex;
justify-content: space-between;
gap: 0.5rem;
.action-button {
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 0.5rem;
border-radius: 4px;
background-color: transparent;
border: 1px solid $text-muted;
color: $text-white;
transition: background-color 0.3s ease;
&:hover {
background-color: $background-light;
cursor: pointer;
}
&:not(:last-child) {
margin-right: 0.5rem;
}
}
}
}
}
}
.action-panel {
flex: 1;
position: sticky;
top: 2rem;
height: fit-content;
padding: 1.5rem;
background-color: $background-light;
border-radius: 8px;
.action-title {
color: $text-white;
margin-bottom: 1rem;
font-size: 1.5rem;
}
.form-control {
width: 100%;
margin-bottom: 1.5rem;
color: $text-white;
select {
background-color: $background-dark;
color: $text-white;
padding: 0.5rem;
border: 1px solid $text-muted;
border-radius: 4px;
}
}
.action-buttons {
display: flex;
flex-direction: column;
gap: 1rem;
.primary-button,
.secondary-button {
width: 100%;
padding: 0.75rem;
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: bold;
transition: background-color 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
&.primary-button {
background-color: $primary-color;
color: $text-white;
&:hover {
background-color: darken($primary-color, 10%);
}
&:disabled {
background-color: darken($primary-color, 20%);
cursor: not-allowed;
}
}
&.secondary-button {
background-color: $secondary-color;
color: $text-white;
&:hover {
background-color: darken($secondary-color, 10%);
}
}
}
}
}
}

View File

@@ -0,0 +1,70 @@
// Variables
$primary-color: #00a346;
$background-dark: #1c1f23;
$text-white: #ffffff;
$hover-scale: 1.05;
$loading-height: 50vh;
.poster-gallery {
margin-top: 2rem;
margin-bottom: 2rem;
.content-paper {
padding: 1.5rem;
border-radius: 4px;
background-color: $background-dark;
}
.title {
color: $text-white;
margin-bottom: 1.5rem;
font-size: 2rem;
font-weight: 400;
}
.poster-card {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
background-color: transparent;
box-shadow: none;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
transform: scale($hover-scale);
}
&.selected-poster {
border: 4px solid $primary-color;
border-radius: 4px;
}
.poster-image {
padding-top: 150%;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
border-radius: 4px;
transition: transform 0.3s ease-in-out;
}
.check-icon {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background-color: $primary-color;
color: $text-white;
border-radius: 50%;
padding: 0.25rem;
}
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: $loading-height;
}
}

View File

@@ -0,0 +1,192 @@
// Variables
$primary-color: #00a346;
$secondary-color: #667788;
$background-dark: #1c1f23;
$text-white: #ffffff;
$text-gray: #667788;
$border-color: #667788;
$hover-color: rgba(102, 119, 136, 0.2);
.poster-selector {
padding: 0 2rem;
max-width: 100% !important; // Override MUI Container
margin: 2rem auto 4rem !important;
.content-paper {
padding: 1.5rem;
border-radius: 4px;
background-color: $background-dark !important;
}
.title {
margin-bottom: 1.5rem;
color: $text-white;
}
.username {
color: $primary-color;
margin-bottom: 1rem;
}
.movies-list {
max-height: 800px;
min-height: 900px;
padding: 0;
}
.movie-item {
margin-bottom: 1rem;
transition: background-color 0.3s ease;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: nowrap;
&:hover {
background-color: $hover-color;
}
&:not(:last-child) {
border-bottom: 1px solid $border-color;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
// Override MUI ListItem styles
&.MuiListItem-root {
padding: 0.5rem !important;
}
.MuiGrid-container {
display: flex;
justify-content: space-between;
width: 100%;
}
.movie-poster {
width: 50px;
height: 75px;
object-fit: cover;
margin-right: 1rem;
border-radius: 4px;
}
.movie-info {
flex-grow: 1;
max-width: calc(100% - 110px);
}
.movie-title {
color: $text-white;
font-family: "TiemposTextWeb-Semibold, Georgia, serif";
font-size: 1.38461538rem;
font-weight: 400;
transition: color 0.3s ease;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
color: $primary-color;
}
}
.movie-year {
color: $text-gray;
}
.watched-date {
color: $text-gray;
}
.watched-day {
font-size: 2rem;
line-height: 1;
margin-bottom: 0.25rem;
}
.watched-month {
font-size: 0.875rem;
}
}
.pagination-container {
display: flex;
justify-content: center;
margin-top: 1.5rem;
// Override MUI Pagination styles
.MuiPagination-root {
.MuiPaginationItem-root {
color: $text-white;
&.Mui-selected {
background-color: $secondary-color;
}
}
}
}
.progress-container {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: 200px;
.MuiLinearProgress-root {
width: 100%;
margin-bottom: 1rem;
}
}
.progress-label {
margin-top: 1rem;
color: $text-white;
}
.no-movies-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400px;
}
.gif-container {
display: flex;
justify-content: center;
position: relative;
width: 200px;
height: 200px;
margin-bottom: 1rem;
}
.circle-background {
position: absolute;
width: 120%;
height: 120%;
border-radius: 50%;
background-color: $text-white;
clip-path: inset(0 0 25% 0);
bottom: -28%;
}
.reaction-gif {
position: absolute;
width: 100%;
height: 100%;
object-fit: contain;
}
.back-button {
margin-top: 1rem;
background-color: $primary-color !important;
color: $text-white !important;
transition: background-color 0.3s ease;
&:hover {
background-color: darken($primary-color, 10%) !important;
}
}
}

View File

@@ -0,0 +1,112 @@
.upload-diary {
padding: 3rem;
background-color: #14181c;
min-height: 100vh;
display: flex !important;
flex-direction: column;
align-items: center;
justify-content: center;
.upload-card {
width: 100%;
max-width: 600px;
background-color: #1c1f23;
border-radius: 4px;
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2);
padding: 2rem;
.title {
margin-bottom: 1.5rem;
font-weight: bold;
color: #ffffff;
}
.username-field {
margin-bottom: 1.5rem;
.MuiOutlinedInput-root {
color: #ffffff;
fieldset {
border-color: #00a346 !important;
}
&:hover fieldset {
border-color: #00a346 !important;
}
&.Mui-focused fieldset {
border-color: #1caff2 !important;
}
}
.MuiInputLabel-root {
color: #ffffff !important;
}
}
.or-text {
color: #ffffff;
margin-bottom: 1rem;
}
.dropzone {
border: 2px dashed #00a346;
border-radius: 4px;
padding: 2rem;
text-align: center;
cursor: pointer;
&:hover {
background-color: #1f252a;
}
.dropzone-icon {
margin-bottom: 1rem;
color: #00a346;
font-size: 4rem;
}
.dropzone-text {
margin-bottom: 1rem;
color: #ffffff;
}
}
.progress-container {
margin-top: 1rem;
}
.submit-button {
color: #fff;
font-size: 0.8rem;
font-weight: 900;
text-transform: uppercase;
letter-spacing: 0.04em;
line-height: 2.8rem;
display: inline-block;
cursor: pointer;
padding: 0 1rem;
border: 0;
border-radius: 4px;
outline: none;
background: #526e89;
transition: background-color 0.3s ease;
&:hover {
background-color: #1caff2;
}
&:disabled {
opacity: 0.7;
cursor: not-allowed;
}
}
}
.overview-text {
color: #456;
margin-top: 2rem;
padding: 1.5rem;
}
}