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,71 +1,82 @@
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",
}, },
})); }));
@@ -75,18 +86,25 @@ const Cart = () => {
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 [selectedForDownload, setSelectedForDownload] = useState(
const [downloadFormat, setDownloadFormat] = useState('zip'); selectedPosters.map(() => true)
);
const [downloadFormat, setDownloadFormat] = useState("zip");
const [renameDialogOpen, setRenameDialogOpen] = useState(false); const [renameDialogOpen, setRenameDialogOpen] = useState(false);
const [currentPosterIndex, setCurrentPosterIndex] = useState(null); const [currentPosterIndex, setCurrentPosterIndex] = useState(null);
const [newPosterName, setNewPosterName] = useState(''); const [newPosterName, setNewPosterName] = useState("");
const handleRemovePoster = (index) => { const handleRemovePoster = (index) => {
dispatch(removePoster(selectedPosters[index].movieId, selectedPosters[index].posterId)); dispatch(
removePoster(
selectedPosters[index].movieId,
selectedPosters[index].posterId
)
);
}; };
const handleDownload = async () => { const handleDownload = async () => {
@@ -96,31 +114,37 @@ const Cart = () => {
movieId: poster.movieId, movieId: poster.movieId,
posterId: poster.posterId, posterId: poster.posterId,
movieName: poster.movieName, movieName: poster.movieName,
movieYear: poster.movieYear movieYear: poster.movieYear,
})); }));
console.log('Downloading posters:', postersToDownload); console.log("Downloading posters:", postersToDownload);
try { try {
const response = await axios.post('http://localhost:5000/api/download-posters', { const response = await axios.post(
"http://localhost:5000/api/download-posters",
{
posters: postersToDownload, posters: postersToDownload,
format: downloadFormat, format: downloadFormat,
}, { },
responseType: 'blob', {
}); responseType: "blob",
}
);
const blob = new Blob([response.data], { type: response.headers['content-type'] }); const blob = new Blob([response.data], {
const link = document.createElement('a'); type: response.headers["content-type"],
});
const link = document.createElement("a");
link.href = window.URL.createObjectURL(blob); link.href = window.URL.createObjectURL(blob);
link.download = `posters.${downloadFormat}`; link.download = `posters.${downloadFormat}`;
link.click(); link.click();
} catch (error) { } catch (error) {
console.error('Failed to download posters:', error); console.error("Failed to download posters:", error);
} }
}; };
const handleShare = () => { const handleShare = () => {
console.log('Sharing selected posters'); console.log("Sharing selected posters");
}; };
const handleRename = (index) => { const handleRename = (index) => {
@@ -135,21 +159,25 @@ const Cart = () => {
}; };
return ( return (
<Container className={classes.root}> <Container className="cart-container">
<div className={classes.posterList}> <div className="poster-list">
<Typography variant="h4" gutterBottom className={classes.typography}>Your Cart</Typography> <Typography variant="h4" gutterBottom className="cart-title">
Your Cart
</Typography>
{selectedPosters.map((poster, index) => ( {selectedPosters.map((poster, index) => (
<Card key={index} className={classes.card}> <Card key={index} className="poster-card">
<CardMedia <CardMedia
className={classes.cardMedia} className="poster-media"
image={`https://image.tmdb.org/t/p/w500${poster.posterId}`} image={`https://image.tmdb.org/t/p/w500${poster.posterId}`}
title={`Poster ${index + 1}`} title={`Poster ${index + 1}`}
/> />
<CardContent className={classes.cardContent}> <CardContent className="poster-content">
<Typography variant="h6" <Typography variant="h6" className="poster-title">
className={classes.typography}>{`Movie ID: ${poster.movieId}`}</Typography> {`Movie ID: ${poster.movieId}`}
<Typography variant="body2" </Typography>
className={classes.typography}>{poster.posterId}</Typography> <Typography variant="body2" className="poster-id">
{poster.posterId}
</Typography>
<FormControlLabel <FormControlLabel
control={ control={
<Checkbox <Checkbox
@@ -159,23 +187,34 @@ const Cart = () => {
newSelected[index] = e.target.checked; newSelected[index] = e.target.checked;
setSelectedForDownload(newSelected); setSelectedForDownload(newSelected);
}} }}
className={classes.checkbox} className="checkbox"
/> />
} }
label="Select for download" label="Select for download"
className={classes.typography}
/> />
<Button startIcon={<DeleteIcon/>} onClick={() => handleRemovePoster(index)} <Button
className={classes.typography}>Remove</Button> startIcon={<DeleteIcon />}
<Button startIcon={<EditIcon/>} onClick={() => handleRename(index)} onClick={() => handleRemovePoster(index)}
className={classes.typography}>Rename</Button> className="action-button"
>
Remove
</Button>
<Button
startIcon={<EditIcon />}
onClick={() => handleRename(index)}
className="action-button"
>
Rename
</Button>
</CardContent> </CardContent>
</Card> </Card>
))} ))}
</div> </div>
<Paper className={classes.actionPanel} elevation={3}> <Paper className="action-panel" elevation={3}>
<div className={classes.actions}> <div className="action-buttons">
<Typography gutterBottom>Download Format</Typography> <Typography gutterBottom className="action-title">
Download Format
</Typography>
<TextField <TextField
select select
value={downloadFormat} value={downloadFormat}
@@ -183,7 +222,7 @@ const Cart = () => {
SelectProps={{ SelectProps={{
native: true, native: true,
}} }}
className={classes.formControl} className="form-control"
> >
<option value="zip">ZIP</option> <option value="zip">ZIP</option>
<option value="tar">TAR</option> <option value="tar">TAR</option>
@@ -192,27 +231,31 @@ const Cart = () => {
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
startIcon={<GetAppIcon/>} startIcon={<GetAppIcon />}
onClick={handleDownload} onClick={handleDownload}
disabled={selectedForDownload.every((selected) => !selected)} disabled={selectedForDownload.every((selected) => !selected)}
className={classes.button} className="primary-button"
> >
Download Selected Download Selected
</Button> </Button>
<Button <Button
variant="contained" variant="contained"
color="secondary" color="secondary"
startIcon={<ShareIcon/>} startIcon={<ShareIcon />}
onClick={handleShare} onClick={handleShare}
className={classes.button} className="secondary-button"
> >
Share Selection Share Selection
</Button> </Button>
</div> </div>
</Paper> </Paper>
<Dialog open={renameDialogOpen} onClose={() => setRenameDialogOpen(false)}> <Dialog
<DialogTitle>Rename Poster</DialogTitle> open={renameDialogOpen}
<DialogContent> onClose={() => setRenameDialogOpen(false)}
className="rename-dialog"
>
<DialogTitle className="dialog-title">Rename Poster</DialogTitle>
<DialogContent className="dialog-content">
<TextField <TextField
autoFocus autoFocus
margin="dense" margin="dense"
@@ -223,7 +266,7 @@ const Cart = () => {
onChange={(e) => setNewPosterName(e.target.value)} onChange={(e) => setNewPosterName(e.target.value)}
/> />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions className="dialog-actions">
<Button onClick={() => setRenameDialogOpen(false)} color="primary"> <Button onClick={() => setRenameDialogOpen(false)} color="primary">
Cancel Cancel
</Button> </Button>

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,13 +115,13 @@ 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>
) : ( ) : (
@@ -129,20 +129,20 @@ const PosterGallery = ({ movieId }) => {
{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>

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,32 +144,34 @@ 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>
@@ -302,26 +181,19 @@ const PosterSelector = () => {
`/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"
className={classes.movieTitle}
>
{movie.Name} {movie.Name}
</Typography> </Typography>
<Typography <Typography variant="body2" className="movie-year">
variant="body2"
color="textSecondary"
className={classes.movieYear}
>
{movie.Year} {movie.Year}
</Typography> </Typography>
</Grid> </Grid>
<Grid item> <Grid item>
<Box className={classes.watchedDate}> <Box className="watched-date">
{(() => { {(() => {
const { day, month } = formatWatchedDate( const { day, month } = formatWatchedDate(
movie["Watched Date"] movie["Watched Date"]
@@ -330,13 +202,13 @@ const PosterSelector = () => {
<> <>
<Typography <Typography
variant="body2" variant="body2"
className={classes.watchedDay} className="watched-day"
> >
{day} {day}
</Typography> </Typography>
<Typography <Typography
variant="body2" variant="body2"
className={classes.watchedMonth} className="watched-month"
> >
{month} {month}
</Typography> </Typography>
@@ -349,7 +221,7 @@ const PosterSelector = () => {
</ListItem> </ListItem>
))} ))}
</List> </List>
<Box className={classes.paginationContainer}> <Box className="pagination-container">
<ThemeProvider theme={paginationTheme}> <ThemeProvider theme={paginationTheme}>
<Pagination <Pagination
count={totalPages} count={totalPages}
@@ -361,13 +233,13 @@ const PosterSelector = () => {
</Box> </Box>
</> </>
) : ( ) : (
<Box className={classes.noMoviesContainer}> <Box className="no-movies-container">
<div className={classes.gifContainer}> <div className="gif-container">
<div className={classes.circleBackground}></div> <div className="circle-background"></div>
<img <img
src={pulpGif} src={pulpGif}
alt="Confused reaction" alt="Confused reaction"
className={classes.gif} className="reaction-gif"
/> />
</div> </div>
<Typography variant="body1" gutterBottom> <Typography variant="body1" gutterBottom>
@@ -376,7 +248,7 @@ const PosterSelector = () => {
</Typography> </Typography>
<Button <Button
variant="contained" variant="contained"
className={classes.backButton} className="back-button"
onClick={handleGoBack} onClick={handleGoBack}
> >
Go Back Go Back

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