diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
new file mode 100644
index 0000000..a09493b
--- /dev/null
+++ b/.gitea/workflows/deploy.yml
@@ -0,0 +1,34 @@
+name: Deploy Frontend
+
+on:
+ push:
+ tags:
+ - 'v*.*.*'
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Extract version tag
+ run: echo "VERSION_TAG=${GITHUB_REF_NAME}" >> $GITHUB_ENV
+
+ - name: Build Docker image
+ run: |
+ docker build \
+ --build-arg REACT_APP_API_URL="" \
+ -t poster-picker-frontend:latest \
+ -t poster-picker-frontend:${{ env.VERSION_TAG }} .
+
+ - name: Stop old container
+ run: |
+ cd ~/dev-server/poster-picker
+ docker compose stop frontend || true
+
+ - name: Start new container
+ run: |
+ cd ~/dev-server/poster-picker
+ docker compose up -d --no-build frontend
diff --git a/.gitignore b/.gitignore
index 8070e75..c56f793 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,6 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+
+*storybook.log
+storybook-static
diff --git a/Dockerfile b/Dockerfile
index 5bcac21..25c61e0 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -5,7 +5,7 @@ WORKDIR /app
COPY package*.json ./
-ENV NODE_OPTIONS="--max-old-space-size=512"
+ENV NODE_OPTIONS="--max-old-space-size=2048"
RUN npm ci --silent --legacy-peer-deps
COPY . .
@@ -22,7 +22,7 @@ FROM nginx:stable-alpine AS runner
# Copy build output
COPY --from=builder /app/build /usr/share/nginx/html
-COPY nginx-spa.conf /etc/nginx/conf.d/nginx-spa.conf
+COPY nginx-spa.conf /etc/nginx/conf.d/default.conf
# Expose port 80 (Caddy fera reverse proxy)
EXPOSE 80
diff --git a/README.md b/README.md
index 4de716c..281acd7 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,18 @@
-# Movie Poster Gallery
+# Letterboxd Diary Poster Picker
-
+
-A web application that allows users to browse and select their favorite movie posters based on their Letterboxd diary.
+A web application to browse and select your favorite movie posters from your Letterboxd diary.
## Features
-- **Letterboxd Integration**: Import your movie diary directly from uploading a CSV file.
-- **Poster Gallery**: Browse through posters of movies you've watched.
-- **User-friendly Interface**: Intuitive design for easy navigation and selection.
-- **Responsive Design**: Works seamlessly on desktop and mobile devices.
+- **CSV Import**: Upload your Letterboxd diary export to load your full watch history.
+- **RSS Sync**: Sync your ~50 most recent diary entries directly via your Letterboxd username — no export needed.
+- **Poster Gallery**: Browse all available posters for each movie via TMDB.
+- **Poster Selection**: Pick your favorite poster for each film.
+- **Selection Recap**: Review all your selections in one place.
+- **ZIP Download**: Download all selected posters as a ZIP file.
+- **Responsive Design**: Works on desktop and mobile.
## Getting Started
@@ -17,6 +20,7 @@ A web application that allows users to browse and select their favorite movie po
- Node.js (v14.0.0 or later)
- npm (v6.0.0 or later)
+- A running instance of the [backend](https://github.com/Hugyouu/Letterboxd-Diary-Posters-Picker-backend)
### Installation
@@ -48,18 +52,19 @@ A web application that allows users to browse and select their favorite movie po
## Usage
-1. On the homepage, upload your diary CSV file.
-2. Once processed, you'll be redirected to the Poster Selector page.
-3. Browse through your watched movies and select your favorite posters.
-4. (Add any additional steps or features here)
+1. **Import your diary** — either upload a `diary.csv` exported from [letterboxd.com/data/export](https://letterboxd.com/data/export/), or enter your Letterboxd username to sync via RSS.
+2. **Browse posters** — for each film in your diary, scroll through the available posters fetched from TMDB.
+3. **Select a poster** — click to pick your favorite for each movie.
+4. **Download** — use the download button in the header to export all selected posters as a ZIP.
-## Technologies Used
+## Technologies
-- React.js
-- Material-UI
-- Backend private for now
+- React / Redux
+- Material UI
+- Flask (backend)
+- TMDB API
## Acknowledgments
-- [Letterboxd](https://letterboxd.com/) for the inspiration and data (diary.csv)
-- [The Movie Database (TMDb)](https://www.themoviedb.org/) for the movie poster images
+- [Letterboxd](https://letterboxd.com/) for the diary data
+- [The Movie Database (TMDb)](https://www.themoviedb.org/) for the poster images
diff --git a/assets/project.jpg b/assets/project.jpg
deleted file mode 100644
index a522d82..0000000
Binary files a/assets/project.jpg and /dev/null differ
diff --git a/assets/project.png b/assets/project.png
new file mode 100644
index 0000000..0fc443c
Binary files /dev/null and b/assets/project.png differ
diff --git a/nginx-spa.conf b/nginx-spa.conf
index e9879c4..3c29bef 100644
--- a/nginx-spa.conf
+++ b/nginx-spa.conf
@@ -6,7 +6,7 @@ server {
# Proxy API to backend container
location /api/ {
- proxy_pass http://poster-picker-backend-1:5000/api/;
+ proxy_pass http://backend:5000/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
diff --git a/package-lock.json b/package-lock.json
index a7e500a..6f109f3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
- "name": "frontend",
- "version": "0.1.0",
+ "name": "poster-picker",
+ "version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "frontend",
- "version": "0.1.0",
+ "name": "poster-picker",
+ "version": "1.0.0",
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
@@ -19,6 +19,7 @@
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.7.6",
+ "jszip": "^3.10.1",
"lucide-react": "^0.453.0",
"material-ui-dropzone": "^3.5.0",
"query-string": "^9.1.0",
@@ -56,57 +57,45 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/@ampproject/remapping": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
- "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
- "license": "Apache-2.0",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.24"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
"node_modules/@babel/code-frame": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz",
- "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
"license": "MIT",
"dependencies": {
- "@babel/highlight": "^7.24.7",
- "picocolors": "^1.0.0"
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/compat-data": {
- "version": "7.25.4",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz",
- "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
- "version": "7.25.2",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz",
- "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"license": "MIT",
"dependencies": {
- "@ampproject/remapping": "^2.2.0",
- "@babel/code-frame": "^7.24.7",
- "@babel/generator": "^7.25.0",
- "@babel/helper-compilation-targets": "^7.25.2",
- "@babel/helper-module-transforms": "^7.25.2",
- "@babel/helpers": "^7.25.0",
- "@babel/parser": "^7.25.0",
- "@babel/template": "^7.25.0",
- "@babel/traverse": "^7.25.2",
- "@babel/types": "^7.25.2",
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -167,15 +156,16 @@
}
},
"node_modules/@babel/generator": {
- "version": "7.25.6",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz",
- "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==",
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.25.6",
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.25",
- "jsesc": "^2.5.1"
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
},
"engines": {
"node": ">=6.9.0"
@@ -207,14 +197,14 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
- "version": "7.25.2",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz",
- "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
"license": "MIT",
"dependencies": {
- "@babel/compat-data": "^7.25.2",
- "@babel/helper-validator-option": "^7.24.8",
- "browserslist": "^4.23.1",
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
},
@@ -303,6 +293,15 @@
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
}
},
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/helper-member-expression-to-functions": {
"version": "7.24.8",
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz",
@@ -317,28 +316,27 @@
}
},
"node_modules/@babel/helper-module-imports": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz",
- "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
"license": "MIT",
"dependencies": {
- "@babel/traverse": "^7.24.7",
- "@babel/types": "^7.24.7"
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
- "version": "7.25.2",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz",
- "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
"license": "MIT",
"dependencies": {
- "@babel/helper-module-imports": "^7.24.7",
- "@babel/helper-simple-access": "^7.24.7",
- "@babel/helper-validator-identifier": "^7.24.7",
- "@babel/traverse": "^7.25.2"
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
@@ -429,27 +427,27 @@
}
},
"node_modules/@babel/helper-string-parser": {
- "version": "7.24.8",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
- "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
- "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==",
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-option": {
- "version": "7.24.8",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz",
- "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -470,40 +468,25 @@
}
},
"node_modules/@babel/helpers": {
- "version": "7.25.6",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz",
- "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==",
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
"license": "MIT",
"dependencies": {
- "@babel/template": "^7.25.0",
- "@babel/types": "^7.25.6"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/highlight": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz",
- "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-validator-identifier": "^7.24.7",
- "chalk": "^2.4.2",
- "js-tokens": "^4.0.0",
- "picocolors": "^1.0.0"
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
- "version": "7.25.6",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz",
- "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==",
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.25.6"
+ "@babel/types": "^7.29.0"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -2139,46 +2122,45 @@
}
},
"node_modules/@babel/template": {
- "version": "7.25.0",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz",
- "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.24.7",
- "@babel/parser": "^7.25.0",
- "@babel/types": "^7.25.0"
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
- "version": "7.25.6",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz",
- "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.24.7",
- "@babel/generator": "^7.25.6",
- "@babel/parser": "^7.25.6",
- "@babel/template": "^7.25.0",
- "@babel/types": "^7.25.6",
- "debug": "^4.3.1",
- "globals": "^11.1.0"
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
- "version": "7.25.6",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz",
- "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
"license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^7.24.8",
- "@babel/helper-validator-identifier": "^7.24.7",
- "to-fast-properties": "^2.0.0"
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
@@ -2649,16 +2631,19 @@
"license": "MIT"
},
"node_modules/@eslint-community/eslint-utils": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
- "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
"license": "MIT",
"dependencies": {
- "eslint-visitor-keys": "^3.3.0"
+ "eslint-visitor-keys": "^3.4.3"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
"peerDependencies": {
"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
}
@@ -3603,17 +3588,23 @@
}
},
"node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
- "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"license": "MIT",
"dependencies": {
- "@jridgewell/set-array": "^1.2.1",
- "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
- },
- "engines": {
- "node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
@@ -3625,15 +3616,6 @@
"node": ">=6.0.0"
}
},
- "node_modules/@jridgewell/set-array": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
- "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
"node_modules/@jridgewell/source-map": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
@@ -3645,15 +3627,15 @@
}
},
"node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
- "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.25",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
- "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -5260,12 +5242,12 @@
}
},
"node_modules/@types/babel__traverse": {
- "version": "7.20.6",
- "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz",
- "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==",
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.20.7"
+ "@babel/types": "^7.28.2"
}
},
"node_modules/@types/body-parser": {
@@ -5316,10 +5298,20 @@
"@types/json-schema": "*"
}
},
+ "node_modules/@types/eslint-scope": {
+ "version": "3.7.7",
+ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
+ "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/eslint": "*",
+ "@types/estree": "*"
+ }
+ },
"node_modules/@types/estree": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
- "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
"node_modules/@types/express": {
@@ -5772,9 +5764,9 @@
"license": "MIT"
},
"node_modules/@types/semver": {
- "version": "7.5.8",
- "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
- "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
+ "version": "7.7.1",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz",
+ "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==",
"license": "MIT"
},
"node_modules/@types/send": {
@@ -6103,148 +6095,148 @@
"license": "ISC"
},
"node_modules/@webassemblyjs/ast": {
- "version": "1.12.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz",
- "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==",
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
+ "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
"license": "MIT",
"dependencies": {
- "@webassemblyjs/helper-numbers": "1.11.6",
- "@webassemblyjs/helper-wasm-bytecode": "1.11.6"
+ "@webassemblyjs/helper-numbers": "1.13.2",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2"
}
},
"node_modules/@webassemblyjs/floating-point-hex-parser": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz",
- "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==",
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
+ "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-api-error": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz",
- "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==",
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
+ "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-buffer": {
- "version": "1.12.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz",
- "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==",
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
+ "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-numbers": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz",
- "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==",
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz",
+ "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
"license": "MIT",
"dependencies": {
- "@webassemblyjs/floating-point-hex-parser": "1.11.6",
- "@webassemblyjs/helper-api-error": "1.11.6",
+ "@webassemblyjs/floating-point-hex-parser": "1.13.2",
+ "@webassemblyjs/helper-api-error": "1.13.2",
"@xtuc/long": "4.2.2"
}
},
"node_modules/@webassemblyjs/helper-wasm-bytecode": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz",
- "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==",
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
+ "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-wasm-section": {
- "version": "1.12.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz",
- "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==",
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz",
+ "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
"license": "MIT",
"dependencies": {
- "@webassemblyjs/ast": "1.12.1",
- "@webassemblyjs/helper-buffer": "1.12.1",
- "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
- "@webassemblyjs/wasm-gen": "1.12.1"
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/wasm-gen": "1.14.1"
}
},
"node_modules/@webassemblyjs/ieee754": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz",
- "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==",
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz",
+ "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
"license": "MIT",
"dependencies": {
"@xtuc/ieee754": "^1.2.0"
}
},
"node_modules/@webassemblyjs/leb128": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz",
- "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==",
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz",
+ "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
"license": "Apache-2.0",
"dependencies": {
"@xtuc/long": "4.2.2"
}
},
"node_modules/@webassemblyjs/utf8": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz",
- "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==",
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
+ "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
"license": "MIT"
},
"node_modules/@webassemblyjs/wasm-edit": {
- "version": "1.12.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz",
- "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==",
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz",
+ "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
"license": "MIT",
"dependencies": {
- "@webassemblyjs/ast": "1.12.1",
- "@webassemblyjs/helper-buffer": "1.12.1",
- "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
- "@webassemblyjs/helper-wasm-section": "1.12.1",
- "@webassemblyjs/wasm-gen": "1.12.1",
- "@webassemblyjs/wasm-opt": "1.12.1",
- "@webassemblyjs/wasm-parser": "1.12.1",
- "@webassemblyjs/wast-printer": "1.12.1"
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/helper-wasm-section": "1.14.1",
+ "@webassemblyjs/wasm-gen": "1.14.1",
+ "@webassemblyjs/wasm-opt": "1.14.1",
+ "@webassemblyjs/wasm-parser": "1.14.1",
+ "@webassemblyjs/wast-printer": "1.14.1"
}
},
"node_modules/@webassemblyjs/wasm-gen": {
- "version": "1.12.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz",
- "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==",
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz",
+ "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
"license": "MIT",
"dependencies": {
- "@webassemblyjs/ast": "1.12.1",
- "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
- "@webassemblyjs/ieee754": "1.11.6",
- "@webassemblyjs/leb128": "1.11.6",
- "@webassemblyjs/utf8": "1.11.6"
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/ieee754": "1.13.2",
+ "@webassemblyjs/leb128": "1.13.2",
+ "@webassemblyjs/utf8": "1.13.2"
}
},
"node_modules/@webassemblyjs/wasm-opt": {
- "version": "1.12.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz",
- "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==",
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz",
+ "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
"license": "MIT",
"dependencies": {
- "@webassemblyjs/ast": "1.12.1",
- "@webassemblyjs/helper-buffer": "1.12.1",
- "@webassemblyjs/wasm-gen": "1.12.1",
- "@webassemblyjs/wasm-parser": "1.12.1"
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/wasm-gen": "1.14.1",
+ "@webassemblyjs/wasm-parser": "1.14.1"
}
},
"node_modules/@webassemblyjs/wasm-parser": {
- "version": "1.12.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz",
- "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==",
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz",
+ "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
"license": "MIT",
"dependencies": {
- "@webassemblyjs/ast": "1.12.1",
- "@webassemblyjs/helper-api-error": "1.11.6",
- "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
- "@webassemblyjs/ieee754": "1.11.6",
- "@webassemblyjs/leb128": "1.11.6",
- "@webassemblyjs/utf8": "1.11.6"
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-api-error": "1.13.2",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/ieee754": "1.13.2",
+ "@webassemblyjs/leb128": "1.13.2",
+ "@webassemblyjs/utf8": "1.13.2"
}
},
"node_modules/@webassemblyjs/wast-printer": {
- "version": "1.12.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz",
- "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==",
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz",
+ "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
"license": "MIT",
"dependencies": {
- "@webassemblyjs/ast": "1.12.1",
+ "@webassemblyjs/ast": "1.14.1",
"@xtuc/long": "4.2.2"
}
},
@@ -6281,9 +6273,9 @@
}
},
"node_modules/acorn": {
- "version": "8.12.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
- "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@@ -6314,13 +6306,16 @@
"node": ">=0.4.0"
}
},
- "node_modules/acorn-import-attributes": {
- "version": "1.9.5",
- "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
- "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==",
+ "node_modules/acorn-import-phases": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
+ "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
"license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ },
"peerDependencies": {
- "acorn": "^8"
+ "acorn": "^8.14.0"
}
},
"node_modules/acorn-jsx": {
@@ -7175,6 +7170,18 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.21",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz",
+ "integrity": "sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==",
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/batch": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
@@ -7329,9 +7336,9 @@
"license": "BSD-2-Clause"
},
"node_modules/browserslist": {
- "version": "4.23.3",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
- "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
"funding": [
{
"type": "opencollective",
@@ -7348,10 +7355,11 @@
],
"license": "MIT",
"dependencies": {
- "caniuse-lite": "^1.0.30001646",
- "electron-to-chromium": "^1.5.4",
- "node-releases": "^2.0.18",
- "update-browserslist-db": "^1.1.0"
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
},
"bin": {
"browserslist": "cli.js"
@@ -7468,9 +7476,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001655",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001655.tgz",
- "integrity": "sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg==",
+ "version": "1.0.30001790",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz",
+ "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==",
"funding": [
{
"type": "opencollective",
@@ -8382,12 +8390,12 @@
}
},
"node_modules/debug": {
- "version": "4.3.6",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
- "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
- "ms": "2.1.2"
+ "ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
@@ -8826,9 +8834,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.13",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz",
- "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==",
+ "version": "1.5.344",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz",
+ "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==",
"license": "ISC"
},
"node_modules/email-addresses": {
@@ -8875,13 +8883,13 @@
}
},
"node_modules/enhanced-resolve": {
- "version": "5.17.1",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
- "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==",
+ "version": "5.21.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz",
+ "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
- "tapable": "^2.2.0"
+ "tapable": "^2.3.3"
},
"engines": {
"node": ">=10.13.0"
@@ -9046,12 +9054,6 @@
"node": ">= 0.4"
}
},
- "node_modules/es-module-lexer": {
- "version": "1.5.4",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
- "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
- "license": "MIT"
- },
"node_modules/es-object-atoms": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
@@ -11399,6 +11401,12 @@
"node": ">= 4"
}
},
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
+ "license": "MIT"
+ },
"node_modules/immer": {
"version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
@@ -14375,15 +14383,15 @@
}
},
"node_modules/jsesc": {
- "version": "2.5.2",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
- "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
},
"engines": {
- "node": ">=4"
+ "node": ">=6"
}
},
"node_modules/json-buffer": {
@@ -14577,6 +14585,54 @@
"node": ">=4.0"
}
},
+ "node_modules/jszip": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+ "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+ "license": "(MIT OR GPL-3.0-or-later)",
+ "dependencies": {
+ "lie": "~3.3.0",
+ "pako": "~1.0.2",
+ "readable-stream": "~2.3.6",
+ "setimmediate": "^1.0.5"
+ }
+ },
+ "node_modules/jszip/node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "license": "MIT"
+ },
+ "node_modules/jszip/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/jszip/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/jszip/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -14663,6 +14719,15 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
"node_modules/lilconfig": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
@@ -14679,12 +14744,16 @@
"license": "MIT"
},
"node_modules/loader-runner": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
- "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz",
+ "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==",
"license": "MIT",
"engines": {
"node": ">=6.11.5"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
}
},
"node_modules/loader-utils": {
@@ -15051,9 +15120,9 @@
}
},
"node_modules/ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/multicast-dns": {
@@ -15157,9 +15226,9 @@
"license": "MIT"
},
"node_modules/node-releases": {
- "version": "2.0.18",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
- "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
+ "version": "2.0.38",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
+ "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
"license": "MIT"
},
"node_modules/normalize-path": {
@@ -15519,6 +15588,12 @@
"integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==",
"license": "BlueOak-1.0.0"
},
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+ "license": "(MIT AND Zlib)"
+ },
"node_modules/param-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
@@ -18496,9 +18571,9 @@
}
},
"node_modules/semver": {
- "version": "7.6.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
- "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -18546,12 +18621,6 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
- "node_modules/send/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
"node_modules/serialize-javascript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
@@ -18686,6 +18755,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/setimmediate": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
+ "license": "MIT"
+ },
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -19621,12 +19696,16 @@
}
},
"node_modules/tapable": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
- "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
"license": "MIT",
"engines": {
"node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
}
},
"node_modules/temp-dir": {
@@ -19703,16 +19782,15 @@
}
},
"node_modules/terser-webpack-plugin": {
- "version": "5.3.10",
- "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz",
- "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==",
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.5.0.tgz",
+ "integrity": "sha512-UYhptBwhWvfIjKd/UuFo6D8uq9xpGLDK+z8EDsj/zWhrTaH34cKEbrkMKfV5YWqGBvAYA3tlzZbs2R+qYrbQJA==",
"license": "MIT",
"dependencies": {
- "@jridgewell/trace-mapping": "^0.3.20",
+ "@jridgewell/trace-mapping": "^0.3.25",
"jest-worker": "^27.4.5",
- "schema-utils": "^3.1.1",
- "serialize-javascript": "^6.0.1",
- "terser": "^5.26.0"
+ "schema-utils": "^4.3.0",
+ "terser": "^5.31.1"
},
"engines": {
"node": ">= 10.13.0"
@@ -19736,15 +19814,50 @@
}
}
},
- "node_modules/terser-webpack-plugin/node_modules/schema-utils": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
- "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
+ "node_modules/terser-webpack-plugin/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"license": "MIT",
"dependencies": {
- "@types/json-schema": "^7.0.8",
- "ajv": "^6.12.5",
- "ajv-keywords": "^3.5.2"
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
+ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3"
+ },
+ "peerDependencies": {
+ "ajv": "^8.8.2"
+ }
+ },
+ "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/terser-webpack-plugin/node_modules/schema-utils": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
+ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/json-schema": "^7.0.9",
+ "ajv": "^8.9.0",
+ "ajv-formats": "^2.1.1",
+ "ajv-keywords": "^5.1.0"
},
"engines": {
"node": ">= 10.13.0"
@@ -19825,15 +19938,6 @@
"integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
"license": "BSD-3-Clause"
},
- "node_modules/to-fast-properties": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
- "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -20218,9 +20322,9 @@
}
},
"node_modules/update-browserslist-db": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
- "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"funding": [
{
"type": "opencollective",
@@ -20237,8 +20341,8 @@
],
"license": "MIT",
"dependencies": {
- "escalade": "^3.1.2",
- "picocolors": "^1.0.1"
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
},
"bin": {
"update-browserslist-db": "cli.js"
@@ -20267,12 +20371,12 @@
}
},
"node_modules/use-sync-external-store": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
- "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util-deprecate": {
@@ -20381,9 +20485,9 @@
}
},
"node_modules/watchpack": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
- "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==",
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz",
+ "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==",
"license": "MIT",
"dependencies": {
"glob-to-regexp": "^0.4.1",
@@ -20418,34 +20522,35 @@
}
},
"node_modules/webpack": {
- "version": "5.94.0",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz",
- "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
+ "version": "5.106.2",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz",
+ "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==",
"license": "MIT",
"dependencies": {
- "@types/estree": "^1.0.5",
- "@webassemblyjs/ast": "^1.12.1",
- "@webassemblyjs/wasm-edit": "^1.12.1",
- "@webassemblyjs/wasm-parser": "^1.12.1",
- "acorn": "^8.7.1",
- "acorn-import-attributes": "^1.9.5",
- "browserslist": "^4.21.10",
+ "@types/eslint-scope": "^3.7.7",
+ "@types/estree": "^1.0.8",
+ "@types/json-schema": "^7.0.15",
+ "@webassemblyjs/ast": "^1.14.1",
+ "@webassemblyjs/wasm-edit": "^1.14.1",
+ "@webassemblyjs/wasm-parser": "^1.14.1",
+ "acorn": "^8.16.0",
+ "acorn-import-phases": "^1.0.3",
+ "browserslist": "^4.28.1",
"chrome-trace-event": "^1.0.2",
- "enhanced-resolve": "^5.17.1",
- "es-module-lexer": "^1.2.1",
+ "enhanced-resolve": "^5.20.0",
+ "es-module-lexer": "^2.0.0",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.2.11",
- "json-parse-even-better-errors": "^2.3.1",
- "loader-runner": "^4.2.0",
- "mime-types": "^2.1.27",
+ "loader-runner": "^4.3.1",
+ "mime-db": "^1.54.0",
"neo-async": "^2.6.2",
- "schema-utils": "^3.2.0",
- "tapable": "^2.1.1",
- "terser-webpack-plugin": "^5.3.10",
- "watchpack": "^2.4.1",
- "webpack-sources": "^3.2.3"
+ "schema-utils": "^4.3.3",
+ "tapable": "^2.3.0",
+ "terser-webpack-plugin": "^5.3.17",
+ "watchpack": "^2.5.1",
+ "webpack-sources": "^3.3.4"
},
"bin": {
"webpack": "bin/webpack.js"
@@ -20605,14 +20710,48 @@
}
},
"node_modules/webpack-sources": {
- "version": "3.2.3",
- "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
- "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.4.0.tgz",
+ "integrity": "sha512-gHwIe1cgBvvfLeu1Yz/dcFpmHfKDVxxyqI+kzqmuxZED81z2ChxpyqPaWcNqigPywhaEke7AjSGga+kxY55gjQ==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
+ "node_modules/webpack/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/webpack/node_modules/ajv-keywords": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
+ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3"
+ },
+ "peerDependencies": {
+ "ajv": "^8.8.2"
+ }
+ },
+ "node_modules/webpack/node_modules/es-module-lexer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
+ "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
+ "license": "MIT"
+ },
"node_modules/webpack/node_modules/eslint-scope": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
@@ -20635,15 +20774,31 @@
"node": ">=4.0"
}
},
+ "node_modules/webpack/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/webpack/node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/webpack/node_modules/schema-utils": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
- "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
+ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
"license": "MIT",
"dependencies": {
- "@types/json-schema": "^7.0.8",
- "ajv": "^6.12.5",
- "ajv-keywords": "^3.5.2"
+ "@types/json-schema": "^7.0.9",
+ "ajv": "^8.9.0",
+ "ajv-formats": "^2.1.1",
+ "ajv-keywords": "^5.1.0"
},
"engines": {
"node": ">= 10.13.0"
diff --git a/package.json b/package.json
index 318be52..c53a71f 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.7.6",
+ "jszip": "^3.10.1",
"lucide-react": "^0.453.0",
"material-ui-dropzone": "^3.5.0",
"query-string": "^9.1.0",
diff --git a/src/App.js b/src/App.js
index ce48591..1268e5b 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,33 +1,36 @@
-import React from "react";
import "./styles/App.scss";
-import { HashRouter as Router, Route, Routes } from "react-router-dom";
+import { HashRouter as Router, Route, Routes, useLocation } from "react-router-dom";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import { store, persistor } from "./services/store";
-import PosterSelector from "./components/PosterSelector";
-import PosterGallery from "./components/PosterGallery";
-import UploadDiary from "./components/UploadDiary";
-import Cart from "./components/Cart";
+import Header from "./components/Header";
+import PosterSelector from "./pages/PosterSelector";
+import PosterGallery from "./pages/PosterGallery";
+import UploadDiary from "./pages/UploadDiary";
+import SelectionRecap from "./pages/SelectionRecap";
+
+function AppContent() {
+ const location = useLocation();
+ return (
+
+ {location.pathname !== "/" && }
+
+ } />
+ } />
+ } />
+ } />
+
+
+ );
+}
function App() {
return (
- {
-
-
-
- } />
- } />
- }
- />
- } />
-
-
-
- }
+
+
+
);
diff --git a/src/components/Cart.js b/src/components/Cart.js
deleted file mode 100644
index d1829df..0000000
--- a/src/components/Cart.js
+++ /dev/null
@@ -1,204 +0,0 @@
-import React, { useState } from "react";
-import { useSelector, useDispatch } from "react-redux";
-import { useNavigate } from "react-router-dom";
-import { removeAllPosters, removePoster } from "../services/action";
-import DeleteIcon from "@material-ui/icons/Delete";
-import {
- Container,
- Box,
- Typography,
- Button,
- Dialog,
- DialogTitle,
- DialogContent,
- DialogContentText,
- DialogActions,
-} from "@material-ui/core";
-
-const apiUrl = process.env.REACT_APP_API_URL;
-
-const Cart = () => {
- const dispatch = useDispatch();
- const navigate = useNavigate();
- const selectedPosters = useSelector((state) => {
- const selections = state.posterSelections;
- return Object.entries(selections).flatMap(([movieId, posters]) =>
- posters.map((poster) => ({
- movieId,
- posterId: typeof poster === "string" ? poster : poster.posterId,
- watchedDate: poster.watchedDate,
- }))
- );
- });
-
- const [downloadFormat, setDownloadFormat] = useState("zip");
-
- // Nouvel état pour la confirmation de suppression globale
- const [openConfirmClear, setOpenConfirmClear] = useState(false);
-
- const handleRemovePoster = (movieId, posterId) => {
- if (movieId && posterId) dispatch(removePoster(movieId, posterId));
- };
-
- // Ouvre la dialog de confirmation (appelé par le bouton "Clear All Posters")
- const handleOpenConfirmClear = () => {
- setOpenConfirmClear(true);
- };
-
- // Ferme la dialog sans supprimer
- const handleCloseConfirmClear = () => {
- setOpenConfirmClear(false);
- };
-
- // Confirme et supprime tous les posters
- const handleConfirmClearAllPosters = () => {
- dispatch(removeAllPosters());
- setOpenConfirmClear(false);
- };
-
- const handleDownload = async () => {
- try {
- const response = await fetch(`${apiUrl}/api/download-posters`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- posters: selectedPosters,
- format: downloadFormat,
- }),
- });
-
- if (!response.ok) throw new Error("Download failed");
-
- const contentType = response.headers.get("content-type");
- if (contentType && contentType.includes("application/zip")) {
- const blob = await response.blob();
- const url = window.URL.createObjectURL(blob);
- const link = document.createElement("a");
- link.href = url;
- link.download = "posters.zip";
- document.body.appendChild(link);
- link.click();
- link.remove();
- window.URL.revokeObjectURL(url);
- } else {
- throw new Error("Invalid response format");
- }
- } catch (error) {
- console.error("Failed to download posters:", error);
- }
- };
-
- return (
- <>
-
-
-
- {selectedPosters.length === 0 ? (
-
-
- Your poster collection is empty. Start exploring movies to add
- some posters!
-
- navigate("/")}
- >
- Browse Movies
-
-
- ) : (
- <>
- {selectedPosters.map((poster) => (
-
-
-
-
- handleRemovePoster(poster.movieId, poster.posterId)
- }
- aria-label="Supprimer ce poster"
- >
-
-
-
-
- ))}
-
navigate("/")}>
- +
-
- >
- )}
-
-
-
-
Download Options
- setDownloadFormat(e.target.value)}
- className="format-select"
- >
- ZIP
- TAR
- 7Z
-
-
- Download Selected
-
-
- Clear All Posters
-
-
-
-
- Confirm deletion
-
-
- Are you sure you want to delete all the posters in your collection?
- This action cannot be undone.
-
-
-
-
- Cancel
-
-
- Delete all
-
-
-
-
-
- >
- );
-};
-
-export default Cart;
diff --git a/src/components/DownloadButton.js b/src/components/DownloadButton.js
deleted file mode 100644
index c95cd2a..0000000
--- a/src/components/DownloadButton.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import React from "react";
-import { useSelector } from "react-redux";
-import { Fab, makeStyles } from "@material-ui/core";
-import GetAppIcon from "@material-ui/icons/GetApp";
-
-const useStyles = makeStyles((theme) => ({
- downloadFab: {
- backgroundColor: "#1caff2",
- position: "fixed",
- bottom: theme.spacing(4),
- left: "50%",
- transform: "translateX(-50%)",
- zIndex: 1000,
- },
-}));
-
-const GlobalDownloadButton = () => {
- const classes = useStyles();
- const selectedPosters = useSelector((state) => {
- const selections = state.posterSelections;
- return Object.values(selections).flat();
- });
-
- const totalSelected = selectedPosters.length;
-
- if (totalSelected === 0) return null;
-
- const handleDownload = () => {
- // Implement your download logic here
- console.log("Downloading", totalSelected, "posters");
- };
-
- return (
-
-
- Download ({totalSelected})
-
- );
-};
-
-export default GlobalDownloadButton;
diff --git a/src/components/Header.js b/src/components/Header.js
new file mode 100644
index 0000000..b0f7de9
--- /dev/null
+++ b/src/components/Header.js
@@ -0,0 +1,252 @@
+import React, { useState } from "react";
+import { useSelector, useDispatch } from "react-redux";
+import { useNavigate, useMatch } from "react-router-dom";
+import { createSelector } from "reselect";
+import { AppBar, Toolbar, Typography, Button, IconButton, LinearProgress, Box, Tooltip, Link, Menu, MenuItem } from "@material-ui/core";
+import { Snackbar, Alert } from "@mui/material";
+import GetAppIcon from "@material-ui/icons/GetApp";
+import ExitToAppIcon from "@material-ui/icons/ExitToApp";
+import ViewModuleIcon from "@material-ui/icons/ViewModule";
+import SyncIcon from "@material-ui/icons/Sync";
+import MoreVertIcon from "@material-ui/icons/MoreVert";
+import JSZip from "jszip";
+import api from "../services/api";
+import { removeAllPosters } from "../services/action";
+
+const selectSelectedPosters = createSelector(
+ (state) => state.posterSelections,
+ (posterSelections) =>
+ Object.entries(posterSelections).flatMap(([movieId, posters]) =>
+ (posters || []).map((p) => ({ movieId, posterId: p.posterId, watchedDate: p.watchedDate }))
+ )
+);
+
+const Header = () => {
+ const navigate = useNavigate();
+ const dispatch = useDispatch();
+ const galleryMatch = useMatch("/posters/:movieName/:movieYear");
+ const movieName = galleryMatch ? decodeURIComponent(galleryMatch.params.movieName) : null;
+
+ const [downloadState, setDownloadState] = useState(null); // null | { current, total }
+ const [syncState, setSyncState] = useState(null); // null | 'syncing' | { added: number }
+ const [menuAnchor, setMenuAnchor] = useState(null);
+ const [snackbar, setSnackbar] = useState(null); // null | { message, severity }
+
+ const selectedPosters = useSelector(selectSelectedPosters);
+ const username = useSelector((state) => state.username);
+
+ const count = selectedPosters.length;
+
+ const handleDownload = async () => {
+ if (downloadState) return;
+ const total = selectedPosters.length;
+ setDownloadState({ current: 0, total });
+
+ try {
+ const zip = new JSZip();
+
+ for (let i = 0; i < selectedPosters.length; i++) {
+ const { movieId, posterId, watchedDate } = selectedPosters[i];
+ const url = `https://image.tmdb.org/t/p/original${posterId}`;
+ const res = await fetch(url);
+ const blob = await res.blob();
+ zip.file(`${watchedDate}_${movieId}.jpg`, blob);
+ setDownloadState({ current: i + 1, total });
+ }
+
+ const content = await zip.generateAsync({ type: "blob" });
+ const objectUrl = URL.createObjectURL(content);
+ const link = document.createElement("a");
+ link.href = objectUrl;
+ link.download = "posters.zip";
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+ URL.revokeObjectURL(objectUrl);
+ } catch (err) {
+ console.error("Download error:", err);
+ } finally {
+ setDownloadState(null);
+ }
+ };
+
+ const handleRssSync = async () => {
+ if (!username || syncState === 'syncing') return;
+ setSyncState('syncing');
+ try {
+ const res = await api.post("/api/sync-rss", { username });
+ setSyncState({ added: res.data.added });
+ setTimeout(() => setSyncState(null), 3000);
+ if (res.data.added > 0) {
+ setSnackbar({ message: `${res.data.added} new film${res.data.added > 1 ? 's' : ''} added to your diary`, severity: 'success' });
+ window.dispatchEvent(new Event('diary-synced'));
+ } else {
+ setSnackbar({ message: 'Already up to date', severity: 'info' });
+ }
+ } catch {
+ setSyncState(null);
+ setSnackbar({ message: 'Sync failed. Please try again.', severity: 'error' });
+ }
+ };
+
+ const handleReset = async () => {
+ try {
+ await api.delete("/api/delete-csv");
+ dispatch(removeAllPosters());
+ navigate("/");
+ } catch (err) {
+ console.error("Reset error:", err);
+ }
+ };
+
+ const isDownloading = !!downloadState;
+ const progress = isDownloading
+ ? Math.round((downloadState.current / downloadState.total) * 100)
+ : 0;
+
+ return (
+ <>
+ setSnackbar(null)}
+ anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
+ >
+ setSnackbar(null)} severity={snackbar?.severity} sx={{ width: '100%' }}>
+ {snackbar?.message}
+
+
+
+
+ navigate("/PosterSelector")}>
+
+
+ Poster Picker
+
+
+
+
+ {movieName ? (
+
+ {movieName}
+
+ ) : username ? (
+ (e.currentTarget.style.opacity = 1)}
+ onMouseLeave={(e) => (e.currentTarget.style.opacity = 0.7)}
+ >
+ {username}
+
+ ) : null}
+
+ {/* Desktop actions */}
+
+ {username && (
+ 0 ? `+${syncState.added} new film${syncState.added > 1 ? 's' : ''}`
+ : `Sync from ${username}'s RSS`
+ }>
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+ {count > 0 && (
+ navigate("/recap")} disabled={isDownloading}>
+
+
+ )}
+ {count > 0 && (
+ }
+ onClick={handleDownload}
+ disabled={isDownloading}
+ >
+ {isDownloading ? (
+
+ {downloadState.current} / {downloadState.total}
+
+
+ ) : (
+ `Download (${count})`
+ )}
+
+ )}
+
+
+ {/* Mobile actions — collapsed into a menu */}
+ setMenuAnchor(e.currentTarget)}>
+
+
+ setMenuAnchor(null)}
+ PaperProps={{ className: "header-menu-paper" }}
+ getContentAnchorEl={null}
+ anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
+ transformOrigin={{ vertical: 'top', horizontal: 'right' }}
+ >
+ {username && (
+ { handleRssSync(); setMenuAnchor(null); }}
+ disabled={isDownloading || syncState === 'syncing'}
+ className="header-menu-item"
+ >
+
+ {syncState === 'syncing' ? 'Syncing…'
+ : syncState?.added === 0 ? 'Up to date'
+ : syncState?.added > 0 ? `+${syncState.added} new film${syncState.added > 1 ? 's' : ''}`
+ : 'Sync RSS'}
+
+ )}
+ {count > 0 && (
+ { navigate('/recap'); setMenuAnchor(null); }} className="header-menu-item">
+
+ My selections ({count})
+
+ )}
+ {count > 0 && (
+ { handleDownload(); setMenuAnchor(null); }}
+ disabled={isDownloading}
+ className="header-menu-item"
+ >
+
+ {isDownloading
+ ? `Downloading… ${downloadState.current}/${downloadState.total}`
+ : `Download (${count})`}
+
+ )}
+ { handleReset(); setMenuAnchor(null); }} className="header-menu-item header-menu-item--danger">
+
+ Reset diary
+
+
+
+
+ >
+ );
+};
+
+export default Header;
diff --git a/src/components/NavBar.js b/src/components/NavBar.js
deleted file mode 100644
index 4c51c06..0000000
--- a/src/components/NavBar.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import React, { useMemo } from "react";
-import axios from "axios";
-import { useSelector } from "react-redux";
-import { AppBar, Toolbar, IconButton, Badge, Button } from "@material-ui/core";
-import ShoppingCartIcon from "@material-ui/icons/ShoppingCart";
-import ArrowBackIcon from "@material-ui/icons/ArrowBack";
-import { useNavigate, useLocation } from "react-router-dom";
-
-const apiUrl = process.env.REACT_APP_API_URL;
-
-const selectPosterSelections = (state) => state.posterSelections;
-
-const NavBar = () => {
- const navigate = useNavigate();
- const location = useLocation();
-
- const posterSelections = useSelector(selectPosterSelections);
- const selectedPosters = useMemo(() => {
- return Object.values(posterSelections).flat();
- }, [posterSelections]);
-
- const totalSelected = selectedPosters.length;
-
- const handleBack = () => {
- navigate(-1);
- };
-
- const handleCart = () => {
- navigate("/Cart");
- };
-
- const handleResetprofile = async () => {
- try {
- await axios.delete(`${apiUrl}/api/delete-csv`, { withCredentials: true });
- navigate("/");
- } catch (error) {
- console.error("Error deleting CSV file:", error);
- }
- };
-
- const isPosterSelectorPage = location.pathname !== "/PosterSelector";
-
- return (
-
-
- {isPosterSelectorPage ? (
-
-
-
- ) : (
-
- {/* Disabled IconButton */}
-
- )}
-
-
- Reset Profile
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default NavBar;
diff --git a/src/components/PosterGallery.js b/src/components/PosterGallery.js
deleted file mode 100644
index c2f1169..0000000
--- a/src/components/PosterGallery.js
+++ /dev/null
@@ -1,167 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { useLocation, useParams } from "react-router-dom";
-import axios from "axios";
-import { useSelector, useDispatch } from "react-redux";
-import { selectPoster, deselectPoster } from "../services/action";
-import {
- Container,
- Typography,
- Grid,
- Card,
- CardMedia,
- CircularProgress,
- Box,
- Paper,
- DialogContent,
- DialogTitle,
- DialogActions,
- Button,
-} from "@material-ui/core";
-import CheckIcon from "@material-ui/icons/Check";
-import NavBar from "./NavBar";
-import { Dialog } from "@mui/material";
-
-const apiUrl = process.env.REACT_APP_API_URL;
-
-const PosterGallery = () => {
- const location = useLocation();
- const { watchedDate } = location.state || {};
- const { movieName, movieYear } = useParams();
- const [posters, setPosters] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const [dialogOpen, setDialogOpen] = useState(false);
- const dispatch = useDispatch();
-
- const movieId = `${movieName}-${movieYear}`;
-
- const selectedPosters = useSelector(
- (state) => state.posterSelections[movieId] || []
- );
-
- useEffect(() => {
- const fetchPosters = async () => {
- try {
- setLoading(true);
- setError(null);
-
- const encodedName = encodeURIComponent(movieName);
- const encodedYear = encodeURIComponent(movieYear);
-
- const response = await axios.get(
- `${apiUrl}/api/posters/${encodedName}/${encodedYear}`
- );
-
- if (response.data && response.data.posters) {
- setPosters(response.data.posters);
- } else {
- throw new Error("No posters data in response");
- }
- } catch (error) {
- console.error("Error fetching posters:", error);
- } finally {
- setLoading(false);
- }
- };
-
- if (movieName && movieYear) {
- fetchPosters();
- }
- }, [movieName, movieYear]);
-
- const isPosterSelected = (posterId) => {
- return selectedPosters.some((poster) => poster.posterId === posterId);
- };
-
- const handlePosterSelect = (posterId) => {
- const isSelected = isPosterSelected(posterId);
-
- if (selectedPosters.length > 0 && !isSelected) {
- setDialogOpen(true);
- return;
- }
- if (isSelected) {
- dispatch(deselectPoster(movieName, movieYear, posterId));
- } else {
- dispatch(selectPoster(movieName, movieYear, posterId, watchedDate));
- }
- };
-
- const handleDialogClose = () => {
- setDialogOpen(false);
- };
-
- if (error) {
- return (
-
-
-
- {error}
-
-
-
-
- );
- }
-
- return (
- <>
-
-
-
- Posters for {movieName} ({movieYear})
-
- {loading ? (
-
-
-
- ) : posters.length > 0 ? (
-
- {posters.map((poster, index) => {
- const isSelected = isPosterSelected(poster.file_path);
- return (
-
- handlePosterSelect(poster.file_path)}
- >
-
- {isSelected && }
-
-
- );
- })}
-
- ) : (
-
- No posters found for this movie.
-
- )}
-
-
-
-
- Poster already selected
-
-
- You can only select one poster per movie. Please deselect the
- current poster first.
-
-
-
-
- OK
-
-
-
- >
- );
-};
-
-export default PosterGallery;
diff --git a/src/components/PosterSelector.js b/src/components/PosterSelector.js
deleted file mode 100644
index c583e04..0000000
--- a/src/components/PosterSelector.js
+++ /dev/null
@@ -1,256 +0,0 @@
-import React, { useState, useEffect } from "react";
-import axios from "axios";
-import { useNavigate, useLocation } from "react-router-dom";
-import {
- Container,
- List,
- ListItem,
- Grid,
- Typography,
- LinearProgress,
- Box,
- Paper,
- Button,
-} from "@material-ui/core";
-import Pagination from "@mui/material/Pagination";
-import { createTheme, ThemeProvider } from "@mui/material/styles";
-import queryString from "query-string";
-import pulpGif from "../static/images/pulp.gif";
-import NavBar from "./NavBar";
-
-const apiUrl = process.env.REACT_APP_API_URL;
-
-const paginationTheme = createTheme({
- palette: {
- mode: "dark",
- primary: {
- main: "#667788",
- },
- text: {
- primary: "#ffffff",
- secondary: "#667788",
- },
- action: {
- hover: "rgba(102, 119, 136, 0.2)",
- },
- },
- components: {
- MuiPaginationItem: {
- styleOverrides: {
- root: {
- "&.Mui-selected": {
- backgroundColor: "#667788",
- color: "#ffffff",
- "&:hover": {
- backgroundColor: "#778899",
- },
- },
- },
- },
- },
- },
-});
-
-const PosterSelector = () => {
- const [movies, setMovies] = useState([]);
- const [loading, setLoading] = useState(true);
- const [page, setPage] = useState(1);
- const [totalPages, setTotalPages] = useState(1);
- //const [progress, setProgress] = useState(0);
- const [username, setUsername] = useState("");
- const navigate = useNavigate();
- const location = useLocation();
- const moviesPerPage = 8;
-
- useEffect(() => {
- const parsed = queryString.parse(location.search);
- const pageNumber = parsed.page ? parseInt(parsed.page, 10) : 1;
- setPage(pageNumber);
-
- fetchMovies(pageNumber);
- }, [location.search]);
-
- // const fetchUsername = async () => {
- // try {
- // const response = await axios.get(`${apiUrl}/api/username`);
- // setUsername(response.data.username);
- // } catch (error) {
- // console.error("Error fetching username:", error);
- // }
- // };
-
- const fetchMovies = async (pageNumber) => {
- try {
- setLoading(true);
- // setProgress(0);
-
- const response = await axios.get(
- `${apiUrl}/api/movies?page=${pageNumber}&limit=${moviesPerPage}`,
- { withCredentials: true }
- );
-
- if (response.data.movies) {
- setMovies(response.data.movies);
- setTotalPages(Math.ceil(response.data.total / moviesPerPage));
- }
- } catch (error) {
- console.error("Error fetching movies:", error);
- } finally {
- setLoading(false);
- }
- };
-
- const handleMovieClick = (movieName, movieYear, watchedDate) => {
- navigate(
- `/posters/${encodeURIComponent(movieName)}/${encodeURIComponent(
- movieYear
- )}`,
- { state: { watchedDate } }
- );
- };
-
- const handlePageChange = (event, value) => {
- setPage(value);
- navigate(`?page=${value}`);
- };
-
- const handleGoBack = () => {
- navigate("/");
- };
-
- const formatWatchedDate = (dateString) => {
- const date = new Date(dateString);
- const day = date.getDate().toString().padStart(2, "0");
- const month = date
- .toLocaleString("default", { month: "short" })
- .toUpperCase();
- return { day, month };
- };
-
- return (
- <>
-
-
-
- Your diary
-
-
- {username && (
-
- Letterboxd User: {username}
-
- )}
-
- {loading ? (
-
-
-
- Downloading movies...
-
-
- ) : movies.length > 0 ? (
- <>
-
- {movies.map((movie, index) => (
-
- handleMovieClick(
- movie.Name,
- movie.Year,
- movie["Watched Date"]
- )
- }
- className="movie-item"
- >
-
-
-
-
-
-
- {movie.Name}
-
-
- {movie.Year}
-
-
-
-
- {(() => {
- const { day, month } = formatWatchedDate(
- movie["Watched Date"]
- );
- return (
- <>
-
- {day}
-
-
- {month}
-
- >
- );
- })()}
-
-
-
-
- ))}
-
-
-
-
-
-
- >
- ) : (
-
-
-
-
-
-
- No movies found in your diary. Please check your Letterboxd
- username or CSV file.
-
-
- Go Back
-
-
- )}
-
-
-
- >
- );
-};
-
-export default PosterSelector;
diff --git a/src/components/UploadDiary.js b/src/components/UploadDiary.js
deleted file mode 100644
index 3073dc9..0000000
--- a/src/components/UploadDiary.js
+++ /dev/null
@@ -1,192 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { useNavigate } from "react-router-dom";
-import {
- Container,
- Grid,
- Typography,
- Button,
- CircularProgress,
- TextField,
- Box,
- Link,
-} from "@material-ui/core";
-import { CloudUploadOutlined } from "@material-ui/icons";
-import CloudDoneOutlinedIcon from "@mui/icons-material/CloudDoneOutlined";
-import { useDropzone } from "react-dropzone";
-import axios from "axios";
-
-const apiUrl = process.env.REACT_APP_API_URL;
-const VERSION = require("../../package.json").version;
-
-const UploadDiary = () => {
- const [file, setFile] = useState(null);
- const [uploading, setUploading] = useState(false);
- const [username, setUsername] = useState("");
- const navigate = useNavigate();
-
- useEffect(() => {
- const checkCSVFile = async () => {
- try {
- const response = await axios.get(`${apiUrl}/api/check-csv`, {
- withCredentials: true,
- });
- if (response.data.fileExists) {
- navigate("/PosterSelector");
- }
- } catch (error) {
- console.error("Error checking CSV file:", error);
- }
- };
- checkCSVFile();
- }, [navigate]);
-
- const { getRootProps, getInputProps } = useDropzone({
- onDrop: (acceptedFiles) => {
- setFile(acceptedFiles[0]);
- },
- maxFiles: 1,
- accept: ".csv",
- });
-
- const handleUpload = async () => {
- if (username || file) {
- setUploading(true);
- try {
- if (username) {
- const response = await axios.post(
- `${apiUrl}/api/fetch-diary`,
- { username },
- { withCredentials: true }
- );
-
- if (response.data && response.data.success) {
- await new Promise((resolve) => setTimeout(resolve, 500));
- navigate("/PosterSelector");
- } else {
- console.error("Failed to fetch diary", response.data);
- }
- } else if (file) {
- const formData = new FormData();
- formData.append("file", file);
-
- await axios.post(`${apiUrl}/api/upload-csv`, formData, {
- headers: { "Content-Type": "multipart/form-data" },
- withCredentials: true,
- });
-
- navigate("/PosterSelector");
- }
- } catch (error) {
- console.error("Error processing diary:", error);
- } finally {
- setUploading(false);
- }
- }
- };
-
- return (
-
-
-
-
-
-
-
- Upload Letterboxd Diary
-
-
setUsername(e.target.value)}
- onKeyUp={(e) => {
- if (e.key === "Enter") handleUpload();
- }}
- placeholder="Enter your Letterboxd username"
- />
-
- Or upload a CSV file:
-
-
-
- {file ? (
- <>
-
-
- Your file has been uploaded: {file.name}
-
- >
- ) : (
- <>
-
-
- Drag and drop a CSV file here or click to select
-
- >
- )}
-
-
- {uploading ? (
-
- ) : (
-
- SUBMIT
-
- )}
-
-
-
- This tool allows you to easily import your Letterboxd diary and select
- your favorite movie poster. Follow these simple steps:
-
- 1. Enter Your Letterboxd Username: Type your
- Letterboxd username to automatically fetch your diary.
-
- 2. Or Upload Your CSV File: If you prefer, you can
- still upload your diary.csv file directly.
-
- 3. Automatic Processing: Once submitted, the
- application will process your diary.
-
- 4. Poster Selection: After processing, you'll be
- redirected to the Poster Selector page, where you can browse and
- choose your favorite movie poster based on your Letterboxd diary.
-
-
-
-
-
- v{VERSION}
-
-
- •
-
- (e.currentTarget.style.opacity = 1)}
- onMouseLeave={(e) => (e.currentTarget.style.opacity = 0.7)}
- >
- Hugyouu
-
-
-
-
- );
-};
-
-export default UploadDiary;
diff --git a/src/index.js b/src/index.js
index d665ff2..4080a97 100644
--- a/src/index.js
+++ b/src/index.js
@@ -5,11 +5,7 @@ import App from "./App";
import reportWebVitals from "./reportWebVitals";
const root = ReactDOM.createRoot(document.getElementById("root"));
-root.render(
-
-
-
-);
+root.render( );
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
diff --git a/src/pages/PosterGallery.js b/src/pages/PosterGallery.js
new file mode 100644
index 0000000..16bac5c
--- /dev/null
+++ b/src/pages/PosterGallery.js
@@ -0,0 +1,150 @@
+import React, { useState, useEffect } from "react";
+import { useLocation, useParams, useNavigate } from "react-router-dom";
+import api from "../services/api";
+import { useSelector, useDispatch } from "react-redux";
+import { selectPoster, deselectPoster } from "../services/action";
+import {
+ Container,
+ Typography,
+ Grid,
+ Card,
+ Box,
+ Paper,
+} from "@material-ui/core";
+import { ToggleButton, ToggleButtonGroup } from "@mui/material";
+import CheckIcon from "@material-ui/icons/Check";
+
+const LANG_FILTERS = [
+ { value: "all", label: "All" },
+ { value: "en", label: "EN" },
+ { value: "fr", label: "FR" },
+ { value: "ja", label: "JA" },
+ { value: "none", label: "—" },
+];
+
+const PosterCard = ({ poster, movieName, isSelected, onClick }) => {
+ const [imgLoaded, setImgLoaded] = useState(false);
+
+ return (
+
+
+
+
setImgLoaded(true)}
+ />
+
+ {isSelected && }
+
+ );
+};
+
+const PosterGallery = () => {
+ const location = useLocation();
+ const { watchedDate } = location.state || {};
+ const { movieName, movieYear } = useParams();
+ const navigate = useNavigate();
+ const [posters, setPosters] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [langFilter, setLangFilter] = useState("all");
+ const dispatch = useDispatch();
+
+ const movieId = `${movieName}-${movieYear}`;
+ const selectedPosters = useSelector(
+ (state) => state.posterSelections[movieId] || []
+ );
+
+ useEffect(() => {
+ const fetchPosters = async () => {
+ try {
+ setLoading(true);
+ const response = await api.get(
+ `/api/posters/${encodeURIComponent(movieName)}/${encodeURIComponent(movieYear)}`
+ );
+ if (response.data?.posters) setPosters(response.data.posters);
+ } catch (error) {
+ console.error("Error fetching posters:", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+ if (movieName && movieYear) fetchPosters();
+ }, [movieName, movieYear]);
+
+ const filteredPosters = posters.filter((p) => {
+ if (langFilter === "all") return true;
+ if (langFilter === "none") return !p.iso_639_1;
+ return p.iso_639_1 === langFilter;
+ });
+
+ const isPosterSelected = (posterId) =>
+ selectedPosters.some((p) => p.posterId === posterId);
+
+ const handlePosterSelect = (posterId) => {
+ if (isPosterSelected(posterId)) {
+ dispatch(deselectPoster(movieName, movieYear, posterId));
+ } else {
+ dispatch(selectPoster(movieName, movieYear, posterId, watchedDate));
+ navigate(-1);
+ }
+ };
+
+ const visiblePosters = loading
+ ? Array.from({ length: 8 }).map((_, i) => ({ _skeleton: true, file_path: `sk-${i}` }))
+ : filteredPosters;
+
+ return (
+
+
+
+ v && setLangFilter(v)}
+ className="lang-toggle-group"
+ >
+ {LANG_FILTERS.map(({ value, label }) => (
+
+ {label}
+
+ ))}
+
+
+
+ {!loading && filteredPosters.length === 0 ? (
+
+ No posters for this language.
+
+ ) : (
+
+ {visiblePosters.map((poster, index) =>
+ poster._skeleton ? (
+
+
+
+ ) : (
+
+ handlePosterSelect(poster.file_path)}
+ />
+
+ )
+ )}
+
+ )}
+
+
+ );
+};
+
+export default PosterGallery;
diff --git a/src/pages/PosterSelector.js b/src/pages/PosterSelector.js
new file mode 100644
index 0000000..04df6a1
--- /dev/null
+++ b/src/pages/PosterSelector.js
@@ -0,0 +1,388 @@
+import React, { useState, useEffect, useRef, useCallback } from "react";
+import api from "../services/api";
+import { useNavigate, useSearchParams } from "react-router-dom";
+import { useSelector } from "react-redux";
+import {
+ Container,
+ Typography,
+ LinearProgress,
+ Box,
+ Paper,
+ Button,
+ IconButton,
+ Select,
+ MenuItem,
+ Popover,
+} from "@material-ui/core";
+// LinearProgress kept for the initializing state only
+import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
+import ChevronRightIcon from "@material-ui/icons/ChevronRight";
+import CheckIcon from "@material-ui/icons/Check";
+import pulpGif from "../static/images/pulp.gif";
+
+const CURRENT_YEAR = new Date().getFullYear();
+const YEARS = Array.from({ length: CURRENT_YEAR - 2000 + 1}, (_, i) => 2000 + i).reverse();
+
+const DAY_LABELS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
+
+const MONTH_NAMES = [
+ "January", "February", "March", "April", "May", "June",
+ "July", "August", "September", "October", "November", "December",
+];
+
+const PosterSelector = () => {
+ const [currentMonth, setCurrentMonth] = useState(null);
+ const [moviesByDate, setMoviesByDate] = useState({});
+ const [loading, setLoading] = useState(true);
+ const [initializing, setInitializing] = useState(true);
+ const [popoverAnchor, setPopoverAnchor] = useState(null);
+ const [popoverMovies, setPopoverMovies] = useState([]);
+ const navigate = useNavigate();
+ const [searchParams, setSearchParams] = useSearchParams();
+ const posterSelections = useSelector((state) => state.posterSelections);
+ const dataSource = useSelector((state) => state.dataSource);
+
+ useEffect(() => {
+ const init = async () => {
+ try {
+ const csvCheck = await api.get("/api/check-csv");
+ if (!csvCheck.data.fileExists) {
+ navigate("/");
+ return;
+ }
+
+ const urlMonth = searchParams.get("month");
+ if (urlMonth) {
+ const [year, month] = urlMonth.split("-").map(Number);
+ setCurrentMonth({ year, month: month - 1 });
+ setInitializing(false);
+ return;
+ }
+
+ const res = await api.get("/api/movies?page=1&limit=1");
+ if (res.data.movies?.length > 0) {
+ const latestDate = res.data.movies[0]["Watched Date"];
+ const [year, month] = latestDate.split("-").map(Number);
+ setCurrentMonth({ year, month: month - 1 });
+ } else {
+ const now = new Date();
+ setCurrentMonth({ year: now.getFullYear(), month: now.getMonth() });
+ }
+ } catch {
+ navigate("/");
+ } finally {
+ setInitializing(false);
+ }
+ };
+ init();
+ }, [navigate, searchParams]);
+
+ useEffect(() => {
+ if (!currentMonth) return;
+ fetchMoviesForMonth(currentMonth.year, currentMonth.month);
+ }, [currentMonth]);
+
+ const fetchMoviesForMonth = useCallback(async (year, month) => {
+ setLoading(true);
+ try {
+ const monthStr = `${year}-${String(month + 1).padStart(2, "0")}`;
+ const res = await api.get(`/api/movies?month=${monthStr}`);
+ const map = {};
+ res.data.movies?.forEach((movie) => {
+ const date = movie["Watched Date"];
+ if (!map[date]) map[date] = [];
+ map[date].push(movie);
+ });
+ setMoviesByDate(map);
+ } catch (err) {
+ console.error("Error fetching movies:", err);
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ const currentMonthRef = useRef(currentMonth);
+ useEffect(() => { currentMonthRef.current = currentMonth; }, [currentMonth]);
+
+ useEffect(() => {
+ const handleSync = () => {
+ const m = currentMonthRef.current;
+ if (m) fetchMoviesForMonth(m.year, m.month);
+ };
+ window.addEventListener('diary-synced', handleSync);
+ return () => window.removeEventListener('diary-synced', handleSync);
+ }, [fetchMoviesForMonth]);
+
+ const updateMonth = (year, month) => {
+ setCurrentMonth({ year, month });
+ setSearchParams({ month: `${year}-${String(month + 1).padStart(2, "0")}` });
+ };
+
+ const goToPrevMonth = () => {
+ const { year, month } = currentMonth;
+ if (month === 0) updateMonth(year - 1, 11);
+ else updateMonth(year, month - 1);
+ };
+
+ const goToNextMonth = () => {
+ const { year, month } = currentMonth;
+ if (month === 11) updateMonth(year + 1, 0);
+ else updateMonth(year, month + 1);
+ };
+
+ const goToToday = () => {
+ const now = new Date();
+ updateMonth(now.getFullYear(), now.getMonth());
+ };
+
+ const handleGoToUpload = async () => {
+ try {
+ await api.delete("/api/delete-csv");
+ } catch {
+ // server may have restarted, proceed anyway
+ }
+ navigate("/");
+ };
+
+ const handleMovieClick = (movie) => {
+ navigate(
+ `/posters/${encodeURIComponent(movie.Name)}/${encodeURIComponent(movie.Year)}`,
+ { state: { watchedDate: movie["Watched Date"] } }
+ );
+ };
+
+ const handleCellClick = (e, movies) => {
+ if (!movies.length) return;
+ if (movies.length === 1) {
+ handleMovieClick(movies[0]);
+ } else {
+ setPopoverMovies(movies);
+ setPopoverAnchor(e.currentTarget);
+ }
+ };
+
+ const buildCalendarCells = () => {
+ if (!currentMonth) return [];
+ const { year, month } = currentMonth;
+ const firstDayOfWeek = new Date(year, month, 1).getDay();
+ const startOffset = (firstDayOfWeek + 6) % 7; // Mon=0
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
+
+ const cells = [];
+ for (let i = 0; i < startOffset; i++) cells.push(null);
+ for (let d = 1; d <= daysInMonth; d++) cells.push(d);
+ return cells;
+ };
+
+ if (initializing) {
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+ }
+
+ const cells = buildCalendarCells();
+ const hasAnyMovie = Object.keys(moviesByDate).length > 0;
+ const today = new Date();
+ const isCurrentMonth =
+ currentMonth.year === today.getFullYear() &&
+ currentMonth.month === today.getMonth();
+ const isFutureMonth =
+ currentMonth.year > today.getFullYear() ||
+ (currentMonth.year === today.getFullYear() && currentMonth.month > today.getMonth());
+
+ return (
+ <>
+
+
+
+
+ Today
+
+
+
+
+
+
+ {currentMonth
+ ? `${MONTH_NAMES[currentMonth.month]} ${currentMonth.year}`
+ : ""}
+
+
+
+
+
+
+ updateMonth(currentMonth.year, e.target.value)}
+ className="calendar-select"
+ disableUnderline
+ MenuProps={{ PaperProps: { className: "calendar-select-menu" } }}
+ >
+ {MONTH_NAMES.map((name, i) => {
+ const disabled =
+ currentMonth.year === today.getFullYear() && i > today.getMonth();
+ return {name} ;
+ })}
+
+ updateMonth(e.target.value, currentMonth.month)}
+ className="calendar-select"
+ disableUnderline
+ MenuProps={{ PaperProps: { className: "calendar-select-menu" } }}
+ >
+ {YEARS.map((y) => (
+ {y}
+ ))}
+
+
+
+
+ {dataSource === 'rss' && (
+
+ ⚠
+ RSS sync — showing your last ~50 films only.{' '}
+
+ Upload a CSV
+ {' '}
+ for your full history.
+
+ )}
+
+
+
+ {DAY_LABELS.map((label) => (
+
{label}
+ ))}
+ {cells.map((day, i) => {
+ if (!day) {
+ return
;
+ }
+
+ const { year, month } = currentMonth;
+ const dateStr = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
+ const isFuture = isFutureMonth || (isCurrentMonth && day > today.getDate());
+ const isToday = isCurrentMonth && day === today.getDate();
+ const movies = isFuture ? [] : (moviesByDate[dateStr] || []);
+ const movie = movies[0];
+
+ const movieId = movie ? `${movie.Name}-${movie.Year}` : null;
+ const selection = movieId ? posterSelections[movieId]?.[0] : null;
+ const posterSrc = selection
+ ? `https://image.tmdb.org/t/p/w200${selection.posterId}`
+ : movie?.Poster
+ ? `https://image.tmdb.org/t/p/w200${movie.Poster}`
+ : null;
+
+ return (
+
!loading && movies.length > 0 && handleCellClick(e, movies)}
+ >
+
{day}
+ {loading && !isFuture && (
+
+ )}
+ {!loading && movie && posterSrc && (
+
+
+
+ {movie.Name}
+ {movie.Year}
+
+ {selection && (
+
+
+
+ )}
+ {movies.length > 1 && (
+
+{movies.length - 1}
+ )}
+
+ )}
+
+ );
+ })}
+
+
+ {!loading && !hasAnyMovie && !isFutureMonth && (
+
+
+
+
+
+
+ No movies watched this month.
+
+ navigate("/")}>
+ Go Back
+
+
+ )}
+
+
+
+
+ setPopoverAnchor(null)}
+ anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
+ transformOrigin={{ vertical: "top", horizontal: "center" }}
+ PaperProps={{ className: "multi-movie-popover" }}
+ >
+ {popoverMovies.map((movie) => {
+ const movieId = `${movie.Name}-${movie.Year}`;
+ const selection = posterSelections[movieId]?.[0];
+ const posterSrc = selection
+ ? `https://image.tmdb.org/t/p/w200${selection.posterId}`
+ : movie.Poster
+ ? `https://image.tmdb.org/t/p/w200${movie.Poster}`
+ : null;
+ return (
+ { handleMovieClick(movie); setPopoverAnchor(null); }}
+ >
+ {posterSrc ? (
+
+ ) : (
+
+ )}
+
+ {movie.Name}
+ {movie.Year}
+
+ {selection &&
}
+
+ );
+ })}
+
+ >
+ );
+};
+
+export default PosterSelector;
diff --git a/src/pages/SelectionRecap.js b/src/pages/SelectionRecap.js
new file mode 100644
index 0000000..40e85ad
--- /dev/null
+++ b/src/pages/SelectionRecap.js
@@ -0,0 +1,173 @@
+import React, { useState } from "react";
+import { useSelector } from "react-redux";
+import { useNavigate } from "react-router-dom";
+import {
+ Container,
+ Typography,
+ Box,
+ Button,
+ IconButton,
+ LinearProgress,
+} from "@material-ui/core";
+import GetAppIcon from "@material-ui/icons/GetApp";
+import ArrowBackIcon from "@material-ui/icons/ArrowBack";
+import CheckIcon from "@material-ui/icons/Check";
+import JSZip from "jszip";
+
+const MONTH_NAMES = [
+ "January", "February", "March", "April", "May", "June",
+ "July", "August", "September", "October", "November", "December",
+];
+
+const SelectionRecap = () => {
+ const navigate = useNavigate();
+ const posterSelections = useSelector((state) => state.posterSelections);
+ const [downloadState, setDownloadState] = useState(null);
+
+ const entries = Object.entries(posterSelections)
+ .flatMap(([movieId, posters]) => {
+ if (!posters?.length) return [];
+ const match = movieId.match(/^(.+)-(\d{4})$/);
+ if (!match) return [];
+ return posters.map((p) => ({
+ movieId,
+ movieName: match[1],
+ movieYear: match[2],
+ posterId: p.posterId,
+ watchedDate: p.watchedDate || "",
+ }));
+ })
+ .sort((a, b) => a.watchedDate.localeCompare(b.watchedDate));
+
+ const groups = entries.reduce((acc, entry) => {
+ const key = entry.watchedDate.slice(0, 7);
+ if (!acc[key]) acc[key] = [];
+ acc[key].push(entry);
+ return acc;
+ }, {});
+
+ const sortedMonths = Object.keys(groups).sort().reverse();
+
+ const handleDownload = async () => {
+ if (downloadState) return;
+ setDownloadState({ current: 0, total: entries.length });
+ try {
+ const zip = new JSZip();
+ for (let i = 0; i < entries.length; i++) {
+ const { movieId, posterId, watchedDate } = entries[i];
+ const res = await fetch(`https://image.tmdb.org/t/p/original${posterId}`);
+ const blob = await res.blob();
+ zip.file(`${watchedDate}_${movieId}.jpg`, blob);
+ setDownloadState({ current: i + 1, total: entries.length });
+ }
+ const content = await zip.generateAsync({ type: "blob" });
+ const url = URL.createObjectURL(content);
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = "posters.zip";
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+ URL.revokeObjectURL(url);
+ } catch (err) {
+ console.error("Download error:", err);
+ } finally {
+ setDownloadState(null);
+ }
+ };
+
+ if (entries.length === 0) {
+ return (
+
+
+ No posters selected yet.
+ navigate("/PosterSelector")}
+ >
+ Go to calendar
+
+
+
+ );
+ }
+
+ const isDownloading = !!downloadState;
+ const progress = isDownloading
+ ? Math.round((downloadState.current / downloadState.total) * 100)
+ : 0;
+
+ return (
+
+
+ navigate(-1)}>
+
+
+
+ Selected Posters ({entries.length})
+
+ }
+ onClick={handleDownload}
+ disabled={isDownloading}
+ >
+ {isDownloading ? (
+
+ {downloadState.current} / {downloadState.total}
+
+
+ ) : (
+ "Download all"
+ )}
+
+
+
+ {sortedMonths.map((monthKey) => {
+ const [year, month] = monthKey.split("-").map(Number);
+ const monthLabel = `${MONTH_NAMES[month - 1]} ${year}`;
+ return (
+
+ {monthLabel}
+
+ {groups[monthKey].map((entry) => (
+
+ navigate(
+ `/posters/${encodeURIComponent(entry.movieName)}/${encodeURIComponent(entry.movieYear)}`,
+ { state: { watchedDate: entry.watchedDate } }
+ )
+ }
+ >
+
+
+
+ {entry.movieName}
+ {entry.movieYear}
+
+
+
+
+
+
+ ))}
+
+
+ );
+ })}
+
+ );
+};
+
+export default SelectionRecap;
diff --git a/src/pages/UploadDiary.js b/src/pages/UploadDiary.js
new file mode 100644
index 0000000..d5b1bfb
--- /dev/null
+++ b/src/pages/UploadDiary.js
@@ -0,0 +1,236 @@
+import React, { useState, useEffect } from "react";
+import { useNavigate } from "react-router-dom";
+import { useDispatch, useSelector } from "react-redux";
+import {
+ Container,
+ Grid,
+ Typography,
+ CircularProgress,
+ Box,
+ Link,
+ TextField,
+ InputAdornment,
+} from "@material-ui/core";
+import { Button } from "@mui/material";
+import { CloudUploadOutlined } from "@material-ui/icons";
+import CloudDoneOutlinedIcon from "@mui/icons-material/CloudDoneOutlined";
+import SyncIcon from "@material-ui/icons/Sync";
+import { useDropzone } from "react-dropzone";
+import api from "../services/api";
+import { setUsername, setDataSource } from "../services/action";
+const VERSION = require("../../package.json").version;
+
+const UploadDiary = () => {
+ const [file, setFile] = useState(null);
+ const [uploading, setUploading] = useState(false);
+ const [usernameInput, setUsernameInput] = useState("");
+ const [syncing, setSyncing] = useState(false);
+ const [syncError, setSyncError] = useState(null);
+ const navigate = useNavigate();
+ const dispatch = useDispatch();
+ const storedUsername = useSelector((state) => state.username);
+
+ useEffect(() => {
+ if (storedUsername) setUsernameInput(storedUsername);
+ }, [storedUsername]);
+
+ useEffect(() => {
+ const checkCSVFile = async () => {
+ try {
+ const response = await api.get("/api/check-csv");
+ if (response.data.fileExists) {
+ navigate("/PosterSelector");
+ }
+ } catch (error) {
+ console.error("Error checking CSV file:", error);
+ }
+ };
+ checkCSVFile();
+ }, [navigate]);
+
+ const { getRootProps, getInputProps } = useDropzone({
+ onDrop: (acceptedFiles) => {
+ setFile(acceptedFiles[0]);
+ },
+ maxFiles: 1,
+ accept: ".csv",
+ });
+
+ const handleUpload = async () => {
+ if (!file) return;
+ setUploading(true);
+ try {
+ const formData = new FormData();
+ formData.append("file", file);
+ await api.post("/api/upload-csv", formData, {
+ headers: { "Content-Type": "multipart/form-data" },
+ });
+ dispatch(setDataSource('csv'));
+ navigate("/PosterSelector");
+ } catch (error) {
+ console.error("Error processing diary:", error);
+ } finally {
+ setUploading(false);
+ }
+ };
+
+ const handleRssSync = async () => {
+ const username = usernameInput.trim();
+ if (!username) return;
+ setSyncing(true);
+ setSyncError(null);
+ try {
+ const res = await api.post("/api/sync-rss", { username });
+ dispatch(setUsername(username));
+ if (res.data.fresh) dispatch(setDataSource('rss'));
+ navigate("/PosterSelector");
+ } catch (err) {
+ const msg = err.response?.data?.error || "Could not fetch RSS feed.";
+ setSyncError(msg);
+ } finally {
+ setSyncing(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ {/* CSV upload */}
+
+ Upload Letterboxd Diary
+
+
+
+ {file ? (
+ <>
+
+
+ Your file has been uploaded: {file.name}
+
+ >
+ ) : (
+ <>
+
+
+ Drag and drop a CSV file here or click to select
+
+ >
+ )}
+
+
+ {uploading ? (
+
+ ) : (
+
+ SUBMIT
+
+ )}
+
+
+ {/* Divider */}
+
+
+ or
+
+
+
+ {/* RSS sync */}
+
+ Sync from Letterboxd RSS
+
+
+ { setUsernameInput(e.target.value); setSyncError(null); }}
+ onKeyDown={(e) => e.key === "Enter" && handleRssSync()}
+ InputProps={{
+ startAdornment: (
+
+ letterboxd.com/
+
+ ),
+ }}
+ />
+ : }
+ >
+ {syncing ? "Syncing…" : "Sync"}
+
+
+ {syncError && (
+
{syncError}
+ )}
+
+ Syncs your ~50 most recent diary entries via the public RSS feed.
+ Use CSV export for full history.
+
+
+
+
+
+ This tool allows you to easily import your Letterboxd diary and select
+ your favorite movie poster. Follow these simple steps:
+
+ 1. Export your diary: {" "}
+
+ Export your Letterboxd data
+ {" "}
+ and extract the diary.csv file from the ZIP.
+
+ 2. Upload the CSV: Drop your diary.csv{" "}
+ file above or click to select it.
+
+ 3. Poster Selection: After processing, you'll be
+ redirected to the calendar where you can browse and choose your
+ favorite poster for each movie.
+
+
+
+
+
+ v{VERSION}
+
+
+ •
+
+ (e.currentTarget.style.opacity = 1)}
+ onMouseLeave={(e) => (e.currentTarget.style.opacity = 0.7)}
+ >
+ Hugyouu
+
+
+
+
+ );
+};
+
+export default UploadDiary;
diff --git a/src/services/action.js b/src/services/action.js
index df1181e..e1d8d98 100644
--- a/src/services/action.js
+++ b/src/services/action.js
@@ -1,3 +1,10 @@
+export const SET_USERNAME = "SET_USERNAME";
+export const setUsername = (username) => ({ type: SET_USERNAME, payload: username });
+
+export const SET_DATA_SOURCE = "SET_DATA_SOURCE";
+// source: 'csv' | 'rss'
+export const setDataSource = (source) => ({ type: SET_DATA_SOURCE, payload: source });
+
export const SELECT_POSTER = "SELECT_POSTER";
export const DESELECT_POSTER = "DESELECT_POSTER";
export const REMOVE_POSTER = "REMOVE_POSTER";
diff --git a/src/services/api.js b/src/services/api.js
index b053a9d..f8261f3 100644
--- a/src/services/api.js
+++ b/src/services/api.js
@@ -1,7 +1,22 @@
import axios from "axios";
-const API_BASE_URL = "http://localhost:5000/api";
+export const getUserId = () => {
+ let id = localStorage.getItem("poster_picker_uid");
+ if (!id) {
+ id = crypto.randomUUID();
+ localStorage.setItem("poster_picker_uid", id);
+ }
+ return id;
+};
-export const fetchMovies = () => axios.get(`${API_BASE_URL}/movies`);
-export const downloadPoster = (movieId) =>
- axios.post(`${API_BASE_URL}/download-poster`, { movie_id: movieId });
+const api = axios.create({
+ baseURL: process.env.REACT_APP_API_URL,
+ withCredentials: true,
+});
+
+api.interceptors.request.use((config) => {
+ config.headers["X-User-ID"] = getUserId();
+ return config;
+});
+
+export default api;
diff --git a/src/services/store.js b/src/services/store.js
index 210f953..86f4c6f 100644
--- a/src/services/store.js
+++ b/src/services/store.js
@@ -1,7 +1,7 @@
import { legacy_createStore as createStore, combineReducers } from "redux";
import { persistStore, persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage";
-import {DESELECT_POSTER, REMOVE_ALL_POSTERS, REMOVE_POSTER, SELECT_POSTER} from "./action";
+import { DESELECT_POSTER, REMOVE_ALL_POSTERS, REMOVE_POSTER, SELECT_POSTER, SET_USERNAME, SET_DATA_SOURCE } from "./action";
const posterSelectionReducer = (state = {}, action) => {
switch (action.type) {
@@ -9,7 +9,7 @@ const posterSelectionReducer = (state = {}, action) => {
const { movieId, posterId, watchedDate } = action.payload;
return {
...state,
- [movieId]: [...(state[movieId] || []), { posterId, watchedDate }],
+ [movieId]: [{ posterId, watchedDate }],
};
}
case DESELECT_POSTER: {
@@ -38,9 +38,22 @@ const posterSelectionReducer = (state = {}, action) => {
}
};
+const usernameReducer = (state = "", action) => {
+ switch (action.type) {
+ case SET_USERNAME: return action.payload;
+ default: return state;
+ }
+};
+
+const dataSourceReducer = (state = null, action) => {
+ if (action.type === SET_DATA_SOURCE) return action.payload;
+ return state;
+};
+
const rootReducer = combineReducers({
posterSelections: posterSelectionReducer,
- // other reducers...
+ username: usernameReducer,
+ dataSource: dataSourceReducer,
});
const persistConfig = {
diff --git a/src/styles/App.scss b/src/styles/App.scss
index 632a0ff..11258aa 100644
--- a/src/styles/App.scss
+++ b/src/styles/App.scss
@@ -1,6 +1,7 @@
@use "./scss/uploadDiary";
@use "./scss/posterSelector";
@use "./scss/posterGallery";
+@use "./scss/selectionRecap";
@use "./scss/cart";
@use "./scss/navbar";
diff --git a/src/styles/scss/_navbar.scss b/src/styles/scss/_navbar.scss
index 74095ac..fa847ee 100644
--- a/src/styles/scss/_navbar.scss
+++ b/src/styles/scss/_navbar.scss
@@ -1,32 +1,206 @@
-// _navbar.scss
+@use "sass:color";
-// Variables de couleur et autres constantes
$background-color: #1c1f23;
-$icon-color: white;
-$margin-right: 8px; // Correspondant à theme.spacing(2)
+$primary-color: #00a346;
+$text-white: #ffffff;
+$text-gray: #667788;
-.navbar {
- top: auto !important;
- bottom: 0;
+.app-header {
background-color: $background-color !important;
+ box-shadow: 0 1px 0 rgba(255, 255, 255, 0.08) !important;
- .toolbar {
+ .header-toolbar {
display: flex;
justify-content: space-between;
+ align-items: center;
+ padding: 0 1.5rem;
}
- .back-button,
- .cart-button,
- .refresh-button {
- color: $icon-color;
+ .header-left {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ cursor: pointer;
+ flex-shrink: 0;
+
+ &:hover .header-title {
+ color: $primary-color;
+ }
}
- .refresh-button {
- margin-right: $margin-right;
+ .header-logo {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ object-fit: cover;
+ flex-shrink: 0;
}
- .back-button[disabled] {
- opacity: 0.3;
- pointer-events: none;
+ .header-title {
+ color: $text-white;
+ font-weight: 700;
+ letter-spacing: 0.02em;
+ transition: color 0.15s ease;
+ }
+
+ .header-center {
+ position: absolute;
+ left: 50%;
+ transform: translateX(-50%);
+ max-width: 40%;
+ }
+
+ .header-movie-name {
+ color: $text-white;
+ font-weight: 400;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .header-username {
+ color: $text-gray;
+ font-size: 0.85rem !important;
+ letter-spacing: 0.03em;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &::before {
+ content: '@';
+ opacity: 0.6;
+ }
+ }
+
+ .header-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ }
+
+ .header-menu-btn {
+ display: none !important;
+ color: $text-gray !important;
+ &:hover { color: $text-white !important; }
+ }
+
+ @media (max-width: 640px) {
+ .header-actions { display: none !important; }
+ .header-menu-btn { display: flex !important; }
+ .header-title { font-size: 1rem !important; }
+ .header-center { display: none; }
+ }
+
+ .header-reset-btn {
+ color: $text-gray !important;
+
+ &:hover {
+ color: $text-white !important;
+ }
+ }
+
+ .header-recap-btn {
+ color: $text-gray !important;
+
+ &:hover {
+ color: $text-white !important;
+ }
+ }
+
+ .header-sync-btn {
+ color: $text-gray !important;
+
+ &:hover { color: $text-white !important; }
+
+ &--active { color: $primary-color !important; }
+ }
+
+ .spin {
+ animation: spin 1s linear infinite;
+ }
+
+ @keyframes spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+ }
+
+ .header-download-btn {
+ background-color: $primary-color !important;
+ color: $text-white !important;
+ font-weight: 600;
+ padding: 0.4rem 1rem;
+ border-radius: 4px;
+ text-transform: none;
+ font-size: 0.875rem;
+
+ &:hover {
+ background-color: color.adjust($primary-color, $lightness: -8%) !important;
+ }
+
+ &--loading {
+ background-color: color.adjust($primary-color, $lightness: -12%) !important;
+ cursor: default !important;
+ min-width: 140px;
+ }
+ }
+
+ .header-download-progress {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 4px;
+ width: 120px;
+
+ span {
+ font-size: 0.8rem;
+ font-weight: 600;
+ color: $text-white;
+ line-height: 1;
+ }
+ }
+
+ .header-progress-bar {
+ width: 100%;
+ border-radius: 2px;
+ height: 4px !important;
+
+ .MuiLinearProgress-bar {
+ background-color: $text-white !important;
+ }
+
+ &.MuiLinearProgress-root {
+ background-color: rgba(255, 255, 255, 0.3) !important;
+ }
}
}
+
+// Menu mobile (portail MUI, hors de .app-header)
+.header-menu-paper {
+ background-color: #1c1f23 !important;
+ border: 1px solid #2c3038 !important;
+ border-radius: 6px !important;
+ min-width: 200px;
+
+ .MuiList-root { padding: 4px 0; }
+}
+
+.header-menu-item {
+ color: #ffffff !important;
+ font-size: 0.875rem !important;
+ gap: 10px;
+ padding: 10px 16px !important;
+
+ &:hover { background-color: rgba(102, 119, 136, 0.2) !important; }
+ &.Mui-disabled { opacity: 0.4 !important; color: #ffffff !important; }
+
+ &--danger {
+ color: #ef9a9a !important;
+ margin-top: 4px;
+ border-top: 1px solid #2c3038;
+ }
+}
+
+.header-menu-icon {
+ opacity: 0.6;
+ flex-shrink: 0;
+}
diff --git a/src/styles/scss/_posterGallery.scss b/src/styles/scss/_posterGallery.scss
index cb8cb1f..73a5652 100644
--- a/src/styles/scss/_posterGallery.scss
+++ b/src/styles/scss/_posterGallery.scss
@@ -6,7 +6,7 @@ $hover-scale: 1.05;
$loading-height: 50vh;
.poster-gallery {
- margin-top: 2rem;
+ margin-top: 5rem;
margin-bottom: 2rem;
.content-paper {
@@ -15,6 +15,49 @@ $loading-height: 50vh;
background-color: $background-dark;
}
+ .lang-filter-bar {
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 1.25rem;
+ }
+
+ .lang-toggle-group {
+ border: 1px solid #2c3038 !important;
+ border-radius: 4px !important;
+ overflow: hidden;
+ }
+
+ .lang-toggle-btn {
+ color: #667788 !important;
+ border: none !important;
+ border-left: 1px solid #2c3038 !important;
+ padding: 4px 14px !important;
+ font-size: 0.75rem !important;
+ font-weight: 600 !important;
+ text-transform: none !important;
+ min-width: unset !important;
+ background-color: transparent !important;
+
+ &:first-child {
+ border-left: none !important;
+ }
+
+ &:hover {
+ color: #ffffff !important;
+ background-color: rgba(102, 119, 136, 0.15) !important;
+ }
+
+ &.Mui-selected {
+ color: #ffffff !important;
+ background-color: #667788 !important;
+ }
+ }
+
+ .no-posters-msg {
+ color: #667788;
+ padding: 3rem 0;
+ }
+
.title {
color: $text-white;
margin-bottom: 1.5rem;
@@ -24,30 +67,19 @@ $loading-height: 50vh;
.poster-card {
position: relative;
- height: 100%;
- display: flex;
- flex-direction: column;
background-color: transparent;
box-shadow: none;
cursor: pointer;
- transition: all 0.3s ease;
+ transition: transform 0.3s ease;
+ border-radius: 4px;
+ overflow: hidden;
&: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;
+ outline: 4px solid $primary-color;
}
.check-icon {
@@ -61,6 +93,46 @@ $loading-height: 50vh;
}
}
+ .poster-aspect-wrapper {
+ position: relative;
+ padding-top: 150%;
+ border-radius: 4px;
+ overflow: hidden;
+ }
+
+ .poster-skeleton {
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(90deg, #1c1f23 25%, #2a2e33 50%, #1c1f23 75%);
+ background-size: 200% 100%;
+ animation: skeleton-shimmer 1.4s infinite;
+ transition: opacity 0.3s ease;
+
+ &--hidden {
+ opacity: 0;
+ pointer-events: none;
+ }
+ }
+
+ .poster-image-img {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ opacity: 0;
+ transition: opacity 0.4s ease;
+
+ &--loaded {
+ opacity: 1;
+ }
+ }
+
+ @keyframes skeleton-shimmer {
+ 0% { background-position: 200% 0; }
+ 100% { background-position: -200% 0; }
+ }
+
.loading-container {
display: flex;
justify-content: center;
diff --git a/src/styles/scss/_posterSelector.scss b/src/styles/scss/_posterSelector.scss
index 4d5101f..a055f94 100644
--- a/src/styles/scss/_posterSelector.scss
+++ b/src/styles/scss/_posterSelector.scss
@@ -1,134 +1,314 @@
@use "sass:color";
-// Variables
$primary-color: #00a346;
$secondary-color: #667788;
$background-dark: #1c1f23;
$text-white: #ffffff;
$text-gray: #667788;
-$border-color: #667788;
+$border-color: #2c3038;
$hover-color: rgba(102, 119, 136, 0.2);
.poster-selector {
- margin-top: 2rem;
+ margin-top: 5rem;
margin-bottom: 2rem;
.content-paper {
padding: 1.5rem;
border-radius: 4px;
- background-color: $background-dark !important;
+ background-color: $background-dark;
+ max-width: calc((120vh - 340px) * 7 / 9 + 3rem);
+ margin: 0 auto;
}
.title {
- margin-bottom: 1.5rem;
+ margin-bottom: 1rem;
color: $text-white;
}
- .username {
- color: $primary-color;
- margin-bottom: 1rem;
- }
-
- .movies-list {
- max-height: 800px;
- min-height: 900px;
- padding: 0;
- }
-
- .movie-item {
- padding: 0 2rem;
- margin-bottom: 1rem;
- transition: background-color 0.3s ease;
+ // Calendar navigation
+ .calendar-nav {
display: flex;
align-items: center;
justify-content: space-between;
- flex-wrap: nowrap;
+ margin-bottom: 1.5rem;
+ }
+
+ .calendar-nav-center {
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+ }
+
+ .calendar-nav-btn {
+ color: $text-white !important;
&: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;
+ background-color: $hover-color !important;
}
}
- .pagination-container {
+ .calendar-month-label {
+ color: $text-white;
+ min-width: 160px;
+ text-align: center;
+ }
+
+ .calendar-today-btn {
+ color: $text-gray !important;
+ font-size: 0.75rem !important;
+ text-transform: none !important;
+ padding: 2px 8px !important;
+ border: 1px solid $text-gray !important;
+ border-radius: 4px !important;
+ min-width: unset !important;
+
+ &:hover {
+ color: $text-white !important;
+ border-color: $text-white !important;
+ }
+ }
+
+ .calendar-month-selects {
display: flex;
- justify-content: center;
- margin-top: 1.5rem;
+ align-items: center;
+ gap: 4px;
+ border: 1px solid $border-color;
+ border-radius: 4px;
+ padding: 0 4px;
- // Override MUI Pagination styles
- .MuiPagination-root {
- .MuiPaginationItem-root {
+ &:hover {
+ border-color: $text-gray;
+ }
+ }
+
+ .calendar-select {
+ color: $text-gray !important;
+ font-size: 0.8rem !important;
+
+ .MuiSelect-root {
+ padding: 4px 24px 4px 6px !important;
+ }
+
+ .MuiSelect-icon {
+ color: $text-gray;
+ }
+
+ &:hover {
+ color: $text-white !important;
+ }
+ }
+
+ .calendar-select-menu {
+ background-color: #1c1f23 !important;
+ color: $text-white !important;
+ border: 1px solid $border-color;
+
+ .MuiMenuItem-root {
+ font-size: 0.85rem;
+ color: $text-gray;
+
+ &:hover, &.Mui-selected {
+ background-color: rgba(102, 119, 136, 0.2) !important;
color: $text-white;
-
- &.Mui-selected {
- background-color: $secondary-color;
- }
}
}
}
+ .rss-notice {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 0.75rem;
+ color: $text-gray;
+ background-color: rgba(102, 119, 136, 0.08);
+ border: 1px solid rgba(102, 119, 136, 0.2);
+ border-radius: 4px;
+ padding: 6px 10px;
+ margin-bottom: 1rem;
+ }
+
+ .rss-notice-icon {
+ font-size: 0.8rem;
+ flex-shrink: 0;
+ }
+
+ .rss-notice-link {
+ color: $primary-color;
+ cursor: pointer;
+ text-decoration: underline;
+
+ &:hover { color: color.scale($primary-color, $lightness: 15%); }
+ }
+
+ // Calendar grid
+ .calendar-grid-wrapper {
+ position: relative;
+ }
+
+ .calendar-grid {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ gap: 6px;
+ max-width: calc((120vh - 340px) * 7 / 9);
+ min-width: 280px;
+ margin: 0 auto;
+ }
+
+ .calendar-day-header {
+ color: $text-gray;
+ font-size: 0.75rem;
+ font-weight: 600;
+ text-align: center;
+ text-transform: uppercase;
+ padding: 0.25rem 0;
+ letter-spacing: 0.05em;
+ }
+
+ .calendar-cell {
+ position: relative;
+ border-radius: 4px;
+ border: 1px solid $border-color;
+ overflow: hidden;
+ background-color: #14181c;
+ aspect-ratio: 2/3;
+ min-height: 14vh;
+
+ &--empty {
+ border-color: transparent;
+ background-color: transparent;
+ }
+
+ &--future {
+ background-color: rgba(255, 255, 255, 0.02);
+ border-color: rgba(255, 255, 255, 0.04);
+
+ .calendar-day-number {
+ opacity: 0.25;
+ }
+ }
+
+ &--today {
+ border-color: $secondary-color;
+
+ .calendar-day-number {
+ color: $text-white;
+ font-weight: 700;
+ }
+ }
+
+ &--has-movie {
+ cursor: pointer;
+ border-color: transparent;
+
+ &:hover .calendar-poster-overlay {
+ opacity: 1;
+ }
+
+ &:hover .calendar-day-number {
+ color: $text-white;
+ text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
+ }
+ }
+ }
+
+ .calendar-day-number {
+ position: absolute;
+ top: 4px;
+ left: 6px;
+ font-size: 0.7rem;
+ color: $text-gray;
+ z-index: 2;
+ line-height: 1;
+ }
+
+ .calendar-poster-wrapper {
+ position: absolute;
+ inset: 0;
+ }
+
+ .calendar-cell-skeleton {
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(90deg, #1c1f23 25%, #252930 50%, #1c1f23 75%);
+ background-size: 200% 100%;
+ animation: calendar-shimmer 1.4s infinite;
+ }
+
+ @keyframes calendar-shimmer {
+ 0% { background-position: 200% 0; }
+ 100% { background-position: -200% 0; }
+ }
+
+ .calendar-poster {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+ }
+
+ .calendar-poster-overlay {
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.85) 0%, rgba(0, 0, 0, 0.2) 60%, transparent 100%);
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ padding: 6px;
+ opacity: 0;
+ transition: opacity 0.2s ease;
+ }
+
+ .calendar-movie-title {
+ color: $text-white;
+ font-size: 0.65rem;
+ font-weight: 600;
+ line-height: 1.2;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ }
+
+ .calendar-movie-year {
+ color: $text-gray;
+ font-size: 0.6rem;
+ margin-top: 2px;
+ }
+
+ .calendar-more-badge {
+ position: absolute;
+ top: 4px;
+ right: 4px;
+ background-color: $primary-color;
+ color: $text-white;
+ font-size: 0.6rem;
+ font-weight: 700;
+ padding: 1px 4px;
+ border-radius: 3px;
+ z-index: 2;
+ }
+
+ .calendar-selected-badge {
+ position: absolute;
+ bottom: 4px;
+ right: 4px;
+ background-color: $primary-color;
+ color: $text-white;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 2;
+ }
+
+ .calendar-cell--selected {
+ box-shadow: 0 0 0 2px $primary-color;
+ border-radius: 4px;
+ }
+
+ // Shared states
.progress-container {
display: flex;
justify-content: center;
@@ -148,12 +328,23 @@ $hover-color: rgba(102, 119, 136, 0.2);
}
.no-movies-container {
+ position: absolute;
+ inset: 0;
+ z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
- height: 400px;
color: $text-white;
+ background: rgba(28, 31, 35, 0.7);
+ backdrop-filter: blur(6px);
+ border-radius: 4px;
+ animation: fade-in 0.3s ease;
+ }
+
+ @keyframes fade-in {
+ from { opacity: 0; }
+ to { opacity: 1; }
}
.gif-container {
@@ -193,3 +384,142 @@ $hover-color: rgba(102, 119, 136, 0.2);
}
}
}
+
+// Responsive
+@media (max-width: 768px) {
+ .poster-selector {
+ margin-top: 3.5rem !important;
+ padding-left: 0.5rem !important;
+ padding-right: 0.5rem !important;
+ }
+
+ .poster-selector .content-paper {
+ padding: 0.75rem !important;
+ max-width: 100% !important;
+ }
+
+ // Nav : deux rangées
+ // Rangée 1 : Today (gauche) | chevrons + label (droite)
+ // Rangée 2 : selects centrés
+ .poster-selector .calendar-nav {
+ flex-wrap: wrap;
+ row-gap: 8px;
+ margin-bottom: 0.75rem;
+ }
+
+ .poster-selector .calendar-today-btn {
+ order: 1;
+ }
+
+ .poster-selector .calendar-nav-center {
+ order: 2;
+ flex: 1;
+ justify-content: flex-end;
+ }
+
+ .poster-selector .calendar-month-label {
+ min-width: unset !important;
+ font-size: 0.9rem !important;
+ }
+
+ .poster-selector .calendar-month-selects {
+ order: 3;
+ width: 100%;
+ justify-content: center !important;
+ border: none !important;
+ border-top: 1px solid #2c3038 !important;
+ border-radius: 0 !important;
+ padding: 6px 0 0 !important;
+ gap: 8px !important;
+ }
+
+ // Grille
+ .poster-selector .calendar-grid {
+ gap: 3px;
+ max-width: 100%;
+ min-width: unset;
+ }
+
+ .poster-selector .calendar-day-header {
+ font-size: 0.6rem;
+ padding: 0.15rem 0;
+ }
+
+ .poster-selector .calendar-cell {
+ min-height: unset; // aspect-ratio 2/3 contrôle la hauteur
+ border-radius: 3px;
+ }
+
+ .poster-selector .calendar-day-number {
+ font-size: 0.6rem;
+ }
+
+ .poster-selector .calendar-movie-title {
+ font-size: 0.55rem;
+ }
+
+ .poster-selector .calendar-more-badge,
+ .poster-selector .calendar-selected-badge {
+ display: none;
+ }
+}
+
+// Popover multi-films (rendu en portal, hors de .poster-selector)
+.multi-movie-popover {
+ background-color: #1c1f23 !important;
+ border: 1px solid #2c3038;
+ border-radius: 6px !important;
+ overflow: hidden;
+ min-width: 220px;
+}
+
+.multi-movie-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 12px;
+ cursor: pointer;
+ border-bottom: 1px solid #2c3038;
+
+ &:last-child { border-bottom: none; }
+
+ &:hover { background-color: rgba(102, 119, 136, 0.2); }
+}
+
+.multi-movie-poster {
+ width: 36px;
+ height: 54px;
+ object-fit: cover;
+ border-radius: 2px;
+ flex-shrink: 0;
+
+ &--empty { background-color: #2c3038; }
+}
+
+.multi-movie-info {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ min-width: 0;
+}
+
+.multi-movie-title {
+ color: #ffffff;
+ font-size: 0.8rem;
+ font-weight: 500;
+ line-height: 1.2;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.multi-movie-year {
+ color: #667788;
+ font-size: 0.7rem;
+}
+
+.multi-movie-check {
+ color: #00a346 !important;
+ flex-shrink: 0;
+}
diff --git a/src/styles/scss/_selectionRecap.scss b/src/styles/scss/_selectionRecap.scss
new file mode 100644
index 0000000..29cf704
--- /dev/null
+++ b/src/styles/scss/_selectionRecap.scss
@@ -0,0 +1,187 @@
+@use "sass:color";
+
+$primary-color: #00a346;
+$background-dark: #1c1f23;
+$text-white: #ffffff;
+$text-gray: #667788;
+$border-color: #2c3038;
+
+.selection-recap {
+ margin-top: 5rem;
+ margin-bottom: 3rem;
+
+ .recap-header {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin-bottom: 2.5rem;
+ }
+
+ .recap-back-btn {
+ color: $text-gray !important;
+ &:hover { color: $text-white !important; }
+ }
+
+ .recap-title {
+ flex: 1;
+ color: $text-white;
+ font-weight: 600;
+ }
+
+ .recap-download-btn {
+ background-color: $primary-color !important;
+ color: $text-white !important;
+ font-weight: 600;
+ text-transform: none !important;
+ padding: 0.4rem 1rem;
+ border-radius: 4px;
+ font-size: 0.875rem;
+ white-space: nowrap;
+
+ &:hover { background-color: color.adjust($primary-color, $lightness: -8%) !important; }
+
+ &--loading {
+ background-color: color.adjust($primary-color, $lightness: -12%) !important;
+ cursor: default !important;
+ min-width: 150px;
+ }
+
+ &.Mui-disabled {
+ color: $text-white !important;
+ opacity: 0.8 !important;
+ }
+ }
+
+ .recap-download-progress {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 4px;
+ width: 120px;
+
+ span {
+ font-size: 0.8rem;
+ font-weight: 600;
+ color: $text-white;
+ line-height: 1;
+ }
+ }
+
+ .recap-progress-bar {
+ width: 100%;
+ border-radius: 2px;
+ height: 4px !important;
+
+ .MuiLinearProgress-bar { background-color: $text-white !important; }
+ &.MuiLinearProgress-root { background-color: rgba(255, 255, 255, 0.3) !important; }
+ }
+
+ .recap-month-section {
+ margin-bottom: 2.5rem;
+ }
+
+ .recap-month-label {
+ color: $text-gray;
+ font-size: 0.8rem !important;
+ font-weight: 700 !important;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ margin-bottom: 1rem !important;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid $border-color;
+ }
+
+ .recap-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
+ gap: 10px;
+ }
+
+ .recap-card {
+ cursor: pointer;
+ border-radius: 4px;
+ overflow: hidden;
+
+ &:hover .recap-overlay { opacity: 1; }
+ &:hover .recap-poster { transform: scale(1.04); }
+ }
+
+ .recap-poster-wrapper {
+ position: relative;
+ padding-top: 150%;
+ overflow: hidden;
+ border-radius: 4px;
+ background-color: #14181c;
+ }
+
+ .recap-poster {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ transition: transform 0.25s ease;
+ }
+
+ .recap-overlay {
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.85) 0%, rgba(0, 0, 0, 0.15) 55%, transparent 100%);
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ padding: 8px;
+ opacity: 0;
+ transition: opacity 0.2s ease;
+ }
+
+ .recap-movie-title {
+ color: $text-white;
+ font-size: 0.68rem;
+ font-weight: 600;
+ line-height: 1.2;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ }
+
+ .recap-movie-year {
+ color: rgba(255, 255, 255, 0.55);
+ font-size: 0.62rem;
+ margin-top: 2px;
+ }
+
+ .recap-check-badge {
+ position: absolute;
+ top: 6px;
+ right: 6px;
+ background-color: $primary-color;
+ color: $text-white;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 2;
+ }
+
+ .recap-empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 60vh;
+ gap: 1.5rem;
+ color: $text-gray;
+
+ .MuiTypography-root { color: $text-gray; }
+ }
+
+ .recap-go-btn {
+ background-color: $primary-color !important;
+ color: $text-white !important;
+ text-transform: none !important;
+ }
+}
diff --git a/src/styles/scss/_uploadDiary.scss b/src/styles/scss/_uploadDiary.scss
index 778be6d..6505ece 100644
--- a/src/styles/scss/_uploadDiary.scss
+++ b/src/styles/scss/_uploadDiary.scss
@@ -1,12 +1,36 @@
-.upload-diary {
- padding: 3rem;
+.upload-page {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
background-color: #14181c;
- min-height: calc(100vh - 60px);
+}
+
+.upload-diary {
+ flex: 1;
+ padding: 3rem;
+ padding-top: 5rem;
display: flex !important;
flex-direction: column;
align-items: center;
justify-content: center;
+ @media (max-width: 600px) {
+ padding: 1.25rem;
+ padding-top: 4.5rem;
+ justify-content: flex-start;
+ }
+
+ .upload-card-wrapper {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ @media (max-width: 600px) {
+ min-height: calc(100svh - 80px);
+ }
+ }
+
.upload-card {
width: 100%;
max-width: 600px;
@@ -17,6 +41,11 @@
padding-top: 4rem;
position: relative;
+ @media (max-width: 600px) {
+ padding: 1.25rem;
+ padding-top: 3rem;
+ }
+
.logo-container {
position: absolute;
top: -80px;
@@ -24,11 +53,20 @@
transform: translate(-50%);
z-index: 10;
+ @media (max-width: 600px) {
+ top: -55px;
+ }
+
.logo {
width: 150px;
height: 150px;
border-radius: 50%;
object-fit: cover;
+
+ @media (max-width: 600px) {
+ width: 100px;
+ height: 100px;
+ }
}
}
@@ -36,35 +74,11 @@
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;
- }
+ @media (max-width: 600px) {
+ font-size: 1.4rem !important;
+ margin-bottom: 1rem;
}
-
- .MuiInputLabel-root {
- color: #ffffff !important;
- }
- }
-
- .or-text {
- color: #ffffff;
- margin-bottom: 1rem;
}
.dropzone {
@@ -74,6 +88,10 @@
text-align: center;
cursor: pointer;
+ @media (max-width: 600px) {
+ padding: 1.25rem;
+ }
+
&:hover {
background-color: #1f252a;
}
@@ -82,11 +100,20 @@
margin-bottom: 1rem;
color: #00a346;
font-size: 4rem;
+
+ @media (max-width: 600px) {
+ font-size: 2.5rem;
+ margin-bottom: 0.5rem;
+ }
}
.dropzone-text {
margin-bottom: 1rem;
color: #ffffff;
+
+ @media (max-width: 600px) {
+ font-size: 0.9rem !important;
+ }
}
}
@@ -94,6 +121,101 @@
margin-top: 1rem;
}
+ .upload-divider {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ margin: 1.75rem 0 1.25rem;
+
+ @media (max-width: 600px) {
+ margin: 1.25rem 0 1rem;
+ }
+ }
+
+ .upload-divider-line {
+ flex: 1;
+ height: 1px;
+ background-color: #2c3038;
+ }
+
+ .upload-divider-text {
+ color: #526e89;
+ font-size: 0.75rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ }
+
+ .rss-label {
+ color: #ffffff !important;
+ font-size: 0.9rem !important;
+ font-weight: 600 !important;
+ margin-bottom: 0.75rem !important;
+ }
+
+ .rss-row {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ }
+
+ .rss-input {
+ flex: 1;
+ min-width: 0;
+
+ .MuiOutlinedInput-root {
+ background-color: #14181c;
+ border-radius: 4px;
+ color: #ffffff;
+ font-size: 0.875rem;
+
+ fieldset { border-color: #2c3038; }
+ &:hover fieldset { border-color: #526e89; }
+ &.Mui-focused fieldset { border-color: #00a346; }
+
+ input { padding: 8px 10px 8px 4px; color: #ffffff; }
+ input::placeholder { color: #526e89; }
+ }
+ }
+
+ .rss-url-prefix {
+ color: #526e89;
+ font-size: 0.8rem;
+ white-space: nowrap;
+ user-select: none;
+
+ @media (max-width: 400px) {
+ display: none;
+ }
+ }
+
+ .rss-sync-button {
+ background-color: #00a346 !important;
+ color: #ffffff !important;
+ font-weight: 700 !important;
+ text-transform: none !important;
+ padding: 7px 16px !important;
+ border-radius: 4px !important;
+ white-space: nowrap;
+ font-size: 0.875rem !important;
+
+ &:hover { background-color: #008a39 !important; }
+ &:disabled { opacity: 0.6 !important; }
+ }
+
+ .rss-note {
+ color: #526e89 !important;
+ font-size: 0.72rem !important;
+ margin-top: 0.5rem !important;
+ line-height: 1.4 !important;
+ }
+
+ .rss-error {
+ color: #e57373 !important;
+ font-size: 0.78rem !important;
+ margin-top: 0.4rem !important;
+ }
+
.submit-button {
color: #fff;
font-size: 0.8rem;
@@ -125,6 +247,15 @@
color: #456;
margin-top: 2rem;
padding: 1.5rem;
+ max-width: 600px;
+ width: 100%;
+
+ @media (max-width: 600px) {
+ padding: 1rem 0.25rem;
+ margin-top: 1.25rem;
+ font-size: 0.85rem !important;
+ // display: block;
+ }
}
}
@@ -142,7 +273,7 @@
.link {
text-decoration: none;
- color: inherit
+ color: inherit;
}
}
-}
\ No newline at end of file
+}