17 Commits

Author SHA1 Message Date
72bdbaf947 fix(deps): update react monorepo to v19
Some checks failed
renovate/artifacts Artifact file update failure
2025-06-11 17:01:47 +00:00
ngn
f717a6b610 ups: update to dded1e5
Some checks failed
docker / docker (push) Successful in 1m33s
ups / ups (push) Failing after 39s
2025-06-10 09:48:19 +03:00
zyachel
648d5cde32 fix(media): fix videos not playing
fixes mimetype
also adds other playable trailers, if they exist
2025-06-10 09:48:05 +03:00
Bnyro
288ec34fb4 feat: redirect paths from specific languages 2025-06-10 09:48:00 +03:00
zyachel
4e03757d69 chore(build): add sharp to trusted dep 2025-06-10 09:47:54 +03:00
zyachel
f92035ed8e feat(opensearch): add abilility to add libremdb as search engine
thanks to @user451421541757324

re https://github.com/zyachel/libremdb/issues/95
2025-06-10 09:47:49 +03:00
zyachel
0e2117e4c7 fix(lockfile): update lockfile 2025-06-10 09:47:42 +03:00
zyachel
ec6c0a9a13 chore(release): 4.2.0 2025-06-10 09:47:33 +03:00
zyachel
34bcdc3b05 fix(name,title): handle empty states in "did you know" section 2025-06-10 09:47:11 +03:00
zyachel
e98ab85034 feat(title): add "interests" in basic info 2025-06-10 09:47:05 +03:00
zyachel
255fbb521f fix(title): fix title route crash
was due to an upstream change.
also show ai summary, ratings distrubution, and multiple user
reviews

re https://github.com/zyachel/libremdb/issues/98
2025-06-10 09:46:57 +03:00
zyachel
ac60bda3bd fix(error): add trace to browser in dev mode
also make AppError a bit easier to use
2025-06-10 09:46:37 +03:00
ngn
bde980536d fix the example docker compose file
Some checks failed
ups / ups (push) Failing after 39s
Signed-off-by: ngn <ngn@ngn.tf>
2025-05-13 01:51:13 +03:00
ngn
e6ebf6ca78 remove the depend docker command
All checks were successful
docker / docker (push) Successful in 1m39s
Signed-off-by: ngn <ngn@ngn.tf>
2025-05-13 01:43:26 +03:00
ngn
9a16fa65c9 attempt to fix the broken pnpm docker command
All checks were successful
docker / docker (push) Successful in 1m46s
Signed-off-by: ngn <ngn@ngn.tf>
2025-05-13 01:35:05 +03:00
ngn
8ba89d1885 fix the pnpm-lock issue
Some checks failed
docker / docker (push) Failing after 12s
Signed-off-by: ngn <ngn@ngn.tf>
2025-05-13 01:29:21 +03:00
ngn
10772a8d6f add the ups workflow
Some checks failed
docker / docker (push) Failing after 20s
Signed-off-by: ngn <ngn@ngn.tf>
2025-05-13 01:09:41 +03:00
29 changed files with 1445 additions and 1055 deletions

View File

@@ -1,28 +1,34 @@
name: Build and publish the docker image
name: docker
on:
push:
branches: ["custom"]
branches:
- "main"
paths-ignore:
- "README.md"
- "LICENSE.txt"
- "docker-compose.example.yml"
- "ups.json"
env:
REGISTRY: git.ngn.tf
IMAGE: ${{gitea.repository}}
jobs:
build:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: "https://github.com/actions/checkout@v4"
uses: actions/checkout@v4
- name: Login to container repo
uses: "https://github.com/docker/login-action@v1"
uses: docker/login-action@v1
with:
registry: ${{env.REGISTRY}}
username: ${{gitea.actor}}
password: ${{secrets.PACKAGES_TOKEN}}
- name: Build image
- name: Build docker image
run: |
docker build . --tag ${{env.REGISTRY}}/${{env.IMAGE}}:latest
docker push ${{env.REGISTRY}}/${{env.IMAGE}}:latest

25
.gitea/workflows/ups.yml Normal file
View File

@@ -0,0 +1,25 @@
name: ups
on:
schedule:
- cron: "@weekly"
jobs:
ups:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt update -y
sudo apt install -y python3 python3-build python3-requests make
- name: Install ups
run: |
git clone https://git.ngn.tf/ngn/ups && cd ups
make && make install
- name: Run ups
run: PATH=~/.local/bin:$PATH ups-check

View File

@@ -1,20 +1,12 @@
FROM node:lts-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm
RUN pnpm install --frozen-lockfile
FROM node:lts-alpine AS builder
WORKDIR /app
COPY . .
COPY --from=deps /app/node_modules ./node_modules
ENV NODE_ENV=production
RUN npm install -g pnpm
RUN pnpm install
RUN pnpm build
FROM node:lts-alpine AS runner

View File

@@ -1,5 +1,7 @@
# [ngn.tf] | libremdb
# libremdb - IMDb frontend
![](https://git.ngn.tf/ngn/libremdb/actions/workflows/build.yml/badge.svg)
![](https://git.ngn.tf/ngn/libremdb/actions/workflows/docker.yml/badge.svg)
![](https://git.ngn.tf/ngn/libremdb/actions/workflows/ups.yml/badge.svg)
A fork of the [libremdb](https://github.com/zyachel/libremdb) project, with my personal changes.
A fork of the [libremdb](https://github.com/zyachel/libremdb) project, with my
personal changes.

View File

@@ -3,16 +3,16 @@ services:
container_name: libremdb
image: git.ngn.tf/ngn/libremdb
ports:
- 80:3000
- 80:3000
env_file: .env.example
depends_on:
- libremdb_redis
- libremdb_redis
tmpfs:
- /opt/app/.next/cache/:size=10M,mode=0770,uid=65534,gid=65534,noexec,nosuid,nodev
- /app/.next/cache/:size=10M,mode=0770,uid=65534,gid=65534,noexec,nosuid,nodev
security_opt:
- no-new-privileges:true
- no-new-privileges:true
cap_drop:
- ALL
- ALL
user: 65534:65534 # equivalent to the nobody user
read_only: true
restart: unless-stopped
@@ -22,10 +22,10 @@ services:
image: redis
user: nobody
tmpfs:
- /data:size=10M,mode=0770,uid=65534,gid=65534,noexec,nosuid,nodev
- /data:size=10M,mode=0770,uid=65534,gid=65534,noexec,nosuid,nodev
security_opt:
- no-new-privileges:true
- no-new-privileges:true
cap_drop:
- ALL
- ALL
read_only: true
restart: unless-stopped

View File

@@ -9,6 +9,11 @@ const nextConfig = {
destination: '/find',
permanent: true,
},
{
source: '/:langcode(\\w{2})/:slug*',
destination: '/:slug*',
permanent: true,
},
];
},
images: {

View File

@@ -1,6 +1,6 @@
{
"name": "libremdb",
"version": "4.1.0",
"version": "4.2.0",
"description": "a free & open source IMDb front-end",
"private": true,
"type": "module",
@@ -21,15 +21,15 @@
"cheerio": "1.0.0-rc.12",
"ioredis": "^5.3.2",
"next": "12.2.5",
"react": "18.2.0",
"react-dom": "18.2.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"sharp": "^0.33.1"
},
"devDependencies": {
"@types/node": "18.7.3",
"@types/react": "18.0.17",
"@types/react-dom": "18.0.6",
"eslint": "8.57.1",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"eslint": "8.22.0",
"eslint-config-next": "12.2.5",
"sass": "^1.62.1",
"typescript": "4.7.4"

262
pnpm-lock.yaml generated
View File

@@ -40,11 +40,11 @@ importers:
specifier: 18.0.6
version: 18.0.6
eslint:
specifier: 8.57.1
version: 8.57.1
specifier: 8.22.0
version: 8.22.0
eslint-config-next:
specifier: 12.2.5
version: 12.2.5(eslint@8.57.1)(typescript@4.7.4)
version: 12.2.5(eslint@8.22.0)(typescript@4.7.4)
sass:
specifier: ^1.62.1
version: 1.62.1
@@ -61,36 +61,19 @@ packages:
'@emnapi/runtime@0.44.0':
resolution: {integrity: sha512-ZX/etZEZw8DR7zAB1eVQT40lNo0jeqpb6dCgOvctB6FIQ5PoXfMuNY8+ayQfu8tNQbAB8gQWSSJupR8NxeiZXw==}
'@eslint-community/eslint-utils@4.4.1':
resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
'@eslint-community/regexpp@4.12.1':
resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
'@eslint/eslintrc@2.1.4':
resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==}
'@eslint/eslintrc@1.4.1':
resolution: {integrity: sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
'@eslint/js@8.57.1':
resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
'@humanwhocodes/config-array@0.13.0':
resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
'@humanwhocodes/config-array@0.10.7':
resolution: {integrity: sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==}
engines: {node: '>=10.10.0'}
deprecated: Use @eslint/config-array instead
'@humanwhocodes/module-importer@1.0.1':
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
engines: {node: '>=12.22'}
'@humanwhocodes/gitignore-to-minimatch@1.0.2':
resolution: {integrity: sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==}
'@humanwhocodes/object-schema@2.0.3':
resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
deprecated: Use @eslint/object-schema instead
'@humanwhocodes/object-schema@1.2.1':
resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==}
'@img/sharp-darwin-arm64@0.33.1':
resolution: {integrity: sha512-esr2BZ1x0bo+wl7Gx2hjssYhjrhUsD88VQulI0FrG8/otRQUOxLWHMBd1Y1qo2Gfg2KUvXNpT0ASnV9BzJCexw==}
@@ -359,16 +342,13 @@ packages:
resolution: {integrity: sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
'@ungap/structured-clone@1.2.1':
resolution: {integrity: sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
acorn@8.14.0:
resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==}
acorn@8.8.2:
resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==}
engines: {node: '>=0.4.0'}
hasBin: true
@@ -500,7 +480,7 @@ packages:
engines: {node: '>= 0.8'}
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
@@ -680,26 +660,31 @@ packages:
peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8
eslint-scope@7.2.2:
resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==}
eslint-scope@7.2.0:
resolution: {integrity: sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
eslint-utils@3.0.0:
resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==}
engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0}
peerDependencies:
eslint: '>=5'
eslint-visitor-keys@2.1.0:
resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==}
engines: {node: '>=10'}
eslint-visitor-keys@3.4.0:
resolution: {integrity: sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
eslint-visitor-keys@3.4.3:
resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
eslint@8.22.0:
resolution: {integrity: sha512-ci4t0sz6vSRKdmkOGmprBo6fmI4PrphDFMy5JEq/fNS0gQkJM3rLmrqcp8ipMcdobH3KtUP40KniAE9W19S4wA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
eslint@8.57.1:
resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
hasBin: true
espree@9.6.1:
resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==}
espree@9.5.1:
resolution: {integrity: sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
esquery@1.5.0:
@@ -784,6 +769,9 @@ packages:
resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==}
engines: {node: '>= 0.4'}
functional-red-black-tree@1.0.1:
resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==}
functions-have-names@1.2.3:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
@@ -804,11 +792,9 @@ packages:
glob@7.1.7:
resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==}
deprecated: Glob versions prior to v9 are no longer supported
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported
globals@13.20.0:
resolution: {integrity: sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==}
@@ -825,8 +811,8 @@ packages:
gopd@1.0.1:
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
grapheme-splitter@1.0.4:
resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==}
has-bigints@1.0.2:
resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==}
@@ -874,7 +860,6 @@ packages:
inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
@@ -942,10 +927,6 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
is-path-inside@3.0.3:
resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
engines: {node: '>=8'}
is-regex@1.1.4:
resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==}
engines: {node: '>= 0.4'}
@@ -1133,8 +1114,8 @@ packages:
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
optionator@0.9.1:
resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==}
engines: {node: '>= 0.8.0'}
p-limit@3.1.0:
@@ -1230,6 +1211,10 @@ packages:
resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==}
engines: {node: '>= 0.4'}
regexpp@3.2.0:
resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==}
engines: {node: '>=8'}
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -1248,7 +1233,6 @@ packages:
rimraf@3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
run-parallel@1.2.0:
@@ -1374,9 +1358,6 @@ packages:
tslib@2.5.0:
resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==}
tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
tsutils@3.21.0:
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
engines: {node: '>= 6'}
@@ -1410,6 +1391,9 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
v8-compile-cache@2.3.0:
resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==}
which-boxed-primitive@1.0.2:
resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
@@ -1425,8 +1409,8 @@ packages:
engines: {node: '>= 8'}
hasBin: true
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
word-wrap@1.2.3:
resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==}
engines: {node: '>=0.10.0'}
wrappy@1.0.2:
@@ -1447,21 +1431,14 @@ snapshots:
'@emnapi/runtime@0.44.0':
dependencies:
tslib: 2.6.2
tslib: 2.5.0
optional: true
'@eslint-community/eslint-utils@4.4.1(eslint@8.57.1)':
dependencies:
eslint: 8.57.1
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.1': {}
'@eslint/eslintrc@2.1.4':
'@eslint/eslintrc@1.4.1':
dependencies:
ajv: 6.12.6
debug: 4.3.4
espree: 9.6.1
espree: 9.5.1
globals: 13.20.0
ignore: 5.2.4
import-fresh: 3.3.0
@@ -1471,19 +1448,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@eslint/js@8.57.1': {}
'@humanwhocodes/config-array@0.13.0':
'@humanwhocodes/config-array@0.10.7':
dependencies:
'@humanwhocodes/object-schema': 2.0.3
'@humanwhocodes/object-schema': 1.2.1
debug: 4.3.4
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
'@humanwhocodes/module-importer@1.0.1': {}
'@humanwhocodes/gitignore-to-minimatch@1.0.2': {}
'@humanwhocodes/object-schema@2.0.3': {}
'@humanwhocodes/object-schema@1.2.1': {}
'@img/sharp-darwin-arm64@0.33.1':
optionalDependencies:
@@ -1643,13 +1618,13 @@ snapshots:
'@types/scheduler@0.16.3': {}
'@typescript-eslint/parser@5.59.1(eslint@8.57.1)(typescript@4.7.4)':
'@typescript-eslint/parser@5.59.1(eslint@8.22.0)(typescript@4.7.4)':
dependencies:
'@typescript-eslint/scope-manager': 5.59.1
'@typescript-eslint/types': 5.59.1
'@typescript-eslint/typescript-estree': 5.59.1(typescript@4.7.4)
debug: 4.3.4
eslint: 8.57.1
eslint: 8.22.0
optionalDependencies:
typescript: 4.7.4
transitivePeerDependencies:
@@ -1681,13 +1656,11 @@ snapshots:
'@typescript-eslint/types': 5.59.1
eslint-visitor-keys: 3.4.0
'@ungap/structured-clone@1.2.1': {}
acorn-jsx@5.3.2(acorn@8.14.0):
acorn-jsx@5.3.2(acorn@8.8.2):
dependencies:
acorn: 8.14.0
acorn: 8.8.2
acorn@8.14.0: {}
acorn@8.8.2: {}
ajv@6.12.6:
dependencies:
@@ -2015,18 +1988,18 @@ snapshots:
escape-string-regexp@4.0.0: {}
eslint-config-next@12.2.5(eslint@8.57.1)(typescript@4.7.4):
eslint-config-next@12.2.5(eslint@8.22.0)(typescript@4.7.4):
dependencies:
'@next/eslint-plugin-next': 12.2.5
'@rushstack/eslint-patch': 1.2.0
'@typescript-eslint/parser': 5.59.1(eslint@8.57.1)(typescript@4.7.4)
eslint: 8.57.1
'@typescript-eslint/parser': 5.59.1(eslint@8.22.0)(typescript@4.7.4)
eslint: 8.22.0
eslint-import-resolver-node: 0.3.7
eslint-import-resolver-typescript: 2.7.1(eslint-plugin-import@2.27.5(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.1(eslint@8.57.1)(typescript@4.7.4))(eslint-import-resolver-typescript@2.7.1)(eslint@8.57.1)
eslint-plugin-jsx-a11y: 6.7.1(eslint@8.57.1)
eslint-plugin-react: 7.32.2(eslint@8.57.1)
eslint-plugin-react-hooks: 4.6.0(eslint@8.57.1)
eslint-import-resolver-typescript: 2.7.1(eslint-plugin-import@2.27.5(eslint@8.22.0))(eslint@8.22.0)
eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.1(eslint@8.22.0)(typescript@4.7.4))(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.27.5(eslint@8.22.0))(eslint@8.22.0))(eslint@8.22.0)
eslint-plugin-jsx-a11y: 6.7.1(eslint@8.22.0)
eslint-plugin-react: 7.32.2(eslint@8.22.0)
eslint-plugin-react-hooks: 4.6.0(eslint@8.22.0)
optionalDependencies:
typescript: 4.7.4
transitivePeerDependencies:
@@ -2041,11 +2014,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.27.5(eslint@8.57.1))(eslint@8.57.1):
eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.27.5(eslint@8.22.0))(eslint@8.22.0):
dependencies:
debug: 4.3.4
eslint: 8.57.1
eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.1(eslint@8.57.1)(typescript@4.7.4))(eslint-import-resolver-typescript@2.7.1)(eslint@8.57.1)
eslint: 8.22.0
eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.1(eslint@8.22.0)(typescript@4.7.4))(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.27.5(eslint@8.22.0))(eslint@8.22.0))(eslint@8.22.0)
glob: 7.2.3
is-glob: 4.0.3
resolve: 1.22.2
@@ -2053,27 +2026,27 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.8.0(@typescript-eslint/parser@5.59.1(eslint@8.57.1)(typescript@4.7.4))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.27.5(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
eslint-module-utils@2.8.0(@typescript-eslint/parser@5.59.1(eslint@8.22.0)(typescript@4.7.4))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.27.5(eslint@8.22.0))(eslint@8.22.0))(eslint@8.22.0):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 5.59.1(eslint@8.57.1)(typescript@4.7.4)
eslint: 8.57.1
'@typescript-eslint/parser': 5.59.1(eslint@8.22.0)(typescript@4.7.4)
eslint: 8.22.0
eslint-import-resolver-node: 0.3.7
eslint-import-resolver-typescript: 2.7.1(eslint-plugin-import@2.27.5(eslint@8.57.1))(eslint@8.57.1)
eslint-import-resolver-typescript: 2.7.1(eslint-plugin-import@2.27.5(eslint@8.22.0))(eslint@8.22.0)
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.59.1(eslint@8.57.1)(typescript@4.7.4))(eslint-import-resolver-typescript@2.7.1)(eslint@8.57.1):
eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.59.1(eslint@8.22.0)(typescript@4.7.4))(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.27.5(eslint@8.22.0))(eslint@8.22.0))(eslint@8.22.0):
dependencies:
array-includes: 3.1.6
array.prototype.flat: 1.3.1
array.prototype.flatmap: 1.3.1
debug: 3.2.7
doctrine: 2.1.0
eslint: 8.57.1
eslint: 8.22.0
eslint-import-resolver-node: 0.3.7
eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.59.1(eslint@8.57.1)(typescript@4.7.4))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.27.5(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.59.1(eslint@8.22.0)(typescript@4.7.4))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.27.5(eslint@8.22.0))(eslint@8.22.0))(eslint@8.22.0)
has: 1.0.3
is-core-module: 2.12.0
is-glob: 4.0.3
@@ -2083,13 +2056,13 @@ snapshots:
semver: 6.3.0
tsconfig-paths: 3.14.2
optionalDependencies:
'@typescript-eslint/parser': 5.59.1(eslint@8.57.1)(typescript@4.7.4)
'@typescript-eslint/parser': 5.59.1(eslint@8.22.0)(typescript@4.7.4)
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
- supports-color
eslint-plugin-jsx-a11y@6.7.1(eslint@8.57.1):
eslint-plugin-jsx-a11y@6.7.1(eslint@8.22.0):
dependencies:
'@babel/runtime': 7.21.0
aria-query: 5.1.3
@@ -2100,7 +2073,7 @@ snapshots:
axobject-query: 3.1.1
damerau-levenshtein: 1.0.8
emoji-regex: 9.2.2
eslint: 8.57.1
eslint: 8.22.0
has: 1.0.3
jsx-ast-utils: 3.3.3
language-tags: 1.0.5
@@ -2109,17 +2082,17 @@ snapshots:
object.fromentries: 2.0.6
semver: 6.3.0
eslint-plugin-react-hooks@4.6.0(eslint@8.57.1):
eslint-plugin-react-hooks@4.6.0(eslint@8.22.0):
dependencies:
eslint: 8.57.1
eslint: 8.22.0
eslint-plugin-react@7.32.2(eslint@8.57.1):
eslint-plugin-react@7.32.2(eslint@8.22.0):
dependencies:
array-includes: 3.1.6
array.prototype.flatmap: 1.3.1
array.prototype.tosorted: 1.1.1
doctrine: 2.1.0
eslint: 8.57.1
eslint: 8.22.0
estraverse: 5.3.0
jsx-ast-utils: 3.3.3
minimatch: 3.1.2
@@ -2132,63 +2105,69 @@ snapshots:
semver: 6.3.0
string.prototype.matchall: 4.0.8
eslint-scope@7.2.2:
eslint-scope@7.2.0:
dependencies:
esrecurse: 4.3.0
estraverse: 5.3.0
eslint-utils@3.0.0(eslint@8.22.0):
dependencies:
eslint: 8.22.0
eslint-visitor-keys: 2.1.0
eslint-visitor-keys@2.1.0: {}
eslint-visitor-keys@3.4.0: {}
eslint-visitor-keys@3.4.3: {}
eslint@8.57.1:
eslint@8.22.0:
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1)
'@eslint-community/regexpp': 4.12.1
'@eslint/eslintrc': 2.1.4
'@eslint/js': 8.57.1
'@humanwhocodes/config-array': 0.13.0
'@humanwhocodes/module-importer': 1.0.1
'@nodelib/fs.walk': 1.2.8
'@ungap/structured-clone': 1.2.1
'@eslint/eslintrc': 1.4.1
'@humanwhocodes/config-array': 0.10.7
'@humanwhocodes/gitignore-to-minimatch': 1.0.2
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.3
debug: 4.3.4
doctrine: 3.0.0
escape-string-regexp: 4.0.0
eslint-scope: 7.2.2
eslint-visitor-keys: 3.4.3
espree: 9.6.1
eslint-scope: 7.2.0
eslint-utils: 3.0.0(eslint@8.22.0)
eslint-visitor-keys: 3.4.0
espree: 9.5.1
esquery: 1.5.0
esutils: 2.0.3
fast-deep-equal: 3.1.3
file-entry-cache: 6.0.1
find-up: 5.0.0
functional-red-black-tree: 1.0.1
glob-parent: 6.0.2
globals: 13.20.0
graphemer: 1.4.0
globby: 11.1.0
grapheme-splitter: 1.0.4
ignore: 5.2.4
import-fresh: 3.3.0
imurmurhash: 0.1.4
is-glob: 4.0.3
is-path-inside: 3.0.3
js-yaml: 4.1.0
json-stable-stringify-without-jsonify: 1.0.1
levn: 0.4.1
lodash.merge: 4.6.2
minimatch: 3.1.2
natural-compare: 1.4.0
optionator: 0.9.4
optionator: 0.9.1
regexpp: 3.2.0
strip-ansi: 6.0.1
strip-json-comments: 3.1.1
text-table: 0.2.0
v8-compile-cache: 2.3.0
transitivePeerDependencies:
- supports-color
espree@9.6.1:
espree@9.5.1:
dependencies:
acorn: 8.14.0
acorn-jsx: 5.3.2(acorn@8.14.0)
eslint-visitor-keys: 3.4.3
acorn: 8.8.2
acorn-jsx: 5.3.2(acorn@8.8.2)
eslint-visitor-keys: 3.4.0
esquery@1.5.0:
dependencies:
@@ -2266,6 +2245,8 @@ snapshots:
es-abstract: 1.21.2
functions-have-names: 1.2.3
functional-red-black-tree@1.0.1: {}
functions-have-names@1.2.3: {}
get-intrinsic@1.2.0:
@@ -2326,7 +2307,7 @@ snapshots:
dependencies:
get-intrinsic: 1.2.0
graphemer@1.4.0: {}
grapheme-splitter@1.0.4: {}
has-bigints@1.0.2: {}
@@ -2445,8 +2426,6 @@ snapshots:
is-number@7.0.0: {}
is-path-inside@3.0.3: {}
is-regex@1.1.4:
dependencies:
call-bind: 1.0.2
@@ -2644,14 +2623,14 @@ snapshots:
dependencies:
wrappy: 1.0.2
optionator@0.9.4:
optionator@0.9.1:
dependencies:
deep-is: 0.1.4
fast-levenshtein: 2.0.6
levn: 0.4.1
prelude-ls: 1.2.1
type-check: 0.4.0
word-wrap: 1.2.5
word-wrap: 1.2.3
p-limit@3.1.0:
dependencies:
@@ -2736,6 +2715,8 @@ snapshots:
define-properties: 1.2.0
functions-have-names: 1.2.3
regexpp@3.2.0: {}
resolve-from@4.0.0: {}
resolve@1.22.2:
@@ -2902,9 +2883,6 @@ snapshots:
tslib@2.5.0: {}
tslib@2.6.2:
optional: true
tsutils@3.21.0(typescript@4.7.4):
dependencies:
tslib: 1.14.1
@@ -2939,6 +2917,8 @@ snapshots:
dependencies:
react: 18.2.0
v8-compile-cache@2.3.0: {}
which-boxed-primitive@1.0.2:
dependencies:
is-bigint: 1.0.4
@@ -2967,7 +2947,7 @@ snapshots:
dependencies:
isexe: 2.0.0
word-wrap@1.2.5: {}
word-wrap@1.2.3: {}
wrappy@1.0.2: {}

2
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,2 @@
onlyBuiltDependencies:
- sharp

7
public/opensearch.xml Normal file
View File

@@ -0,0 +1,7 @@
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>libremdb</ShortName>
<Description>Search libremdb</Description>
<InputEncoding>UTF-8</InputEncoding>
<Image width="16" height="16" type="image/x-icon">https://libremdb.iket.me/favicon.ico</Image>
<Url type="text/html" method="get" template="https://libremdb.iket.me/find?q={searchTerms}"/>
</OpenSearchDescription>

View File

@@ -11,6 +11,7 @@ type Props = {
message: string;
statusCode?: number;
originalPath?: string;
stack?: string;
/** props specific to error boundary. */
misc?: {
subtext: string;
@@ -19,7 +20,9 @@ type Props = {
};
};
const ErrorInfo = ({ message, statusCode, misc, originalPath }: Props) => {
const isDev = process.env.NODE_ENV === 'development';
const ErrorInfo = ({ message, statusCode, misc, originalPath, stack }: Props) => {
const title = statusCode ? `${message} (${statusCode})` : message;
return (
<>
@@ -39,6 +42,11 @@ const ErrorInfo = ({ message, statusCode, misc, originalPath }: Props) => {
<use href='/svg/sadgnu.svg#sad-gnu'></use>
</svg>
<h1 className={`heading heading__primary ${styles.heading}`}>{title}</h1>
{Boolean(stack && isDev) && (
<pre className={styles.stack}>
<code>{stack}</code>
</pre>
)}
{misc ? (
<>
<p>{misc.subtext}</p>
@@ -64,6 +72,13 @@ const ErrorInfo = ({ message, statusCode, misc, originalPath }: Props) => {
.
</p>
)}
<p>
If you think this shouldn't happen,{' '}
<Link href='/contact'>
<a className='link'>let it be known</a>
</Link>
.
</p>
</Layout>
</>
);

View File

@@ -14,32 +14,41 @@ type Props = {
const Media = ({ className, media }: Props) => {
return (
<div className={`${className} ${styles.media}`}>
{(media.trailer || !!media.videos.total) && (
{(media.trailers?.length || !!media.videos.total) && (
<section className={styles.videos}>
<h2 className='heading heading__secondary'>Videos</h2>
<div className={styles.videos__container}>
{media.trailer && (
<div className={styles.trailer}>
{media.trailers?.map(trailer => (
<div className={styles.trailer} key={trailer.id}>
<video
aria-label='trailer video'
aria-label={trailer.caption ?? 'trailer video'}
controls
playsInline
poster={getProxiedIMDbImgUrl(modifyIMDbImg(media.trailer.thumbnail))}
poster={getProxiedIMDbImgUrl(modifyIMDbImg(trailer.thumbnail))}
className={styles.trailer__video}
preload='none'
muted
>
{media.trailer.urls.map(source => (
{trailer.urls.map(source => (
<source
key={source.url}
type={source.mimeType ?? undefined}
type='video/mp4'
src={getProxiedIMDbImgUrl(source.url)}
media={source.resolution !== 'SD' ? '(min-width: 450px)' : undefined}
data-res={source.resolution}
/>
))}
<p>
{trailer.caption}:{' '}
<Link href={getProxiedIMDbImgUrl(trailer.urls[0]?.url)}>
<a className='link'>link</a>
</Link>
</p>
</video>
</div>
)}
))}
{!!media.videos.total &&
media.videos.videos.map(video => (

View File

@@ -36,6 +36,13 @@ const Meta = ({
<meta property='og:locale' content='en_US' />
<meta property='og:type' content='video.movie' />
<meta property='og:image' content={url.toString()} />
<link
rel='search'
type='application/opensearchdescription+xml'
href='/opensearch.xml'
title='libremdb'
></link>
</Head>
);
};

View File

@@ -9,43 +9,54 @@ type Props = {
const DidYouKnow = ({ data }: Props) => (
<section className={styles.container}>
<h2 className='heading heading__secondary'>Did you know</h2>
{!!data.trivia?.total && (
<section>
<h3 className='heading heading__tertiary'>Trivia</h3>
<div dangerouslySetInnerHTML={{ __html: data.trivia.html }}></div>
</section>
)}
{!!data.quotes?.total && (
<section>
<h3 className='heading heading__tertiary'>Quotes</h3>
<div dangerouslySetInnerHTML={{ __html: data.quotes.html }}></div>
</section>
)}
{!!data.trademark?.total && (
<section>
<h3 className='heading heading__tertiary'>Trademark</h3>
<div dangerouslySetInnerHTML={{ __html: data.trademark.html }}></div>
</section>
)}
{!!data.nicknames.length && (
<section>
<h3 className='heading heading__tertiary'>Nicknames</h3>
<p>{data.nicknames.join(', ')}</p>
</section>
)}
{!!data.salary?.total && (
<section>
<h3 className='heading heading__tertiary'>Salary</h3>
<p>
<span>{data.salary.value} in </span>
<Link href={`/title/${data.salary.title.id}`}>
<a className={'link'}>{data.salary.title.text}</a>
</Link>
<span> ({data.salary.title.year})</span>
</p>
</section>
{isEmpty(data) ? (
<p>Nothing interesting to show.</p>
) : (
<>
{!!data.trivia?.total && (
<section>
<h3 className='heading heading__tertiary'>Trivia</h3>
<div dangerouslySetInnerHTML={{ __html: data.trivia.html }}></div>
</section>
)}
{!!data.quotes?.total && (
<section>
<h3 className='heading heading__tertiary'>Quotes</h3>
<div dangerouslySetInnerHTML={{ __html: data.quotes.html }}></div>
</section>
)}
{!!data.trademark?.total && (
<section>
<h3 className='heading heading__tertiary'>Trademark</h3>
<div dangerouslySetInnerHTML={{ __html: data.trademark.html }}></div>
</section>
)}
{!!data.nicknames.length && (
<section>
<h3 className='heading heading__tertiary'>Nicknames</h3>
<p>{data.nicknames.join(', ')}</p>
</section>
)}
{!!data.salary?.total && (
<section>
<h3 className='heading heading__tertiary'>Salary</h3>
<p>
<span>{data.salary.value} in </span>
<Link href={`/title/${data.salary.title.id}`}>
<a className={'link'}>{data.salary.title.text}</a>
</Link>
<span> ({data.salary.title.year})</span>
</p>
</section>
)}
</>
)}
</section>
);
export default DidYouKnow;
const isEmpty = (data: Props['data']) =>
Boolean(
!data.nicknames.length && !data.quotes && !data.salary && !data.trademark && !data.trivia
);

View File

@@ -86,6 +86,21 @@ const Basic = ({ data, className }: Props) => {
))}
</p>
)}
{!!data.interests.length && (
<p className={styles.genres}>
<span className={styles.genres__heading}>Interests: </span>
{data.interests.map((interest, i) => (
<Fragment key={interest.id}>
{i > 0 && ', '}
<Link href={`/interest/${interest.id}`}>
<a className={styles.link}>{interest.text}</a>
</Link>
</Fragment>
))}
</p>
)}
<p className={styles.overview}>
<span className={styles.overview__heading}>Plot: </span>
<span className={styles.overview__text}>{data.plot || '-'}</span>

View File

@@ -7,7 +7,13 @@ type Props = {
};
const DidYouKnow = ({ data }: Props) => {
if (!Object.keys(data).length) return <></>;
if (!Object.keys(data).length)
return (
<section className={styles.didYouKnow}>
<h2 className='heading heading__secondary'>Did you know</h2>
<p>Nothing interesting to show.</p>
</section>
);
return (
<section className={styles.didYouKnow}>
<h2 className='heading heading__secondary'>Did you know</h2>

View File

@@ -1,82 +1,127 @@
import { useRouter } from 'next/router';
import Link from 'next/link';
import { Reviews } from 'src/interfaces/shared/title';
import type { Reviews as TReviews } from 'src/interfaces/shared/title';
import { formatNumber } from 'src/utils/helpers';
import styles from 'src/styles/modules/components/title/reviews.module.scss';
type Props = {
reviews: Reviews;
reviews: TReviews;
};
const Reviews = ({ reviews }: Props) => {
return (
<section className={styles.reviews}>
<h2 className='heading heading__secondary'>Reviews</h2>
<RatingsDistribution ratings={reviews.ratingsDistribution} />
<section className={styles.userReviews}>
<h3 className='heading heading__tertiary'>User Reviews</h3>
{reviews.featuredReviews ? (
<ul className={styles.userReviews__list} role='list'>
{reviews.featuredReviews.map(featuredReview => (
<li key={featuredReview.id}>
<details className={styles.review}>
<summary className={styles.review__summary}>
<strong>{featuredReview.review.summary}</strong>
</summary>
<div
className={styles.review__text}
dangerouslySetInnerHTML={{
__html: featuredReview.review.html,
}}
></div>
<footer className={styles.review__metadata}>
<p>
{featuredReview.rating && <span>Rated {featuredReview.rating}/10</span>}
<span>
{' '}
by{' '}
<Link href={`/user/${featuredReview.reviewer.id}`}>
<a className='link'>{featuredReview.reviewer.name}</a>
</Link>
</span>
</p>
</footer>
</details>
</li>
))}
</ul>
) : (
<p>No reviews yet.</p>
)}
</section>
{reviews.ai?.summary && (
<details className={styles.reviewAi}>
<summary className='heading heading__tertiary'>AI Summary</summary>
<p dangerouslySetInnerHTML={{ __html: reviews.ai.summary }} />
<ul>
{reviews.ai.themes.map(theme => (
<li key={theme.id}>{theme.text}</li>
))}
</ul>
</details>
)}
<ReviewStats reviews={reviews} />
</section>
);
};
export default Reviews;
const RatingsDistribution = ({ ratings }: { ratings: Props['reviews']['ratingsDistribution'] }) => {
const maxRating = Math.max(...ratings.map(r => r.votes));
return (
<div className={styles.ratingsDistribution}>
<h3 className='heading heading__tertiary'>Ratings Distribution</h3>
{ratings.length ? (
<ul>
{ratings.map(rating => (
<li
key={rating.rating}
style={
{
'--bar-height': `${((rating.votes / maxRating) * 100).toFixed(2)}%`,
} as React.CSSProperties
}
>
<span>
{rating.rating} <span>({formatNumber(rating.votes)})</span>
</span>
</li>
))}
</ul>
) : (
<p>No ratings yet.</p>
)}
</div>
);
};
const ReviewStats = ({ reviews }: { reviews: Props['reviews'] }) => {
const router = useRouter();
const { titleId } = router.query;
return (
<section className={styles.reviews}>
<h2 className="heading heading__secondary">Reviews</h2>
{reviews.featuredReview && (
<article className={styles.reviews__reviewContainer}>
<details className={styles.review}>
<summary className={styles.review__summary}>
<strong>{reviews.featuredReview.review.summary}</strong>
</summary>
<div
className={styles.review__text}
dangerouslySetInnerHTML={{
__html: reviews.featuredReview.review.html,
}}
></div>
</details>
<footer className={styles.review__metadata}>
<p>
{reviews.featuredReview.rating && (
<span>Rated {reviews.featuredReview.rating}/10</span>
)}
<span>
{' '}
by{' '}
<Link href={`/user/${reviews.featuredReview.reviewer.id}`}>
<a className="link">{reviews.featuredReview.reviewer.name}</a>
</Link>
</span>
<span> on {reviews.featuredReview.date}.</span>
</p>
<p>
<span>
{formatNumber(reviews.featuredReview.votes.up)} upvotes
</span>
<span>
, {formatNumber(reviews.featuredReview.votes.down)} downvotes
</span>
</p>
</footer>
</article>
)}
<div className={styles.reviews__stats}>
<p>
<Link href={`/title/${titleId}/reviews`}>
<a className="link">
{formatNumber(reviews.numUserReviews)} User reviews
</a>
</Link>
</p>
<p>
<Link href={`/title/${titleId}/externalreviews`}>
<a className="link">
{formatNumber(reviews.numCriticReviews)} Critic reviews
</a>
</Link>
</p>
<p>
<Link href={`/title/${titleId}/criticreviews`}>
<a className="link"> {reviews.metacriticScore} Metascore</a>
</Link>
</p>
</div>
</section>
<div className={styles.reviewStats}>
<p>
<Link href={`/title/${titleId}/reviews`}>
<a className='link'>{formatNumber(reviews.numUserReviews)} User reviews</a>
</Link>
</p>
<p>
<Link href={`/title/${titleId}/externalreviews`}>
<a className='link'>{formatNumber(reviews.numCriticReviews)} Critic reviews</a>
</Link>
</p>
<p>
<Link href={`/title/${titleId}/criticreviews`}>
<a className='link'> {reviews.metacriticScore} Metascore</a>
</Link>
</p>
</div>
);
};
export default Reviews;

View File

@@ -143,7 +143,7 @@ export default interface Name {
value: string;
language: string;
};
mimeType?: string;
videoMimeType?: string;
url: string;
}>;
recommendedTimedTextTrack?: {

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,3 @@
import { AppError as AppErrorClass } from 'src/utils/helpers';
export type AppError = Omit<InstanceType<typeof AppErrorClass>, 'name'>;
export type AppError = Pick<InstanceType<typeof AppErrorClass>, 'message' | 'statusCode' | 'stack'>;

View File

@@ -67,13 +67,13 @@ export const getServerSideProps: GetServerSideProps<Data, FindQueryParams> = asy
props: { data: { title: query, results: res }, error: null, originalPath },
};
} catch (error) {
const { message, statusCode } = getErrorProperties(error);
ctx.res.statusCode = statusCode;
ctx.res.statusMessage = message;
const err = getErrorProperties(error);
ctx.res.statusCode = err.statusCode;
ctx.res.statusMessage = err.message;
return {
props: {
error: { message, statusCode },
error: { message: err.message, statusCode: err.statusCode, stack: err.format() },
data: { title: query, results: null },
originalPath,
},

View File

@@ -55,11 +55,17 @@ export const getServerSideProps: GetServerSideProps<Data, Params> = async ctx =>
return { props: { data, error: null, originalPath } };
} catch (error) {
const { message, statusCode } = getErrorProperties(error);
ctx.res.statusCode = statusCode;
ctx.res.statusMessage = message;
const err = getErrorProperties(error);
ctx.res.statusCode = err.statusCode;
ctx.res.statusMessage = err.message;
return { props: { error: { message, statusCode }, data: null, originalPath } };
return {
props: {
error: { message: err.message, statusCode: err.statusCode, stack: err.format() },
data: null,
originalPath,
},
};
}
};

View File

@@ -5,7 +5,7 @@ import ErrorInfo from 'src/components/error/ErrorInfo';
import Media from 'src/components/media/Media';
import { Basic, Cast, DidYouKnow, Info, MoreLikeThis, Reviews } from 'src/components/title';
import Title from 'src/interfaces/shared/title';
import { AppError } from 'src/interfaces/shared/error';
import type { AppError } from 'src/interfaces/shared/error';
import getOrSetApiCache from 'src/utils/getOrSetApiCache';
import title from 'src/utils/fetchers/title';
import { getErrorProperties, getProxiedIMDbImgUrl } from 'src/utils/helpers';
@@ -63,12 +63,18 @@ export const getServerSideProps: GetServerSideProps<Data, Params> = async ctx =>
const data = await getOrSetApiCache(titleKey(titleId), title, titleId);
return { props: { data, error: null, originalPath } };
} catch (error) {
const { message, statusCode } = getErrorProperties(error);
ctx.res.statusCode = statusCode;
ctx.res.statusMessage = message;
} catch (e) {
const err = getErrorProperties(e);
ctx.res.statusCode = err.statusCode;
ctx.res.statusMessage = err.message;
return { props: { error: { message, statusCode }, data: null, originalPath } };
const error = {
message: err.message,
statusCode: err.statusCode,
stack: err.format(),
};
console.error(err);
return { props: { error, data: null, originalPath } };
}
};

View File

@@ -8,7 +8,7 @@
display: grid;
justify-content: center;
justify-items: center;
gap: var(--spacer-1);
gap: var(--comp-whitespace);
@include helper.bp('bp-700') {
--doc-whitespace: var(--spacer-5);
@@ -31,6 +31,19 @@
text-align: center;
}
.stack {
max-width: 90%;
max-height: 20rem;
padding: var(--spacer-3);
white-space: pre-wrap;
overflow: scroll;
user-select: all;
border-radius: var(--spacer-1);
background-color: var(--clr-bg-muted);
}
.button {
align-self: end;

View File

@@ -1,26 +1,94 @@
.reviews {
display: grid;
gap: var(--comp-whitespace);
}
&__reviewContainer {
// background-color: antiquewhite;
.ratingsDistribution {
overflow: hidden;
ul {
list-style: none;
display: flex;
gap: var(--spacer-2);
overflow-x: auto;
padding-block: var(--spacer-1);
}
&__stats {
li {
text-align: center;
display: flex;
flex-direction: column;
height: 5em;
align-items: center;
flex-shrink: 0;
&::before {
display: block;
content: '';
height: var(--bar-height);
margin-top: auto;
background-color: var(--clr-fill);
width: var(--spacer-6);
border-radius: var(--spacer-0);
}
span {
font-weight: var(--fw-bold);
> span {
font-weight: initial;
font-size: 0.9em;
color: var(--clr-text-muted);
}
}
}
}
.reviewAi {
summary {
cursor: pointer;
}
p {
padding-block: var(--spacer-2);
}
ul {
list-style: none;
display: flex;
gap: var(--spacer-1);
flex-wrap: wrap;
gap: var(--spacer-2);
li {
padding: var(--spacer-1);
background-color: var(--clr-bg-muted);
border-radius: var(--spacer-0);
}
}
}
.reviewStats {
display: flex;
flex-wrap: wrap;
gap: var(--spacer-2);
}
.userReviews {
&__list {
padding-block-start: var(--spacer-1);
display: grid;
gap: var(--spacer-1);
list-style: none;
}
}
.review {
&__summary {
font-size: calc(var(--fs-5) * 1.1);
cursor: pointer;
}
&__text,
&__metadata {
padding-top: var(--spacer-2);
padding-top: var(--spacer-1);
}
}

View File

@@ -42,18 +42,18 @@ const cleanName = (rawData: RawName) => {
},
media: {
...(main.primaryVideos.edges.length && {
trailer: {
id: main.primaryVideos.edges[0].node.id,
isMature: main.primaryVideos.edges[0].node.isMature,
thumbnail: main.primaryVideos.edges[0].node.thumbnail.url,
runtime: main.primaryVideos.edges[0].node.runtime.value,
caption: main.primaryVideos.edges[0].node.description?.value ?? null,
urls: main.primaryVideos.edges[0].node.playbackURLs.map(url => ({
resolution: url.displayName.value,
mimeType: url.mimeType ?? null,
trailers: main.primaryVideos.edges.map(trailer => ({
id: trailer.node.id,
isMature: trailer.node.isMature,
thumbnail: trailer.node.thumbnail.url,
runtime: trailer.node.runtime.value,
caption: trailer.node.description?.value ?? null,
urls: trailer.node.playbackURLs.map(url => ({
resolution: url.displayName.value as 'SD' | '480p',
mimeType: url.videoMimeType ?? null,
url: url.url,
})),
},
})),
}),
images: {
total: misc.images.total,

View File

@@ -12,6 +12,7 @@ const cleanTitle = (rawData: RawTitle) => {
titleId: main.id,
basic: {
id: main.id,
isAdult: main.isAdult,
title: main.titleText.text,
// ...(main.originalTitleText.text.toLowerCase() !==
// main.titleText.text.toLowerCase() && {
@@ -50,6 +51,10 @@ const cleanTitle = (rawData: RawTitle) => {
id: genre.id,
text: genre.text,
})),
interests: main.interests.edges.map(interest => ({
id: interest.node.id,
text: interest.node.primaryText.text,
})),
plot: main.plot?.plotText?.plainText || null,
primaryCrew: main.principalCredits.map(type => ({
type: { category: type.category.text, id: type.category.id },
@@ -76,18 +81,18 @@ const cleanTitle = (rawData: RawTitle) => {
})),
media: {
...(main.primaryVideos.edges.length && {
trailer: {
id: main.primaryVideos.edges[0].node.id,
isMature: main.primaryVideos.edges[0].node.isMature,
thumbnail: main.primaryVideos.edges[0].node.thumbnail.url,
runtime: main.primaryVideos.edges[0].node.runtime.value,
caption: main.primaryVideos.edges[0].node.description?.value ?? null,
urls: main.primaryVideos.edges[0].node.playbackURLs.map(url => ({
resolution: url.displayName.value,
mimeType: url.mimeType ?? null,
trailers: main.primaryVideos.edges.map(trailer => ({
id: trailer.node.id,
isMature: trailer.node.isMature,
thumbnail: trailer.node.thumbnail.url,
runtime: trailer.node.runtime.value,
caption: trailer.node.description?.value ?? null,
urls: trailer.node.playbackURLs.map(url => ({
resolution: url.displayName.value as 'SD' | '480p',
mimeType: url.videoMimeType ?? null,
url: url.url,
})),
},
})),
}),
images: {
total: misc.titleMainImages.total,
@@ -122,6 +127,9 @@ const cleanTitle = (rawData: RawTitle) => {
}),
topRating: misc.ratingsSummary.topRanking?.rank || null,
},
watchlist: {
text: main.engagementStatistics?.watchlistStatistics.displayableCount.text || null,
},
meta: {
// for tv episode
...(main.series && {
@@ -208,25 +216,35 @@ const cleanTitle = (rawData: RawTitle) => {
metacriticScore: main.metacritic?.metascore.score || null,
numCriticReviews: main.criticReviewsTotal.total,
numUserReviews: misc.reviews.total,
...(misc.featuredReviews.edges.length && {
featuredReview: {
id: misc.featuredReviews.edges[0].node.id,
reviewer: {
id: misc.featuredReviews.edges[0].node.author.userId,
name: misc.featuredReviews.edges[0].node.author.nickName,
},
rating: misc.featuredReviews.edges[0].node.authorRating,
date: formatDate(misc.featuredReviews.edges[0].node.submissionDate),
votes: {
up: misc.featuredReviews.edges[0].node.helpfulness.upVotes,
down: misc.featuredReviews.edges[0].node.helpfulness.downVotes,
},
review: {
summary: misc.featuredReviews.edges[0].node.summary.originalText,
html: misc.featuredReviews.edges[0].node.text.originalText.plaidHtml,
},
ratingsDistribution:
misc.aggregateRatingsBreakdown.histogram?.histogramValues.map(v => ({
rating: v.rating,
votes: v.voteCount,
})) || [],
...(misc.reviewSummary && {
ai: {
summary: misc.reviewSummary.overall.medium.value.plaidHtml,
themes: misc.reviewSummary.themes.map(t => ({
text: t.label.value,
id: t.themeId,
sentiment: t.sentiment as 'POSITIVE' | 'NEGATIVE',
})),
},
}),
...(misc.featuredReviews.edges.length && {
featuredReviews: misc.featuredReviews.edges.map(featuredReview => ({
id: featuredReview.node.id,
reviewer: {
id: featuredReview.node.author.userId,
name: featuredReview.node.author.username.text,
},
rating: featuredReview.node.authorRating,
review: {
summary: featuredReview.node.summary.originalText,
html: featuredReview.node.text.originalText.plaidHtml,
},
})),
}),
},
details: {
...(misc.releaseDate && {
@@ -242,8 +260,8 @@ const cleanTitle = (rawData: RawTitle) => {
},
},
}),
...(misc.countriesOfOrigin && {
countriesOfOrigin: misc.countriesOfOrigin.countries.map(country => ({
...(misc.countriesDetails && {
countriesOfOrigin: misc.countriesDetails.countries.map(country => ({
id: country.id,
text: country.text,
})),
@@ -353,6 +371,10 @@ const cleanTitle = (rawData: RawTitle) => {
},
genres: title.node.titleGenres?.genres.map(genre => genre.genre.text) ?? null,
})),
faqs: {
questions: misc.faqs.edges.map(q => ({ question: q.node.question, id: q.node.id })),
total: misc.faqs.total,
},
};
return cleanData;

View File

@@ -74,12 +74,33 @@ export const getProxiedIMDbImgUrl = (url: string) => {
};
export const AppError = class extends Error {
constructor(message: string, public statusCode: number, errorOptions?: unknown) {
const saneErrorOptions = getErrorOptions(errorOptions);
super(message, saneErrorOptions);
constructor(message: string, public statusCode: number, cause?: unknown) {
const _cause = cause ? AppError.toError(cause) : undefined;
super(message, { cause: _cause });
Error.captureStackTrace(this, AppError);
if (process.env.NODE_ENV === 'development') console.error(this);
}
static toError(err: unknown) {
if (err instanceof Error) return err;
return new Error(`Unexpected: ${JSON.stringify(err)}`);
}
format() {
let str = '';
let cur: Error | null = this;
let depth = 0;
while (cur && depth <= 4) {
if (cur.stack) str += `${cur.stack}\n`;
else str += `${cur.name}: ${cur.message}\n`;
cur = cur.cause instanceof Error ? cur.cause : null;
if (cur) str += 'Caused by:\n';
depth++;
}
return str.trimEnd();
}
};
@@ -110,19 +131,6 @@ export const isLocalStorageAvailable = () => {
}
};
const getErrorOptions = (error: unknown): ErrorOptions | undefined => {
if (!error || typeof error !== 'object') return undefined;
let cause: unknown;
// @ts-expect-error it's not an error! just that project's ts version is old, which can't be upgraded
if ('cause' in error) cause = error.cause;
// @ts-expect-error it's not an error! just that project's ts version is old, which can't be upgraded
else if ('stack' in error) cause = error.stack;
// @ts-expect-error it's not an error! just that project's ts version is old, which can't be upgraded
return { cause };
};
export const getErrorProperties = (
error: unknown,
message = 'Something went very wrong',
@@ -130,4 +138,4 @@ export const getErrorProperties = (
) => {
if (error instanceof AppError) return error;
return new AppError(message, statusCode, error);
};
};

5
ups.json Normal file
View File

@@ -0,0 +1,5 @@
{
"upstream": "https://github.com/zyachel/libremdb",
"provider": "github",
"commit": "dded1e59ca6df40ccb1b3f2c70de14144afbe922"
}