Compare commits
No commits in common. "main" and "v2.0.0" have entirely different histories.
146 changed files with 1623 additions and 6552 deletions
|
@ -1,43 +1,9 @@
|
|||
################################################################################
|
||||
### PLEASE FILL/ENABLE REQUIRED VARS AT LEAST BEFORE RUNNING THE APPLICATION ###
|
||||
################################################################################
|
||||
|
||||
################################################################################
|
||||
### 1. REQUIRED VARS(site may not work as expected without these).
|
||||
################################################################################
|
||||
## used for meta tags. e.g: 'https://libremdb.iket.me'. don't add end slash.
|
||||
# required fields
|
||||
# used for meta tags. e.g: 'https://libremdb.iket.me' don't add end slash.
|
||||
NEXT_PUBLIC_URL=
|
||||
## used when fetching data from IMDb. not adding these could result in not getting any response.
|
||||
## example useragent header: 'Mozilla/5.0 (X11; Linux x86_64; rv:108.0) Gecko/20100101 Firefox/108.0'
|
||||
AXIOS_USERAGENT=
|
||||
## example accept header: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8'
|
||||
AXIOS_ACCEPT=
|
||||
|
||||
################################################################################
|
||||
### 2. OPTIONAL VARS(enabling these is encouraged)
|
||||
################################################################################
|
||||
## for forcing a certain language for data we get from imdb. Useful when you don't want your IP to determine the preferred language.
|
||||
# AXIOS_LANGUAGE='en-US,en;q=0.5'
|
||||
## comment it out if you wish to enable nextjs stats collection. more at https://nextjs.org/telemetry
|
||||
NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
################################################################################
|
||||
### 3. REDIS CONFIG(optional if you don't need redis)
|
||||
################################################################################
|
||||
## enables caching of api routes as well as media
|
||||
# USE_REDIS=true
|
||||
## in case you don't want to cache media but only api routes
|
||||
# USE_REDIS_FOR_API_ONLY=true
|
||||
## ttl for media and api
|
||||
# REDIS_CACHE_TTL_API=3600
|
||||
# REDIS_CACHE_TTL_MEDIA=3600
|
||||
## for docker, just set the domain to the container name, default is 'libremdb_redis'
|
||||
# REDIS_URL=localhost:6379
|
||||
|
||||
################################################################################
|
||||
### 4. INSTANCE META FIELDS(not required but good to have)
|
||||
################################################################################
|
||||
## example: 'https://iket.me'.
|
||||
NEXT_PUBLIC_INSTANCE_MAIN_URL=
|
||||
## eg: 'zyachel'
|
||||
NEXT_PUBLIC_INSTANCE_NAME=
|
||||
# optional fields. uncomment them and add the values if you wish so.
|
||||
# default useragent for requesting data from imdb is 'axios/0.27.2'
|
||||
# AXIOS_USERAGENT=
|
||||
# default accept header is 'application/json, text/plain, */*'
|
||||
# AXIOS_ACCEPT=
|
||||
|
|
29
.github/workflows/release.yml
vendored
Normal file
29
.github/workflows/release.yml
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
name: release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- next
|
||||
|
||||
jobs:
|
||||
changelog-and-release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: generate changelog and bump version
|
||||
id: changelog
|
||||
uses: TriPSs/conventional-changelog-action@v3
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: create release
|
||||
uses: actions/create-release@v1
|
||||
if: ${{ steps.changelog.outputs.skipped == 'false' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ steps.changelog.outputs.tag }}
|
||||
release_name: ${{ steps.changelog.outputs.tag }}
|
||||
body: ${{ steps.changelog.outputs.clean_changelog }}
|
11
.gitignore
vendored
11
.gitignore
vendored
|
@ -28,15 +28,8 @@ yarn-error.log*
|
|||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
next-env.d.ts
|
||||
|
||||
#just dev stuff
|
||||
dev/*
|
||||
|
||||
# other lockfiles
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
|
||||
# docker
|
||||
docker-compose.yml
|
||||
dump.rdb
|
||||
yarn.lock
|
13
.prettierrc
13
.prettierrc
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"arrowParens": "avoid",
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
36
.versionrc
36
.versionrc
|
@ -1,36 +0,0 @@
|
|||
{
|
||||
"types": [
|
||||
{
|
||||
"type": "feat",
|
||||
"section": "Features"
|
||||
},
|
||||
{
|
||||
"type": "fix",
|
||||
"section": "Bug Fixes"
|
||||
},
|
||||
{
|
||||
"type": "chore",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"type": "docs",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"type": "style",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"type": "refactor",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"type": "perf",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"type": "test",
|
||||
"hidden": true
|
||||
}
|
||||
]
|
||||
}
|
121
CHANGELOG.md
121
CHANGELOG.md
|
@ -1,105 +1,54 @@
|
|||
# Changelog
|
||||
# [2.0.0](https://github.com/zyachel/libremdb/compare/v0.1.2...v2.0.0) (2022-10-31)
|
||||
|
||||
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
||||
|
||||
## [3.2.0](https://github.com/zyachel/libremdb/compare/v3.1.1...v3.2.0) (2023-10-28)
|
||||
### Bug Fixes
|
||||
|
||||
* change to poster for og:image ([f207d68](https://github.com/zyachel/libremdb/commit/f207d688e2dc0d6c12a0b6e8f6ddc7b0eadf5e0b))
|
||||
* remove double space in inspiration credit ([3f987b5](https://github.com/zyachel/libremdb/commit/3f987b59dcadbb5f931dda4d510b4c13a4ed5cd0))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **list:** add list route ([97f1432](https://github.com/zyachel/libremdb/commit/97f1432ac5d23206229d806b7cb3e04af6dec36f))
|
||||
* add "og:image" property for social media embeds ([d152cf4](https://github.com/zyachel/libremdb/commit/d152cf4b6210b3dd5eb33274d05695bd5593cd06))
|
||||
* major rewrite ([9891204](https://github.com/zyachel/libremdb/commit/9891204f5a11eb24ad7c924f50f0e069589b82ff))
|
||||
|
||||
## [3.1.1](https://github.com/zyachel/libremdb/compare/v3.1.0...v3.1.1) (2023-10-14)
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* the whole application is rewritten from scratch.
|
||||
|
||||
|
||||
|
||||
## [0.1.2](https://github.com/zyachel/libremdb/compare/v0.1.1...v0.1.2) (2022-06-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **card:** fix long attributes in cards under 'Known For' section ([736d680](https://github.com/zyachel/libremdb/commit/736d6802430a3f4f364915f3df93fc548a51ebf1))
|
||||
* **error:** fix incorrect 'view on IMDb' link on error page ([0aea2f4](https://github.com/zyachel/libremdb/commit/0aea2f47dad6eb78e319ea1abd8c444f2cba4424))
|
||||
* **media proxy:** fix 304 response code with body error ([c610ef4](https://github.com/zyachel/libremdb/commit/c610ef4d1be39c122715a0eb200155537e7d6abf))
|
||||
* **name:** fix name route crash ([38ed0c6](https://github.com/zyachel/libremdb/commit/38ed0c62177532b93f61af4172ffa6e5b9995bdc))
|
||||
* **name:** fix route crash for some ids ([e91c313](https://github.com/zyachel/libremdb/commit/e91c313f127632f1bd44d190af71bc841bbe87b7))
|
||||
* **title:** fix a crash in title route ([21a1c83](https://github.com/zyachel/libremdb/commit/21a1c83d95b703fa08cdb96c206626f22d5366c9))
|
||||
* change the order in which env vars are loaded ([55c0eba](https://github.com/zyachel/libremdb/commit/55c0eba6e47c85654242173796e76205328f5f31))
|
||||
* **robots.txt:** disallow all robots ([f39998d](https://github.com/zyachel/libremdb/commit/f39998d57bd2531fd1bd8b21e32ca563baf7565c))
|
||||
|
||||
## [3.1.0](https://github.com/zyachel/libremdb/compare/v3.0.0...v3.1.0) (2023-05-21)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* implement caching of static files ([170fbab](https://github.com/zyachel/libremdb/commit/170fbabe5ef4b8cec63ca8831a4ae2a79798a6b0))
|
||||
|
||||
|
||||
|
||||
## [0.1.1](https://github.com/zyachel/libremdb/compare/v0.1.0...v0.1.1) (2022-06-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* typo in URL ([#2](https://github.com/zyachel/libremdb/issues/2)) ([9f35a66](https://github.com/zyachel/libremdb/commit/9f35a668b508d79353da5db70014d99094788d5a))
|
||||
|
||||
|
||||
|
||||
# [0.1.0](https://github.com/zyachel/libremdb/compare/30dac07ba33dbe4331a5c9fa6cd2c332100868df...v0.1.0) (2022-05-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **cache:** implement caching of routes ([c53c88d](https://github.com/zyachel/libremdb/commit/c53c88db9bf98258547e2ca512f864800821cb1f))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **form:** fix hydration error ([8599ae2](https://github.com/zyachel/libremdb/commit/8599ae2c5ac11f2818f56c9f7de7666a38b4386c))
|
||||
* **name:** fix a couple of crashes in name and title route ([8d9b663](https://github.com/zyachel/libremdb/commit/8d9b6630a576b7e8331eb5431cd90d02733b4917))
|
||||
|
||||
## [3.0.0](https://github.com/zyachel/libremdb/compare/v2.4.0...v3.0.0) (2023-04-15)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **title:** older versions won't work, at least for title route
|
||||
|
||||
### Features
|
||||
|
||||
* add info related to the current instance ([2c5d2f8](https://github.com/zyachel/libremdb/commit/2c5d2f86e46a52223f07d573b152bad5174ee2d9))
|
||||
* **route:** add name route ([75732e0](https://github.com/zyachel/libremdb/commit/75732e00869f9777e87e767a48648996345f02f7))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **title:** fix title page crash ([8ce02d0](https://github.com/zyachel/libremdb/commit/8ce02d02364c8e1f03a8b16594bc20ee6766a8c6))
|
||||
|
||||
# [2.4.0](https://github.com/zyachel/libremdb/compare/v2.3.1...v2.4.0) (2023-01-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix app crash ([71d1d5b](https://github.com/zyachel/libremdb/commit/71d1d5b34e2866729ae0c96c59ea51e8d1a3dcca))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add error boundary ([5cc2ef0](https://github.com/zyachel/libremdb/commit/5cc2ef02cec0b31c5d449e189a054fbef5801f60))
|
||||
* add review section ([30dac07](https://github.com/zyachel/libremdb/commit/30dac07ba33dbe4331a5c9fa6cd2c332100868df))
|
||||
|
||||
|
||||
|
||||
## [2.3.1](https://github.com/zyachel/libremdb/compare/v2.3.0...v2.3.1) (2023-01-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix unseekable videos on webkit-based browsers ([a32785c](https://github.com/zyachel/libremdb/commit/a32785ce00b638e9079f0924fd9b00f98c077348))
|
||||
|
||||
|
||||
|
||||
# [2.3.0](https://github.com/zyachel/libremdb/compare/v2.2.2...v2.3.0) (2022-12-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* couple css improvements for webkit-based browsers ([81eaf2f](https://github.com/zyachel/libremdb/commit/81eaf2fd5e5980c0c4d59a8805cf541fa8fe51f9))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **search:** add basic search functionality ([0cff34a](https://github.com/zyachel/libremdb/commit/0cff34a766b09ba17be2a89f6290889dbf225436))
|
||||
|
||||
|
||||
|
||||
## [2.2.2](https://github.com/zyachel/libremdb/compare/v2.2.1...v2.2.2) (2022-12-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* app crash on qutebrowser ([78b14ec](https://github.com/zyachel/libremdb/commit/78b14ec07955d29403b8b5ae0d449f38eea2bbc5))
|
||||
|
||||
|
||||
|
||||
## [2.2.1](https://github.com/zyachel/libremdb/compare/v2.2.0...v2.2.1) (2022-12-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **title:** fix site crash ([dd75df0](https://github.com/zyachel/libremdb/commit/dd75df01eb7c03d8945a8bd20ed231a66bd88b8f))
|
||||
|
|
35
Dockerfile
35
Dockerfile
|
@ -1,35 +0,0 @@
|
|||
# Thanks @yordis on Github! https://github.com/vercel/next.js/discussions/16995#discussioncomment-132339
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM node:lts-alpine AS deps
|
||||
|
||||
WORKDIR /opt/app
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN npm install -g pnpm
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
# This is where because may be the case that you would try
|
||||
# to build the app based on some `X_TAG` in my case (Git commit hash)
|
||||
# but the code hasn't changed.
|
||||
FROM node:lts-alpine AS builder
|
||||
|
||||
ENV NODE_ENV=production
|
||||
WORKDIR /opt/app
|
||||
RUN npm install -g pnpm
|
||||
COPY . .
|
||||
COPY --from=deps /opt/app/node_modules ./node_modules
|
||||
RUN pnpm build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM gcr.io/distroless/nodejs18-debian11 AS runner
|
||||
ARG X_TAG
|
||||
WORKDIR /opt/app
|
||||
ENV NODE_ENV=production
|
||||
COPY --from=builder /opt/app/next.config.mjs ./
|
||||
COPY --from=builder /opt/app/public ./public
|
||||
COPY --from=builder /opt/app/.next ./.next
|
||||
COPY --from=builder /opt/app/node_modules ./node_modules
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=3000
|
||||
CMD ["./node_modules/next/dist/bin/next", "start"]
|
67
README.md
67
README.md
|
@ -6,7 +6,7 @@ Inspired by projects like [teddit](https://codeberg.org/teddit/teddit), [nitter]
|
|||
|
||||
| | |
|
||||
| -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| <img src="./public/img/misc/preview.jpg" title="screenshot (desktop screen, light mode)" width="1500" /> | <img src="./public/img/misc/preview2.jpg" title="screenshot (mobile screen, dark mode)" width="400" /> |
|
||||
| <img src="./public/img/misc/preview.png" title="screenshot (desktop screen, light mode)" width="1500" /> | <img src="./public/img/misc/preview2.png" title="screenshot (mobile screen, dark mode)" width="385" /> |
|
||||
|
||||
---
|
||||
|
||||
|
@ -32,28 +32,23 @@ Inspired by projects like [teddit](https://codeberg.org/teddit/teddit), [nitter]
|
|||
|
||||
## Instances
|
||||
|
||||
### Clearnet
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
| Instance URL | Region | Notes |
|
||||
| ------------ | ------ | ----- |
|
||||
| 1. Clearnet | | |
|
||||
| [libremdb.iket.me](https://libremdb.iket.me) | Canada | Operated by me |
|
||||
| [libremdb.pussthecat.org](https://libremdb.pussthecat.org) | Germany | Operated by [PussTheCat.org](https://pussthecat.org/) |
|
||||
| [ld.vern.cc](https://ld.vern.cc) | US | Operated by [~vern](https://vern.cc) |
|
||||
| [binge.whatever.social](https://binge.whatever.social) | US & Germany | Operated by [Whatever Social](https://whatever.social) |
|
||||
| [libremdb.lunar.icu](https://libremdb.lunar.icu) | Germany (Cloudflare) | Operated by [lunar.icu](https://lunar.icu/) |
|
||||
| [libremdb.jeikobu.net](https://libremdb.jeikobu.net) | Germany (Cloudflare) | Operated by [shindouj](https://github.com/shindouj) |
|
||||
| [lmdb.hostux.net](https://lmdb.hostux.net) | France | Operated by [Hostux.net](https://hostux.net) |
|
||||
| [binge.whateveritworks.org](https://binge.whateveritworks.org) | Germany (Cloudflare) | Operated by [WhateverItWorks](https://github.com/WhateverItWorks) |
|
||||
| [libremdb.nerdyfam.tech](https://libremdb.nerdyfam.tech) | US | Operated by [Nerdyfam.tech](https://nerdyfam.tech/) |
|
||||
| [libremdb.tux.pizza](https://libremdb.tux.pizza) | US | Operated by [tux.pizza](https://tux.pizza) |
|
||||
| [libremdb.frontendfriendly.xyz](https://libremdb.frontendfriendly.xyz) | — | Operated by [frontendfriendly.xyz](https://frontendfriendly.xyz) |
|
||||
[d.opnxng.com](https://d.opnxng.com) | Singapore | Operated by [Opnxng](https://about.opnxng.com/)
|
||||
[libremdb.catsarch.com](https://libremdb.catsarch.com) | US | Operated by [Butter Cat](https://catsarch.com/)
|
||||
[mdb.sudovanilla.com](https://mdb.sudovanilla.com) | US (Cloudflare) | Operated by [SudoVanilla](https://sudovanilla.com/)
|
||||
| [libremdbeu.herokuapp.com](https://libremdbeu.herokuapp.com) | Europe | Operated by [toyboatcash](https://github.com/toyboatcash) |
|
||||
| [lmdb.tokhmi.xyz](https://lmdb.tokhmi.xyz) | U.S. | Operated by [Tokhmi](https://tokhmi.xyz) |
|
||||
| [libremdb.esmailelbob.xyz](https://libremdb.esmailelbob.xyz) | Canada | Operated by [Esmail EL BoB](https://esmailelbob.xyz) |
|
||||
| [ld.vern.cc](https://ld.vern.cc) | Canada | Operated by [~vern](https://vern.cc) |
|
||||
| 2. Onion | | |
|
||||
| [ld.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion](http://ld.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion) | US | Operated by [~vern](https://vern.cc) |
|
||||
| [libremdb.esmail5pdn24shtvieloeedh7ehz3nrwcdivnfhfcedl7gf4kwddhkqd.onion](http://libremdb.esmail5pdn24shtvieloeedh7ehz3nrwcdivnfhfcedl7gf4kwddhkqd.onion) | Canada | Operated by [Esmail EL BoB](https://esmailelbob.xyz) |
|
||||
| [ld.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion](http://ld.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion) | Canada | Operated by [~vern](https://vern.cc) |
|
||||
| 3. I2P | | |
|
||||
| [vernz3ubrntql4wrgyrssd6u3qzi36zrhz2agbo6vibzbs5olk2q.b32.i2p](http://vernz3ubrntql4wrgyrssd6u3qzi36zrhz2agbo6vibzbs5olk2q.b32.i2p) | US | Operated by [~vern](https://vern.cc) |
|
||||
| [vernz3ubrntql4wrgyrssd6u3qzi36zrhz2agbo6vibzbs5olk2q.b32.i2p](http://vernz3ubrntql4wrgyrssd6u3qzi36zrhz2agbo6vibzbs5olk2q.b32.i2p) | Canada | Operated by [~vern](https://vern.cc) |
|
||||
|
||||
---
|
||||
|
||||
|
@ -69,12 +64,15 @@ Inspired by projects like [teddit](https://codeberg.org/teddit/teddit), [nitter]
|
|||
- It doesn't have all routes.
|
||||
I'll implement more with time :)
|
||||
|
||||
- Is content served from third-parties, like Amazon?
|
||||
Nope, libremdb proxies all image and video requests through the instance to avoid exposing your IP address, browser information and other personally identifiable metadata ([Contributor](https://github.com/httpjamesm)).
|
||||
- I see connection being made to some Amazon domains.
|
||||
For now, images and videos are directly served from Amazon. If I have enough time in the future, I'll implement a way to serve the images from libremdb instead.
|
||||
|
||||
- Will Amazon track me then?
|
||||
They may log your IP address, useragent, and other such
|
||||
identifiers. I'd recommend using a VPN, or accessing the website through TOR for mitigating this risk.
|
||||
|
||||
- Why not just use IMDb?
|
||||
Refer to the [features section](#some-features) above.
|
||||
|
||||
- Why didn't you use other databases like [TMDB](https://www.themoviedb.org/) or [OMDb](https://www.omdbapi.com/)?
|
||||
IMDb simply has superior dataset compared to all other alternatives. With that being said, I'd encourage you to check out those alternatives too.
|
||||
|
||||
|
@ -89,7 +87,7 @@ Inspired by projects like [teddit](https://codeberg.org/teddit/teddit), [nitter]
|
|||
A key named 'theme' is stored in Local Storage provided by your browser, if you ever override the default theme. To remove it, go to site data settings, and clear the data for this website. To permamently disable libremdb from storing your theme prefrences, either turn off JavaScript or disable access to Local Storage for libremdb.
|
||||
|
||||
- Information collected by other services:
|
||||
None. libremdb proxies images anonymously through the instance for maximum privacy ([Contributor](https://github.com/httpjamesm)).
|
||||
libremdb connects to 'media-amazon.com' and 'media-imdb.com' for fetching images and videos. So, Amazon might log your IP address, and other information(such as http headers) sent by your browser.
|
||||
|
||||
---
|
||||
|
||||
|
@ -110,13 +108,13 @@ Inspired by projects like [teddit](https://codeberg.org/teddit/teddit), [nitter]
|
|||
|
||||
- [ ] lists
|
||||
- [ ] moviemeter
|
||||
- [x] person info(includes directors and actors)
|
||||
- [ ] person info(includes directors and actors)
|
||||
- [ ] company info
|
||||
- [ ] user info
|
||||
|
||||
- [X] use redis, or any other caching strategy
|
||||
- [ ] use redis, or any other caching strategy
|
||||
- [x] implement a better installation method
|
||||
- [x] serve images and videos from libremdb itself
|
||||
- [ ] serve images and videos from libremdb itself
|
||||
|
||||
---
|
||||
|
||||
|
@ -130,36 +128,27 @@ As libremdb is made with Next.js, you can deploy it anywhere where Next.js is su
|
|||
for Node.js, visit [their website](https://nodejs.org/en/).
|
||||
for Git, run `sudo apt install git` if you're on a Debian-based distro. Else visit [their website](https://git-scm.com/).
|
||||
|
||||
2. Install redis(optional).
|
||||
You can install redis from [here](https://redis.io).
|
||||
|
||||
3. Clone and set up the repo.
|
||||
2. Clone and set up the repo.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/zyachel/libremdb.git # replace github.com with codeberg.org if you wish so.
|
||||
cd libremdb
|
||||
# change the configuration file to your liking.
|
||||
# optional configuration
|
||||
cp .env.local.example .env.local
|
||||
# replace 'pnpm' with yarn or npm if you use those.
|
||||
# replace 'pnpm' with yarn or npm if you use those
|
||||
pnpm install
|
||||
pnpm build
|
||||
pnpm start
|
||||
# optional: if you're using redis
|
||||
redis-server
|
||||
```
|
||||
|
||||
libremdb will start running at http://localhost:3000.
|
||||
To change port, modify the last command like this: `pnpm start -- -p <port-number>`.
|
||||
|
||||
### Docker (Local)
|
||||
### Docker
|
||||
|
||||
You can build the docker image using the provided Dockerfile(thanks to [@httpjamesm](https://github.com/httpjamesm)) and set it up using the [example docker-compose file](./docker-compose.example.yml).
|
||||
There's a [docker image](https://github.com/PussTheCat-org/docker-libremdb-quay) made by [@TheFrenchGhosty](https://github.com/TheFrenchGhosty) for [PussTheCat.org's instance](https://libremdb.pussthecat.org). You can use that in case you wish to use docker.
|
||||
|
||||
Change the docker-compose file to your liking and run `docker-compose up -d` to start the container, that's all!
|
||||
|
||||
### Docker (Built)
|
||||
|
||||
There's a [docker image](https://github.com/PussTheCat-org/docker-libremdb-quay) made by [@TheFrenchGhosty](https://github.com/TheFrenchGhosty) for [PussTheCat.org's instance](https://libremdb.pussthecat.org). You can use that as well.
|
||||
---
|
||||
|
||||
## Miscellaneous
|
||||
|
||||
|
@ -171,15 +160,13 @@ There's a [docker image](https://github.com/PussTheCat-org/docker-libremdb-quay)
|
|||
```
|
||||
Description: redirect IMDb to libremdb
|
||||
Example URL: https://www.imdb.com/title/tt0258463/?ref_=tt_sims_tt_t_4
|
||||
Include pattern: https?:\/\/(www\.)?imdb\.com\/(.*)
|
||||
Include pattern: https?:\/\/(www\.)?imdb\.com\/([^\?]*)
|
||||
Redirect to: https://libremdb.iket.me/$2
|
||||
Pattern type: Regular Expression
|
||||
```
|
||||
|
||||
- [LibRedirect](https://github.com/libredirect/libredirect/)
|
||||
|
||||
- [Privacy Redirector](https://github.com/dybdeskarphet/privacy-redirector)
|
||||
|
||||
### Similar projects
|
||||
|
||||
- [Teddit](https://codeberg.org/teddit/teddit)
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
# docker-compose.yml
|
||||
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
libremdb:
|
||||
container_name: libremdb
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3000:3000"
|
||||
env_file: .env.local.example
|
||||
depends_on:
|
||||
- libremdb-redis
|
||||
restart: always
|
||||
user: 65534:65534 # equivalent to the nobody user
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /opt/app/.next/cache/:size=10M,mode=0770,uid=65534,gid=65534,noexec,nosuid,nodev
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
networks:
|
||||
- libremdb
|
||||
|
||||
libremdb-redis:
|
||||
container_name: libremdb_redis
|
||||
image: redis
|
||||
# FOR DEBUGGING ONLY
|
||||
# ports:
|
||||
# - "6379:6379"
|
||||
restart: always
|
||||
user: nobody
|
||||
read_only: true
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
tmpfs:
|
||||
- /data:size=10M,mode=0770,uid=65534,gid=65534,noexec,nosuid,nodev
|
||||
cap_drop:
|
||||
- ALL
|
||||
networks:
|
||||
- libremdb
|
||||
|
||||
networks:
|
||||
libremdb:
|
5
next-env.d.ts
vendored
Normal file
5
next-env.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
|
@ -6,7 +6,7 @@ const nextConfig = {
|
|||
return [
|
||||
{
|
||||
source: '/',
|
||||
destination: '/find',
|
||||
destination: '/about',
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
|
@ -20,7 +20,6 @@ const nextConfig = {
|
|||
},
|
||||
isrMemoryCacheSize: 20 * 1024 * 1024,
|
||||
},
|
||||
poweredByHeader: false,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
15
package.json
15
package.json
|
@ -1,15 +1,11 @@
|
|||
{
|
||||
"name": "libremdb",
|
||||
"version": "3.2.0",
|
||||
"version": "2.0.0",
|
||||
"description": "a free & open source IMDb front-end",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"author": "libremdb-contributors",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/zyachel/libremdb/"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
|
@ -19,11 +15,10 @@
|
|||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"ioredis": "^5.3.2",
|
||||
"next": "12.2.5",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"sharp": "^0.31.3"
|
||||
"sharp": "^0.31.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.7.3",
|
||||
|
@ -31,11 +26,11 @@
|
|||
"@types/react-dom": "18.0.6",
|
||||
"eslint": "8.22.0",
|
||||
"eslint-config-next": "12.2.5",
|
||||
"sass": "^1.62.1",
|
||||
"sass": "^1.54.4",
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.5.0",
|
||||
"pnpm": ">=8.0.0"
|
||||
"pnpm": ">=7.0.0"
|
||||
}
|
||||
}
|
||||
}
|
1433
pnpm-lock.yaml
generated
1433
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Binary file not shown.
Before Width: | Height: | Size: 96 KiB |
BIN
public/img/misc/preview.png
Normal file
BIN
public/img/misc/preview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 805 KiB |
Binary file not shown.
Before Width: | Height: | Size: 126 KiB |
BIN
public/img/misc/preview2.png
Normal file
BIN
public/img/misc/preview2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 MiB |
|
@ -15,19 +15,42 @@
|
|||
</symbol>
|
||||
|
||||
<!--miscellaneous logos-->
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-cancel">
|
||||
<path d="M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256s256-114.6 256-256S397.4 0 256 0zM64 256c0-41.4 13.3-79.68 35.68-111.1l267.4 267.4C335.7 434.7 297.4 448 256 448C150.1 448 64 361.9 64 256zM412.3 367.1L144.9 99.68C176.3 77.3 214.6 64 256 64c105.9 0 192 86.13 192 192C448 297.4 434.7 335.7 412.3 367.1z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" id="icon-code-document">
|
||||
<path d="M162.1 257.8c-7.812-7.812-20.47-7.812-28.28 0l-48 48c-7.812 7.812-7.812 20.5 0 28.31l48 48C137.8 386.1 142.9 388 148 388s10.23-1.938 14.14-5.844c7.812-7.812 7.812-20.5 0-28.31L128.3 320l33.86-33.84C169.1 278.3 169.1 265.7 162.1 257.8zM365.3 93.38l-74.63-74.64C278.6 6.742 262.3 0 245.4 0H64C28.65 0 0 28.65 0 64l.0065 384c0 35.34 28.65 64 64 64H320c35.2 0 64-28.8 64-64V138.6C384 121.7 377.3 105.4 365.3 93.38zM336 448c0 8.836-7.164 16-16 16H64.02c-8.838 0-16-7.164-16-16L48 64.13c0-8.836 7.164-16 16-16h160L224 128c0 17.67 14.33 32 32 32h79.1V448zM221.9 257.8c-7.812 7.812-7.812 20.5 0 28.31L255.7 320l-33.86 33.84c-7.812 7.812-7.812 20.5 0 28.31C225.8 386.1 230.9 388 236 388s10.23-1.938 14.14-5.844l48-48c7.812-7.812 7.812-20.5 0-28.31l-48-48C242.3 250 229.7 250 221.9 257.8z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" id="icon-computer-home">
|
||||
<path d="M218.3 8.486C230.6-2.829 249.4-2.829 261.7 8.486L469.7 200.5C476.4 206.7 480 215.2 480 224H336C316.9 224 299.7 232.4 288 245.7V208C288 199.2 280.8 192 272 192H208C199.2 192 192 199.2 192 208V272C192 280.8 199.2 288 208 288H271.1V416H112C85.49 416 64 394.5 64 368V256H32C18.83 256 6.996 247.9 2.198 235.7C-2.6 223.4 .6145 209.4 10.3 200.5L218.3 8.486zM336 256H560C577.7 256 592 270.3 592 288V448H624C632.8 448 640 455.2 640 464C640 490.5 618.5 512 592 512H303.1C277.5 512 255.1 490.5 255.1 464C255.1 455.2 263.2 448 271.1 448H303.1V288C303.1 270.3 318.3 256 336 256zM352 304V448H544V304H352z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" id="icon-link-slash">
|
||||
<path d="M485.1 354.9l113.5-113.5c55.21-55.21 55.21-144.7 0-199.9C570.1 13.8 534.8 0 498.6 0s-72.36 13.8-99.96 41.41l-43.36 43.36c15.11 8.012 29.47 17.58 41.91 30.02c3.146 3.146 5.898 6.518 8.742 9.838l37.96-37.96C458.5 72.05 477.1 64 498.6 64s40.1 8.047 54.71 22.66c14.61 14.61 22.66 34.04 22.66 54.71s-8.049 40.1-22.66 54.71l-119 119l-30.09-23.59c21.49-51.28 12.12-112.4-29.63-154.1C346.1 109.8 310.8 96 274.6 96c-29.6 0-58.93 9.752-83.83 28.23L38.81 5.109C34.41 1.672 29.19 0 24.03 0c-7.125 0-14.19 3.156-18.91 9.187c-8.188 10.44-6.375 25.53 4.062 33.7l591.1 463.1c10.5 8.203 25.56 6.328 33.69-4.078c8.188-10.44 6.375-25.53-4.062-33.7L485.1 354.9zM350.8 249.6L244.3 166.2C253.8 162.2 264 160 274.6 160c20.67 0 40.1 8.049 54.71 22.66c14.62 14.61 22.66 34.04 22.66 54.71C352 241.5 351.4 245.6 350.8 249.6zM234 387.4l-37.96 37.96C181.5 439.1 162 448 141.4 448c-20.67 0-40.1-8.047-54.71-22.66c-14.61-14.61-22.66-34.04-22.66-54.71s8.049-40.1 22.66-54.71l84.83-84.83L120.7 191.3L41.41 270.7c-55.21 55.21-55.21 144.7 0 199.9C69.01 498.2 105.2 512 141.4 512c36.18 0 72.36-13.8 99.96-41.41l43.36-43.36c-15.11-8.012-29.47-17.58-41.91-30.02C239.6 394.1 236.9 390.7 234 387.4zM265.4 374.6C293 402.2 329.2 416 365.4 416c11.98 0 23.84-2.082 35.51-5.111L224.6 272.7C223.9 309.5 237.3 346.5 265.4 374.6z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" id="icon-eye-slash">
|
||||
<path d="M320 400c-75.85 0-137.25-58.71-142.9-133.11L72.2 185.82c-13.79 17.3-26.48 35.59-36.72 55.59a32.35 32.35 0 0 0 0 29.19C89.71 376.41 197.07 448 320 448c26.91 0 52.87-4 77.89-10.46L346 397.39a144.13 144.13 0 0 1-26 2.61zm313.82 58.1l-110.55-85.44a331.25 331.25 0 0 0 81.25-102.07 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64a308.15 308.15 0 0 0-147.32 37.7L45.46 3.37A16 16 0 0 0 23 6.18L3.37 31.45A16 16 0 0 0 6.18 53.9l588.36 454.73a16 16 0 0 0 22.46-2.81l19.64-25.27a16 16 0 0 0-2.82-22.45zm-183.72-142l-39.3-30.38A94.75 94.75 0 0 0 416 256a94.76 94.76 0 0 0-121.31-92.21A47.65 47.65 0 0 1 304 192a46.64 46.64 0 0 1-1.54 10l-73.61-56.89A142.31 142.31 0 0 1 320 112a143.92 143.92 0 0 1 144 144c0 21.63-5.29 41.79-13.9 60.11z"></path>
|
||||
</symbol>
|
||||
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-feather">
|
||||
<path d="M483.4 244.2L351.9 287.1h97.74c-9.874 10.62 3.75-3.125-46.24 46.87l-147.6 49.12h98.24c-74.99 73.12-194.6 70.62-246.8 54.1l-66.14 65.99c-9.374 9.374-24.6 9.374-33.98 0s-9.374-24.6 0-33.98l259.5-259.2c6.249-6.25 6.249-16.37 0-22.62c-6.249-6.249-16.37-6.249-22.62 0l-178.4 178.2C58.78 306.1 68.61 216.7 129.1 156.3l85.74-85.68c90.62-90.62 189.8-88.27 252.3-25.78C517.8 95.34 528.9 169.7 483.4 244.2z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-fast-forward">
|
||||
<path d="M52.51 440.6l171.5-142.9V214.3L52.51 71.41C31.88 54.28 0 68.66 0 96.03v319.9C0 443.3 31.88 457.7 52.51 440.6zM308.5 440.6l192-159.1c15.25-12.87 15.25-36.37 0-49.24l-192-159.1c-20.63-17.12-52.51-2.749-52.51 24.62v319.9C256 443.3 287.9 457.7 308.5 440.6z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-graph-rising">
|
||||
<path d="M472 432h-48a24 24 0 01-24-24V104a24 24 0 0124-24h48a24 24 0 0124 24v304a24 24 0 01-24 24zM344 432h-48a24 24 0 01-24-24V184a24 24 0 0124-24h48a24 24 0 0124 24v224a24 24 0 01-24 24zM216 432h-48a24 24 0 01-24-24V248a24 24 0 0124-24h48a24 24 0 0124 24v160a24 24 0 01-24 24zM88 432H40a24 24 0 01-24-24v-96a24 24 0 0124-24h48a24 24 0 0124 24v96a24 24 0 01-24 24z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" id="icon-rating">
|
||||
<path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-rewind">
|
||||
<path d="M459.5 71.41l-171.5 142.9v83.45l171.5 142.9C480.1 457.7 512 443.3 512 415.1V96.03C512 68.66 480.1 54.28 459.5 71.41zM203.5 71.41L11.44 231.4c-15.25 12.87-15.25 36.37 0 49.24l192 159.1c20.63 17.12 52.51 2.749 52.51-24.62v-319.9C255.1 68.66 224.1 54.28 203.5 71.41z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-like-dislike">
|
||||
<path d="M22.5,10H15.75C15.13,10 14.6,10.38 14.37,10.91L12.11,16.2C12.04,16.37 12,16.56 12,16.75V18A1,1 0 0,0 13,19H18.18L17.5,22.18V22.42C17.5,22.73 17.63,23 17.83,23.22L18.62,24L23.56,19.06C23.83,18.79 24,18.41 24,18V11.5A1.5,1.5 0 0,0 22.5,10M12,6A1,1 0 0,0 11,5H5.82L6.5,1.82V1.59C6.5,1.28 6.37,1 6.17,0.79L5.38,0L0.44,4.94C0.17,5.21 0,5.59 0,6V12.5A1.5,1.5 0 0,0 1.5,14H8.25C8.87,14 9.4,13.62 9.63,13.09L11.89,7.8C11.96,7.63 12,7.44 12,7.25V6Z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" id="icon-person-slash">
|
||||
<path d="M95.1 477.3c0 19.14 15.52 34.67 34.66 34.67h378.7c5.625 0 10.73-1.65 15.42-4.029L264.9 304.3C171.3 306.7 95.1 383.1 95.1 477.3zM630.8 469.1l-277.1-217.9c54.69-14.56 95.18-63.95 95.18-123.2C447.1 57.31 390.7 0 319.1 0C250.2 0 193.7 55.93 192.3 125.4l-153.4-120.3C34.41 1.672 29.19 0 24.03 0C16.91 0 9.845 3.156 5.127 9.187c-8.187 10.44-6.375 25.53 4.062 33.7L601.2 506.9c10.5 8.203 25.56 6.328 33.69-4.078C643.1 492.4 641.2 477.3 630.8 469.1z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-image-slash">
|
||||
<path d="M21 17.2L6.8 3H19C20.1 3 21 3.9 21 5V17.2M20.7 22L19.7 21H5C3.9 21 3 20.1 3 19V4.3L2 3.3L3.3 2L22 20.7L20.7 22M16.8 18L12.9 14.1L11 16.5L8.5 13.5L5 18H16.8Z"></path>
|
||||
</symbol>
|
||||
|
@ -37,10 +60,14 @@
|
|||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-responsive">
|
||||
<path d="M4,6V16H9V12A2,2 0 0,1 11,10H16A2,2 0 0,1 18,12V16H20V6H4M0,20V18H4A2,2 0 0,1 2,16V6A2,2 0 0,1 4,4H20A2,2 0 0,1 22,6V16A2,2 0 0,1 20,18H24V20H18V20C18,21.11 17.1,22 16,22H11A2,2 0 0,1 9,20H9L0,20M11.5,20A0.5,0.5 0 0,0 11,20.5A0.5,0.5 0 0,0 11.5,21A0.5,0.5 0 0,0 12,20.5A0.5,0.5 0 0,0 11.5,20M15.5,20A0.5,0.5 0 0,0 15,20.5A0.5,0.5 0 0,0 15.5,21A0.5,0.5 0 0,0 16,20.5A0.5,0.5 0 0,0 15.5,20M13,20V21H14V20H13M11,12V19H16V12H11Z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-search">
|
||||
<path d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"></path>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-legal">
|
||||
<path d="M12,3C10.73,3 9.6,3.8 9.18,5H3V7H4.95L2,14C1.53,16 3,17 5.5,17C8,17 9.56,16 9,14L6.05,7H9.17C9.5,7.85 10.15,8.5 11,8.83V20H2V22H22V20H13V8.82C13.85,8.5 14.5,7.85 14.82,7H17.95L15,14C14.53,16 16,17 18.5,17C21,17 22.56,16 22,14L19.05,7H21V5H14.83C14.4,3.8 13.27,3 12,3M12,5A1,1 0 0,1 13,6A1,1 0 0,1 12,7A1,1 0 0,1 11,6A1,1 0 0,1 12,5M5.5,10.25L7,14H4L5.5,10.25M18.5,10.25L20,14H17L18.5,10.25Z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-external-link">
|
||||
<path d="M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"></path>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-code-block">
|
||||
<path d="M5,3H7V5H5V10A2,2 0 0,1 3,12A2,2 0 0,1 5,14V19H7V21H5C3.93,20.73 3,20.1 3,19V15A2,2 0 0,0 1,13H0V11H1A2,2 0 0,0 3,9V5A2,2 0 0,1 5,3M19,3A2,2 0 0,1 21,5V9A2,2 0 0,0 23,11H24V13H23A2,2 0 0,0 21,15V19A2,2 0 0,1 19,21H17V19H19V14A2,2 0 0,1 21,12A2,2 0 0,1 19,10V5H17V3H19M12,15A1,1 0 0,1 13,16A1,1 0 0,1 12,17A1,1 0 0,1 11,16A1,1 0 0,1 12,15M8,15A1,1 0 0,1 9,16A1,1 0 0,1 8,17A1,1 0 0,1 7,16A1,1 0 0,1 8,15M16,15A1,1 0 0,1 17,16A1,1 0 0,1 16,17A1,1 0 0,1 15,16A1,1 0 0,1 16,15Z"></path>
|
||||
</symbol>
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-ads-slash">
|
||||
<path d="M12.2 9L10.2 7H13C14.1 7 15 7.9 15 9V11.8L13 9.8V9H12.2M23 9V7H19C17.9 7 17 7.9 17 9V11C17 12.1 17.9 13 19 13H21V15H18.2L20.2 17H21C22.1 17 23 16.1 23 15V13C23 11.9 22.1 11 21 11H19V9H23M22.1 21.5L20.8 22.8L14.4 16.4C14.1 16.7 13.6 17 13 17H9V10.9L7 8.9V17H5V13H3V17H1V9C1 7.9 1.9 7 3 7H5.1L1.1 3L2.4 1.7L22.1 21.5M5 9H3V11H5V9M13 14.9L11 12.9V15H13V14.9Z"></path>
|
||||
</symbol>
|
||||
|
||||
</svg>
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 13 KiB |
48
src/components/Error/ErrorInfo.tsx
Normal file
48
src/components/Error/ErrorInfo.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
import Link from 'next/link';
|
||||
import Layout from '../../layouts/Layout';
|
||||
import Meta from '../Meta/Meta';
|
||||
|
||||
import styles from '../../styles/modules/components/error/error-info.module.scss';
|
||||
|
||||
// for details regarding the svg, go to sadgnu.svg file
|
||||
// description copied verbatim from https://www.gnu.org/graphics/sventsitsky-sadgnu.html
|
||||
// 404 idea from ninamori.org 404 page.
|
||||
|
||||
const ErrorInfo = ({ message = 'Not found, sorry.', statusCode = 404 }) => {
|
||||
return (
|
||||
<>
|
||||
<Meta
|
||||
title={`${message} (${statusCode})`}
|
||||
description='you encountered an error page!'
|
||||
/>
|
||||
<Layout className={styles.error}>
|
||||
<svg
|
||||
className={styles.gnu}
|
||||
focusable='false'
|
||||
role='img'
|
||||
aria-labelledby='gnu-title gnu-desc'
|
||||
>
|
||||
<title id='gnu-title'>GNU and Tux</title>
|
||||
<desc id='gnu-desc'>
|
||||
A pencil drawing of a big gnu and a small penguin, both very sad.
|
||||
GNU is despondently sitting on a bench, and Tux stands beside him,
|
||||
looking down and patting him on the back.
|
||||
</desc>
|
||||
<use href='/svg/sadgnu.svg#sad-gnu'></use>
|
||||
</svg>
|
||||
<h1 className={`heading heading__primary ${styles.heading}`}>
|
||||
<span>{message}</span>
|
||||
<span> ({statusCode})</span>
|
||||
</h1>
|
||||
<p className={styles.back}>
|
||||
Go back to{' '}
|
||||
<Link href='/about'>
|
||||
<a className='link'>the homepage</a>
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default ErrorInfo;
|
|
@ -1,5 +1,4 @@
|
|||
import Head from 'next/head';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
|
@ -7,15 +6,11 @@ type Props = {
|
|||
imgUrl?: string;
|
||||
};
|
||||
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_URL ?? 'https://iket.me';
|
||||
|
||||
const Meta = ({
|
||||
title,
|
||||
description = 'libremdb, a free & open source IMDb front-end.',
|
||||
imgUrl = 'icon.svg',
|
||||
}: Props) => {
|
||||
const url = new URL(imgUrl, BASE_URL);
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<meta charSet='UTF-8' />
|
||||
|
@ -35,7 +30,10 @@ const Meta = ({
|
|||
<meta property='og:site_name' content='libremdb' />
|
||||
<meta property='og:locale' content='en_US' />
|
||||
<meta property='og:type' content='video.movie' />
|
||||
<meta property='og:image' content={url.toString()} />
|
||||
<meta
|
||||
property='og:image'
|
||||
content={`${process.env.NEXT_PUBLIC_URL}/${imgUrl}`}
|
||||
/>
|
||||
</Head>
|
||||
);
|
||||
};
|
|
@ -1,6 +1,7 @@
|
|||
import { useContext } from 'react';
|
||||
import { themeContext } from 'src/context/theme-context';
|
||||
import styles from 'src/styles/modules/components/buttons/themeToggler.module.scss';
|
||||
import { themeContext } from '../../context/theme-context';
|
||||
|
||||
import styles from '../../styles/modules/components/buttons/themeToggler.module.scss';
|
||||
|
||||
type Props = {
|
||||
className: string;
|
||||
|
@ -16,9 +17,9 @@ const ThemeToggler = (props: Props) => {
|
|||
return (
|
||||
<button
|
||||
className={`${styles.button} ${props.className}`}
|
||||
aria-label='Change theme'
|
||||
onClick={clickHandler}
|
||||
>
|
||||
<span className='visually-hidden'>Change theme</span>
|
||||
<svg
|
||||
className={`icon ${styles.icon}`}
|
||||
focusable='false'
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
import type { ReactNode, ElementType, ComponentPropsWithoutRef } from 'react';
|
||||
import styles from 'src/styles/modules/components/card/card.module.scss';
|
||||
|
||||
// ensuring that other attributes to <Card/> are correct based on the value of 'as' prop.
|
||||
// a cheap implementation of as prop found in libraries like CharkaUI or MaterialUI.
|
||||
type Props<T extends ElementType> = {
|
||||
children: ReactNode;
|
||||
as?: T | 'section';
|
||||
hoverable?: true;
|
||||
} & ComponentPropsWithoutRef<T>;
|
||||
|
||||
const Card = <T extends ElementType = 'li'>({
|
||||
children,
|
||||
as,
|
||||
hoverable,
|
||||
className,
|
||||
...rest
|
||||
}: Props<T>) => {
|
||||
const Component = as ?? 'li';
|
||||
const classNames = `${hoverable ? styles.hoverable : ''} ${styles.card} ${className}`;
|
||||
|
||||
return (
|
||||
<Component className={classNames} {...rest}>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
|
||||
export default Card;
|
|
@ -1,45 +0,0 @@
|
|||
import { ComponentPropsWithoutRef, CSSProperties, ReactNode } from 'react';
|
||||
import Image from 'next/future/image';
|
||||
import Card from './Card';
|
||||
import { getProxiedIMDbImgUrl, modifyIMDbImg } from 'src/utils/helpers';
|
||||
import styles from 'src/styles/modules/components/card/card-basic.module.scss';
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
image?: string;
|
||||
title: string;
|
||||
} & ComponentPropsWithoutRef<'section'>;
|
||||
|
||||
const CardBasic = ({ image, children, className, title, ...rest }: Props) => {
|
||||
const style: CSSProperties = {
|
||||
backgroundImage: image && `url(${getProxiedIMDbImgUrl(modifyIMDbImg(image, 300))})`,
|
||||
};
|
||||
|
||||
return (
|
||||
<Card as='section' className={`${styles.container} ${className}`} {...rest}>
|
||||
<div className={styles.imageContainer} style={style}>
|
||||
{image ? (
|
||||
<Image
|
||||
className={styles.image}
|
||||
src={modifyIMDbImg(image)}
|
||||
alt=''
|
||||
priority
|
||||
fill
|
||||
sizes='300px'
|
||||
/>
|
||||
) : (
|
||||
<svg className={styles.imageNA}>
|
||||
<use href='/svg/sprite.svg#icon-image-slash' />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<h1 className={`${styles.title} heading heading__primary`}>{title}</h1>
|
||||
{children}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardBasic;
|
|
@ -1,51 +0,0 @@
|
|||
import Card from './Card';
|
||||
import styles from 'src/styles/modules/components/card/card-cast.module.scss';
|
||||
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/future/image';
|
||||
import { modifyIMDbImg } from 'src/utils/helpers';
|
||||
|
||||
type Props = {
|
||||
link: string;
|
||||
name: string;
|
||||
characters: string[] | null;
|
||||
attributes: string[] | null;
|
||||
image?: string | null;
|
||||
children?: ReactNode;
|
||||
} & ComponentPropsWithoutRef<'li'>;
|
||||
|
||||
const CardCast = ({ link, name, image, children, characters, attributes, ...rest }: Props) => {
|
||||
return (
|
||||
<Card hoverable {...rest}>
|
||||
<Link href={link}>
|
||||
<a className={styles.item}>
|
||||
<div className={styles.imgContainer}>
|
||||
{image ? (
|
||||
<Image
|
||||
src={modifyIMDbImg(image, 400)}
|
||||
alt=''
|
||||
fill
|
||||
className={styles.img}
|
||||
sizes='200px'
|
||||
/>
|
||||
) : (
|
||||
<svg className={styles.imgNA}>
|
||||
<use href='/svg/sprite.svg#icon-image-slash' />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.textContainer}>
|
||||
<p className={`heading ${styles.name}`}>{name}</p>
|
||||
<p className={styles.role}>
|
||||
{characters?.join(', ')}
|
||||
{attributes && <span> ({attributes.join(', ')})</span>}
|
||||
</p>
|
||||
{children}
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardCast;
|
|
@ -1,40 +0,0 @@
|
|||
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/future/image';
|
||||
import Card from './Card';
|
||||
import { modifyIMDbImg } from 'src/utils/helpers';
|
||||
import styles from 'src/styles/modules/components/card/card-result.module.scss';
|
||||
|
||||
type Props = {
|
||||
link: string;
|
||||
name: string;
|
||||
image?: string;
|
||||
showImage?: true;
|
||||
children?: ReactNode;
|
||||
} & ComponentPropsWithoutRef<'li'>;
|
||||
|
||||
const CardResult = ({ link, name, image, showImage, children, ...rest }: Props) => {
|
||||
let ImageComponent = null;
|
||||
if (showImage)
|
||||
ImageComponent = image ? (
|
||||
<Image src={modifyIMDbImg(image, 400)} alt='' fill className={styles.img} sizes='200px' />
|
||||
) : (
|
||||
<svg className={styles.imgNA}>
|
||||
<use href='/svg/sprite.svg#icon-image-slash' />
|
||||
</svg>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card hoverable {...rest} className={`${styles.item} ${!showImage && styles.sansImage}`}>
|
||||
<div className={styles.imgContainer}>{ImageComponent}</div>
|
||||
<div className={styles.info}>
|
||||
<Link href={link}>
|
||||
<a className={`heading ${styles.heading}`}>{name}</a>
|
||||
</Link>
|
||||
{children}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardResult;
|
|
@ -1,63 +0,0 @@
|
|||
import Card from './Card';
|
||||
import styles from 'src/styles/modules/components/card/card-title.module.scss';
|
||||
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/future/image';
|
||||
import { formatNumber, modifyIMDbImg } from 'src/utils/helpers';
|
||||
|
||||
type Props = {
|
||||
link: string;
|
||||
name: string;
|
||||
titleType: string;
|
||||
year?: { start: number; end: number | null };
|
||||
ratings?: { avg: number | null; numVotes: number };
|
||||
image?: string;
|
||||
children?: ReactNode;
|
||||
} & ComponentPropsWithoutRef<'li'>;
|
||||
|
||||
const CardTitle = ({ link, name, year, image, ratings, titleType, children, ...rest }: Props) => {
|
||||
const years = year?.end ? `${year.start}-${year.end}` : year?.start;
|
||||
|
||||
return (
|
||||
<Card hoverable {...rest}>
|
||||
<Link href={link}>
|
||||
<a className={styles.item}>
|
||||
<div className={styles.imgContainer}>
|
||||
{image ? (
|
||||
<Image
|
||||
src={modifyIMDbImg(image, 400)}
|
||||
alt=''
|
||||
fill
|
||||
className={styles.img}
|
||||
sizes='200px'
|
||||
/>
|
||||
) : (
|
||||
<svg className={styles.imgNA}>
|
||||
<use href='/svg/sprite.svg#icon-image-slash' />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.textContainer}>
|
||||
<p className={`heading ${styles.name}`}>{name}</p>
|
||||
<p>
|
||||
<span>{titleType}</span>
|
||||
<span>{years && ` (${years})`}</span>
|
||||
</p>
|
||||
{ratings?.avg && (
|
||||
<p className={styles.rating}>
|
||||
<span className={styles.ratingNum}>{ratings.avg}</span>
|
||||
<svg className={styles.ratingIcon}>
|
||||
<use href='/svg/sprite.svg#icon-rating'></use>
|
||||
</svg>
|
||||
<span> ({formatNumber(ratings.numVotes)} votes)</span>
|
||||
</p>
|
||||
)}
|
||||
<div className={styles.children}>{children}</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardTitle;
|
|
@ -1,5 +0,0 @@
|
|||
export { default as Card } from './Card';
|
||||
export { default as CardTitle } from './CardTitle';
|
||||
export { default as CardBasic } from './CardBasic';
|
||||
export { default as CardCast } from './CardCast';
|
||||
export { default as CardResult } from './CardResult';
|
|
@ -1,45 +0,0 @@
|
|||
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
||||
import ErrorInfoComponent from './ErrorInfo';
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
type State = {
|
||||
hasError: boolean;
|
||||
};
|
||||
|
||||
class ErrorBoundary extends Component<Props, State> {
|
||||
state: State = {
|
||||
hasError: false,
|
||||
};
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
console.error('Uncaught error:', error, errorInfo);
|
||||
}
|
||||
|
||||
resetError() {
|
||||
this.setState({ hasError: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError)
|
||||
return (
|
||||
<ErrorInfoComponent
|
||||
message='Something weird happened on your browser.'
|
||||
misc={{
|
||||
subtext: 'Check console for more information.',
|
||||
buttonClickHandler: this.resetError.bind(this),
|
||||
buttonText: 'Reload Page',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
|
@ -1,71 +0,0 @@
|
|||
import Link from 'next/link';
|
||||
import Layout from 'src/components/layout';
|
||||
import Meta from 'src/components/meta/Meta';
|
||||
import styles from 'src/styles/modules/components/error/error-info.module.scss';
|
||||
|
||||
// for details regarding the svg, go to sadgnu.svg file
|
||||
// description copied verbatim from https://www.gnu.org/graphics/sventsitsky-sadgnu.html
|
||||
// 404 idea from ninamori.org 404 page.
|
||||
|
||||
type Props = {
|
||||
message: string;
|
||||
statusCode?: number;
|
||||
originalPath?: string;
|
||||
/** props specific to error boundary. */
|
||||
misc?: {
|
||||
subtext: string;
|
||||
buttonText: string;
|
||||
buttonClickHandler: () => void;
|
||||
};
|
||||
};
|
||||
|
||||
const ErrorInfo = ({ message, statusCode, misc, originalPath }: Props) => {
|
||||
const title = statusCode ? `${message} (${statusCode})` : message;
|
||||
return (
|
||||
<>
|
||||
<Meta title={title} description='you encountered an error page!' />
|
||||
<Layout className={styles.error} originalPath={originalPath}>
|
||||
<svg
|
||||
className={styles.gnu}
|
||||
focusable='false'
|
||||
role='img'
|
||||
aria-labelledby='gnu-title gnu-desc'
|
||||
>
|
||||
<title id='gnu-title'>GNU and Tux</title>
|
||||
<desc id='gnu-desc'>
|
||||
A pencil drawing of a big gnu and a small penguin, both very sad. GNU is despondently
|
||||
sitting on a bench, and Tux stands beside him, looking down and patting him on the back.
|
||||
</desc>
|
||||
<use href='/svg/sadgnu.svg#sad-gnu'></use>
|
||||
</svg>
|
||||
<h1 className={`heading heading__primary ${styles.heading}`}>{title}</h1>
|
||||
{misc ? (
|
||||
<>
|
||||
<p>{misc.subtext}</p>
|
||||
<button className={styles.button} onClick={misc.buttonClickHandler}>
|
||||
{misc.buttonText}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<p>
|
||||
Go back to{' '}
|
||||
<Link href='/'>
|
||||
<a className='link'>the homepage</a>
|
||||
</Link>
|
||||
, or view this route{' '}
|
||||
<a
|
||||
className='link'
|
||||
href={`https://www.imdb.com${originalPath ?? ''}`}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
on IMDb
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
)}
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default ErrorInfo;
|
|
@ -1,13 +0,0 @@
|
|||
import { CardResult } from 'src/components/card';
|
||||
import { Companies } from 'src/interfaces/shared/search';
|
||||
|
||||
type Props = { company: Companies[number] };
|
||||
|
||||
const Company = ({ company }: Props) => (
|
||||
<CardResult name={company.name} link={`/search/title?companies=${company.id}`}>
|
||||
{company.country && <p>{company.country}</p>}
|
||||
{!!company.type && <p>{company.type}</p>}
|
||||
</CardResult>
|
||||
);
|
||||
|
||||
export default Company;
|
|
@ -1,12 +0,0 @@
|
|||
import { CardResult } from 'src/components/card';
|
||||
import { Keywords } from 'src/interfaces/shared/search';
|
||||
|
||||
type Props = { keyword: Keywords[number] };
|
||||
|
||||
const Keyword = ({ keyword }: Props) => (
|
||||
<CardResult link={`/search/keyword?keywords=${keyword.text}`} name={keyword.text}>
|
||||
{keyword.numTitles && <p>{keyword.numTitles} titles</p>}
|
||||
</CardResult>
|
||||
);
|
||||
|
||||
export default Keyword;
|
|
@ -1,20 +0,0 @@
|
|||
import { CardResult } from 'src/components/card';
|
||||
import { People } from 'src/interfaces/shared/search';
|
||||
import styles from 'src/styles/modules/components/find/person.module.scss';
|
||||
|
||||
type Props = { person: People[number] };
|
||||
|
||||
const Person = ({ person }: Props) => {
|
||||
return (
|
||||
<CardResult showImage name={person.name} link={`/name/${person.id}`} image={person.image?.url}>
|
||||
<p>{person.aka}</p>
|
||||
<p>{person.jobCateogry}</p>
|
||||
<ul className={styles.basicInfo} aria-label='quick facts'>
|
||||
{person.knownForTitle && <li>{person.knownForTitle}</li>}
|
||||
{person.knownInYear && <li>{person.knownInYear}</li>}
|
||||
</ul>
|
||||
</CardResult>
|
||||
);
|
||||
};
|
||||
|
||||
export default Person;
|
|
@ -1,37 +0,0 @@
|
|||
import Link from 'next/link';
|
||||
import { CardResult } from 'src/components/card';
|
||||
import { Titles } from 'src/interfaces/shared/search';
|
||||
import styles from 'src/styles/modules/components/find/title.module.scss';
|
||||
|
||||
type Props = { title: Titles[number] };
|
||||
|
||||
const Title = ({ title }: Props) => {
|
||||
return (
|
||||
<CardResult showImage name={title.name} link={`/title/${title.id}`} image={title.image?.url}>
|
||||
<ul aria-label='quick facts' className={styles.basicInfo}>
|
||||
<li>{title.type}</li>
|
||||
<li>{title.sAndE}</li>
|
||||
<li>{title.releaseYear}</li>
|
||||
</ul>
|
||||
{!!title.credits.length && (
|
||||
<p className={styles.stars}>
|
||||
<span>Stars: </span>
|
||||
{title.credits.join(', ')}
|
||||
</p>
|
||||
)}
|
||||
{title.seriesId && (
|
||||
<ul aria-label='quick series facts' className={styles.seriesInfo}>
|
||||
{title.seriesType && <li>{title.seriesType}</li>}
|
||||
<li>
|
||||
<Link href={`/title/${title.seriesId}`}>
|
||||
<a className='link'>{title.seriesName}</a>
|
||||
</Link>
|
||||
</li>
|
||||
{title.seriesReleaseYear && <li>{title.seriesReleaseYear}</li>}
|
||||
</ul>
|
||||
)}
|
||||
</CardResult>
|
||||
);
|
||||
};
|
||||
|
||||
export default Title;
|
|
@ -1,92 +0,0 @@
|
|||
import Company from './Company';
|
||||
import Person from './Person';
|
||||
import Title from './Title';
|
||||
import Keyword from './Keyword';
|
||||
import Find from 'src/interfaces/shared/search';
|
||||
import { getResTitleTypeHeading } from 'src/utils/helpers';
|
||||
import styles from 'src/styles/modules/components/find/results.module.scss';
|
||||
|
||||
type Props = {
|
||||
results: Find | null;
|
||||
className?: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
const resultsExist = (
|
||||
results: Props['results']
|
||||
): results is NonNullable<Props['results']> =>
|
||||
Boolean(
|
||||
results &&
|
||||
(results.people.length ||
|
||||
results.keywords.length ||
|
||||
results.companies.length ||
|
||||
results.titles.length)
|
||||
);
|
||||
|
||||
// MAIN COMPONENT
|
||||
const Results = ({ results, className, title }: Props) => {
|
||||
if (!resultsExist(results))
|
||||
return (
|
||||
<h1 className={`heading heading__primary ${className}`}>
|
||||
No results found
|
||||
</h1>
|
||||
);
|
||||
|
||||
const { titles, people, keywords, companies, meta } = results;
|
||||
const titlesSectionHeading = getResTitleTypeHeading(
|
||||
meta.type,
|
||||
meta.titleType
|
||||
);
|
||||
|
||||
return (
|
||||
<article className={`${className} ${styles.results}`}>
|
||||
<h1 className='heading heading__primary'>Results for '{title}'</h1>
|
||||
<div className={styles.results__list}>
|
||||
{!!titles.length && (
|
||||
<section className={styles.titles}>
|
||||
<h2 className='heading heading__secondary'>
|
||||
{titlesSectionHeading}
|
||||
</h2>
|
||||
<ul className={styles.titles__list}>
|
||||
{titles.map(title => (
|
||||
<Title title={title} key={title.id} />
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
{!!people.length && (
|
||||
<section className={styles.people}>
|
||||
<h2 className='heading heading__secondary'>People</h2>
|
||||
<ul className={styles.people__list}>
|
||||
{people.map(person => (
|
||||
<Person person={person} key={person.id} />
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
{!!companies.length && (
|
||||
<section className={styles.people}>
|
||||
<h2 className='heading heading__secondary'>Companies</h2>
|
||||
<ul className={styles.people__list}>
|
||||
{companies.map(company => (
|
||||
<Company company={company} key={company.id} />
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
{!!keywords.length && (
|
||||
<section className={styles.people}>
|
||||
<h2 className='heading heading__secondary'>Keywords</h2>
|
||||
<ul className={styles.people__list}>
|
||||
{keywords.map(keyword => (
|
||||
<Keyword keyword={keyword} key={keyword.id} />
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
|
||||
export default Results;
|
|
@ -1,121 +0,0 @@
|
|||
import { ChangeEventHandler, FormEventHandler, useRef, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { cleanQueryStr } from 'src/utils/helpers';
|
||||
import { QueryTypes } from 'src/interfaces/shared/search';
|
||||
import { resultTypes, resultTitleTypes } from 'src/utils/constants/find';
|
||||
import styles from 'src/styles/modules/components/form/find.module.scss';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const Form = ({ className }: Props) => {
|
||||
const router = useRouter();
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const [isDisabled, setIsDisabled] = useState(false);
|
||||
|
||||
// title types can't be selected unless type selected is 'title'
|
||||
const typesChangeHandler: ChangeEventHandler<HTMLFieldSetElement> = e => {
|
||||
const el = e.target as unknown as HTMLInputElement; // we have only radios that'll fire change event.
|
||||
const value = el.value as QueryTypes;
|
||||
|
||||
if (value === 'tt') setIsDisabled(false);
|
||||
else setIsDisabled(true);
|
||||
};
|
||||
|
||||
// preventing page refresh and instead handling submission through js
|
||||
const submitHandler: FormEventHandler<HTMLFormElement> = e => {
|
||||
e.preventDefault();
|
||||
|
||||
const formEl = formRef.current!;
|
||||
const formData = new FormData(formEl);
|
||||
const query = (formData.get('q') as string).trim();
|
||||
|
||||
const entries = [...formData.entries()] as [string, string][];
|
||||
const queryStr = cleanQueryStr(entries);
|
||||
|
||||
if (query) router.push(`/find?${queryStr}`);
|
||||
else setIsDisabled(false);
|
||||
formEl.reset();
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
action='/find'
|
||||
onSubmit={submitHandler}
|
||||
ref={formRef}
|
||||
className={`${className} ${styles.form}`}
|
||||
>
|
||||
<p className='heading heading__primary'>Search</p>
|
||||
|
||||
<p className={styles.searchbar}>
|
||||
<svg
|
||||
className={`icon ${styles.searchbar__icon}`}
|
||||
focusable='false'
|
||||
aria-hidden='true'
|
||||
role='img'
|
||||
>
|
||||
<use href='/svg/sprite.svg#icon-search'></use>
|
||||
</svg>
|
||||
<input
|
||||
id='searchbar'
|
||||
type='search'
|
||||
name='q'
|
||||
placeholder='movies, people...'
|
||||
className={styles.searchbar__input}
|
||||
required
|
||||
minLength={2}
|
||||
/>
|
||||
<label className='visually-hidden' htmlFor='searchbar'>
|
||||
Search for anything
|
||||
</label>
|
||||
</p>
|
||||
<fieldset className={styles.types} onChange={typesChangeHandler}>
|
||||
<legend className={`heading ${styles.types__heading}`}>Filter by Type</legend>
|
||||
<RadioBtns data={resultTypes} className={styles.type} />
|
||||
</fieldset>
|
||||
<fieldset className={styles.titleTypes} disabled={isDisabled}>
|
||||
<legend className={`heading ${styles.titleTypes__heading}`}>Filter by Title Type</legend>
|
||||
<RadioBtns data={resultTitleTypes} className={styles.titleType} />
|
||||
</fieldset>
|
||||
<p className={styles.exact}>
|
||||
<label htmlFor='exact'>Exact Matches</label>
|
||||
<input type='checkbox' name='exact' id='exact' value='true' />
|
||||
</p>
|
||||
<div className={styles.buttons}>
|
||||
<button type='reset' className={styles.button}>
|
||||
Clear
|
||||
</button>
|
||||
<button type='submit' className={styles.button}>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const RadioBtns = ({
|
||||
data,
|
||||
className,
|
||||
}: {
|
||||
data: typeof resultTypes | typeof resultTitleTypes;
|
||||
className: string;
|
||||
}) => (
|
||||
<>
|
||||
{data.types.map(({ name, val }) => (
|
||||
<p className={className} key={val}>
|
||||
<input
|
||||
type='radio'
|
||||
name={data.key}
|
||||
id={`${data.key}:${val}`}
|
||||
value={val}
|
||||
className='visually-hidden'
|
||||
/>
|
||||
<label htmlFor={`${data.key}:${val}`}>{name}</label>
|
||||
</p>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
export default Form;
|
|
@ -1,52 +0,0 @@
|
|||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import styles from 'src/styles/modules/layout/footer.module.scss';
|
||||
|
||||
const links = [
|
||||
{ path: '/about', text: 'About' },
|
||||
{ path: '/find', text: 'Find' },
|
||||
{ path: '/privacy', text: 'Privacy' },
|
||||
{ path: '/contact', text: 'Contact' },
|
||||
] as const;
|
||||
|
||||
const Footer = () => {
|
||||
const { pathname } = useRouter();
|
||||
|
||||
return (
|
||||
<footer id='footer' className={styles.footer}>
|
||||
<nav aria-label='primary navigation' className={styles.nav}>
|
||||
<ul className={styles.list}>
|
||||
{links.map(link => (
|
||||
<li className={styles.nav__item} key={link.path}>
|
||||
<Link href={link.path}>
|
||||
<a
|
||||
className={styles.nav__link}
|
||||
aria-current={pathname === link.path ? 'page' : undefined}
|
||||
>
|
||||
{link.text}
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
<li className={styles.nav__item}>
|
||||
<a href='#' className={styles.nav__link}>
|
||||
Back to top
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<p className={styles.licence}>
|
||||
Licensed under{' '}
|
||||
<a
|
||||
className={styles.nav__link}
|
||||
href='https://www.gnu.org/licenses/agpl-3.0-standalone.html'
|
||||
>
|
||||
GNU AGPLv3
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
|
@ -1,23 +0,0 @@
|
|||
import type { DataKind, Data as TData } from 'src/interfaces/shared/list';
|
||||
import type { ToArray } from 'src/interfaces/shared';
|
||||
import Images from './Images';
|
||||
import Names from './Names';
|
||||
import Titles from './Titles';
|
||||
|
||||
type Props = {
|
||||
data: ToArray<TData<DataKind>>;
|
||||
};
|
||||
|
||||
const Data = ({ data }: Props) => {
|
||||
if (isDataImages(data)) return <Images images={data} />;
|
||||
if (isDataNames(data)) return <Names names={data} />;
|
||||
|
||||
return <Titles titles={data} />;
|
||||
};
|
||||
export default Data;
|
||||
|
||||
const isDataImages = (data: unknown): data is TData<'images'>[] =>
|
||||
Array.isArray(data) && typeof data[0] === 'string';
|
||||
|
||||
const isDataNames = (data: unknown): data is TData<'names'>[] =>
|
||||
Array.isArray(data) && data[0] && typeof data[0] === 'object' && 'about' in data[0];
|
|
@ -1,22 +0,0 @@
|
|||
import Image from 'next/future/image';
|
||||
import { modifyIMDbImg } from 'src/utils/helpers';
|
||||
import type { Data } from 'src/interfaces/shared/list';
|
||||
import styles from 'src/styles/modules/components/list/images.module.scss';
|
||||
|
||||
type Props = {
|
||||
images: Data<'images'>[];
|
||||
};
|
||||
|
||||
const Images = ({ images }: Props) => {
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
{images.map(image => (
|
||||
<figure className={styles.imgContainer} key={image}>
|
||||
<Image src={modifyIMDbImg(image, 400)} alt='' fill className={styles.img} sizes='200px'/>
|
||||
</figure>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Images;
|
|
@ -1,35 +0,0 @@
|
|||
import Link from 'next/link';
|
||||
import { formatDate } from 'src/utils/helpers';
|
||||
import List from 'src/interfaces/shared/list';
|
||||
import styles from 'src/styles/modules/components/list/meta.module.scss';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
meta: List['meta'];
|
||||
description: List['description'];
|
||||
};
|
||||
const Meta = ({ title, meta, description }: Props) => {
|
||||
const by = meta.by.link ? (
|
||||
<Link href={meta.by.link}>
|
||||
<a className='link'>{meta.by.name}</a>
|
||||
</Link>
|
||||
) : (
|
||||
meta.by.name
|
||||
);
|
||||
|
||||
return (
|
||||
<header className={styles.container}>
|
||||
<h1 className='heading heading__secondary'>{title}</h1>
|
||||
<ul className={styles.list}>
|
||||
<li>by {by}</li>
|
||||
<li>{meta.created}</li>
|
||||
{meta.updated && <li>{meta.updated}</li>}
|
||||
<li>
|
||||
{meta.num} {meta.type}
|
||||
</li>
|
||||
</ul>
|
||||
{description && <p>{description}</p>}
|
||||
</header>
|
||||
);
|
||||
};
|
||||
export default Meta;
|
|
@ -1,57 +0,0 @@
|
|||
import Image from 'next/future/image';
|
||||
import { getProxiedIMDbImgUrl, modifyIMDbImg } from 'src/utils/helpers';
|
||||
import { Card } from 'src/components/card';
|
||||
import type { Data } from 'src/interfaces/shared/list';
|
||||
import styles from 'src/styles/modules/components/list/names.module.scss';
|
||||
import OptionalLink from './OptionalLink';
|
||||
|
||||
type Props = {
|
||||
names: Data<'names'>[];
|
||||
};
|
||||
|
||||
const Names = ({ names }: Props) => {
|
||||
return (
|
||||
<ul className={styles.names}>
|
||||
{names.map(name => (
|
||||
<Name {...name} key={name.name} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
export default Names;
|
||||
|
||||
const Name = ({ about, image, job, knownFor, knownForLink, name, url }: Props['names'][number]) => {
|
||||
// const style: CSSProperties = {
|
||||
// backgroundImage: image ? `url(${getProxiedIMDbImgUrl(modifyIMDbImg(image, 300))})` : undefined,
|
||||
// };
|
||||
|
||||
return (
|
||||
<Card hoverable className={styles.name}>
|
||||
<div className={styles.imgContainer}>
|
||||
{image ? (
|
||||
<Image src={modifyIMDbImg(image, 400)} alt='' fill className={styles.img} sizes='200px' />
|
||||
) : (
|
||||
<svg className={styles.imgNA}>
|
||||
<use href='/svg/sprite.svg#icon-image-slash' />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<h2 className={`heading ${styles.heading}`}>
|
||||
<OptionalLink href={url} className={`heading ${styles.heading}`}>
|
||||
{name}
|
||||
</OptionalLink>
|
||||
</h2>
|
||||
<ul className={styles.basicInfo} aria-label='quick facts'>
|
||||
{job && <li>{job}</li>}
|
||||
{knownFor && (
|
||||
<li>
|
||||
<OptionalLink href={knownForLink}>{knownFor}</OptionalLink>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
<p>{about}</p>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
|
@ -1,20 +0,0 @@
|
|||
import type { ReactNode, ComponentPropsWithoutRef } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
const OptionalLink = ({
|
||||
href,
|
||||
children,
|
||||
...rest
|
||||
}: { href?: string | null; children: ReactNode } & Omit<ComponentPropsWithoutRef<'a'>, 'href'>) => (
|
||||
<>
|
||||
{href ? (
|
||||
<Link href={href}>
|
||||
<a {...rest}>{children}</a>
|
||||
</Link>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
export default OptionalLink;
|
|
@ -1,33 +0,0 @@
|
|||
import OptionalLink from './OptionalLink';
|
||||
import type List from 'src/interfaces/shared/list';
|
||||
import styles from 'src/styles/modules/components/list/pagination.module.scss';
|
||||
|
||||
type Props = {
|
||||
pagination: List['pagination'];
|
||||
};
|
||||
const Pagination = ({ pagination }: Props) => {
|
||||
const prevLink = pagination.prev && pagination.prev !== '#' ? pagination.prev : null;
|
||||
const nextLink = pagination.next && pagination.next !== '#' ? pagination.next : null;
|
||||
|
||||
if (!prevLink && !nextLink) return null;
|
||||
|
||||
return (
|
||||
<nav aria-label='pagination'>
|
||||
<ul className={styles.nav}>
|
||||
<li aria-hidden={!prevLink}>
|
||||
<OptionalLink href={prevLink} className='link'>
|
||||
Prev
|
||||
</OptionalLink>
|
||||
</li>
|
||||
<li>{pagination.range} shown</li>
|
||||
<li aria-hidden={!nextLink}>
|
||||
<OptionalLink href={nextLink} className='link'>
|
||||
Next
|
||||
</OptionalLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pagination;
|
|
@ -1,79 +0,0 @@
|
|||
import Image from 'next/future/image';
|
||||
import { getProxiedIMDbImgUrl, modifyIMDbImg } from 'src/utils/helpers';
|
||||
import { Card } from 'src/components/card';
|
||||
import type { Data } from 'src/interfaces/shared/list';
|
||||
import styles from 'src/styles/modules/components/list/titles.module.scss';
|
||||
import { CSSProperties } from 'react';
|
||||
import OptionalLink from './OptionalLink';
|
||||
|
||||
type Props = {
|
||||
titles: Data<'titles'>[];
|
||||
};
|
||||
|
||||
const Titles = ({ titles }: Props) => {
|
||||
return (
|
||||
<ul className={styles.titles}>
|
||||
{titles.map(title => (
|
||||
<Title {...title} key={title.name} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
export default Titles;
|
||||
|
||||
const Title = (props: Props['titles'][number]) => {
|
||||
const style: CSSProperties = {
|
||||
backgroundImage: props.image
|
||||
? `url(${getProxiedIMDbImgUrl(modifyIMDbImg(props.image, 300))})`
|
||||
: undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<Card hoverable className={styles.title}>
|
||||
<div className={styles.imgContainer}>
|
||||
{props.image ? (
|
||||
<Image src={modifyIMDbImg(props.image, 400)} alt='' fill className={styles.img} />
|
||||
) : (
|
||||
<svg className={styles.imgNA}>
|
||||
<use href='/svg/sprite.svg#icon-image-slash' />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<h2 className={`heading heading__tertiary ${styles.heading}`}>
|
||||
<OptionalLink href={props.url} className={`heading ${styles.heading}`}>
|
||||
{props.name} {props.year}
|
||||
</OptionalLink>
|
||||
</h2>
|
||||
<ul className={styles.basicInfo} aria-label='quick facts'>
|
||||
{props.certificate && <li>{props.certificate}</li>}
|
||||
{props.runtime && <li>{props.runtime}</li>}
|
||||
{props.genre && <li>{props.genre}</li>}
|
||||
</ul>
|
||||
<ul className={styles.ratings}>
|
||||
{Boolean(props.rating) && <li className={styles.rating}>
|
||||
<span className={styles.rating__num}>{props.rating}</span>
|
||||
<svg className={styles.rating__icon}>
|
||||
<use href='/svg/sprite.svg#icon-rating'></use>
|
||||
</svg>
|
||||
<span className={styles.rating__text}> Avg. rating</span>
|
||||
</li>}
|
||||
{Boolean(props.metascore) && <li className={styles.rating}>
|
||||
<span className={styles.rating__num}>{props.metascore}</span>
|
||||
<span className={styles.rating__text}>Metascore</span>
|
||||
</li>}
|
||||
</ul>
|
||||
<p className={styles.plot}>
|
||||
<span>Plot:</span> {props.plot}
|
||||
</p>
|
||||
<ul className={styles.otherInfo}>
|
||||
{props.otherInfo.map(([infoHeading, info]) => (
|
||||
<li key={infoHeading}>
|
||||
<span>{infoHeading}:</span> {info}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
|
@ -1,3 +0,0 @@
|
|||
export { default as Data } from './Data';
|
||||
export { default as Meta } from './Meta';
|
||||
export { default as Pagination } from './Pagination';
|
|
@ -1,4 +1,4 @@
|
|||
import styles from 'src/styles/modules/components/loaders/progress-bar.module.scss';
|
||||
import styles from '../../styles/modules/components/loaders/progress-bar.module.scss';
|
||||
|
||||
const ProgressBar = () => {
|
||||
return <span className={styles.progress} role='progressbar'></span>;
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
import { CardBasic } from 'src/components/card';
|
||||
import { Basic as BasicType } from 'src/interfaces/shared/name';
|
||||
import { formatNumber } from 'src/utils/helpers';
|
||||
import styles from 'src/styles/modules/components/name/basic.module.scss';
|
||||
|
||||
type Props = {
|
||||
className: string;
|
||||
data: BasicType;
|
||||
};
|
||||
|
||||
const Basic = ({ data, className }: Props) => {
|
||||
return (
|
||||
<CardBasic className={className} image={data.poster?.url} title={data.name}>
|
||||
<div className={styles.ratings}>
|
||||
{data.ranking && (
|
||||
<p className={styles.rating}>
|
||||
<span className={styles.rating__num}>{formatNumber(data.ranking.position)}</span>
|
||||
<svg className={styles.rating__icon}>
|
||||
<use href='/svg/sprite.svg#icon-graph-rising'></use>
|
||||
</svg>
|
||||
<span className={styles.rating__text}>
|
||||
{' '}
|
||||
Popularity (
|
||||
<span className={styles.rating__sub}>{getRankingStats(data.ranking)}</span>)
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!!data.primaryProfessions.length && (
|
||||
<p className={styles.genres}>
|
||||
<span className={styles.heading}>Profession: </span>
|
||||
{data.primaryProfessions.join(', ')}
|
||||
</p>
|
||||
)}
|
||||
{
|
||||
<p className={styles.overview}>
|
||||
<span className={styles.heading}>About: </span>
|
||||
{data.bio.short}...
|
||||
</p>
|
||||
}
|
||||
{data.knownFor.title && (
|
||||
<p className={styles.genres}>
|
||||
<span className={styles.heading}>Known for: </span>
|
||||
{data.knownFor.title} ({data.knownFor.role})
|
||||
</p>
|
||||
)}
|
||||
</CardBasic>
|
||||
);
|
||||
};
|
||||
|
||||
const getRankingStats = (ranking: NonNullable<Props['data']['ranking']>) => {
|
||||
if (ranking.direction === 'FLAT') return '\u2192';
|
||||
|
||||
const change = formatNumber(ranking.change);
|
||||
return (ranking.direction === 'UP' ? '\u2191' : '\u2193') + change;
|
||||
};
|
||||
|
||||
export default Basic;
|
|
@ -1,12 +0,0 @@
|
|||
import styles from 'src/styles/modules/components/name/did-you-know.module.scss';
|
||||
|
||||
type Props = { bio: string };
|
||||
|
||||
const Bio = ({ bio }: Props) => (
|
||||
<section className={styles.bio}>
|
||||
<h2 className='heading heading__secondary'>About</h2>
|
||||
<div dangerouslySetInnerHTML={{ __html: bio }} />
|
||||
</section>
|
||||
);
|
||||
|
||||
export default Bio;
|
|
@ -1,43 +0,0 @@
|
|||
import { Credits } from 'src/interfaces/shared/name';
|
||||
import { CardTitle } from 'src/components/card';
|
||||
import styles from 'src/styles/modules/components/name/credits.module.scss';
|
||||
|
||||
type Props = {
|
||||
className: string;
|
||||
data: Credits;
|
||||
};
|
||||
|
||||
const Credits = ({ className, data }: Props) => {
|
||||
if (!data.total) return null;
|
||||
|
||||
return (
|
||||
<section className={`${className} ${styles.credits}`}>
|
||||
<h2 className='heading heading__secondary'>Credits</h2>
|
||||
{data.released.map(
|
||||
(item, i) =>
|
||||
!!item.total && (
|
||||
<details open={i === 0} key={item.category.id}>
|
||||
<summary>
|
||||
{item.category.text} ({item.total})
|
||||
</summary>
|
||||
<ul className={styles.container} key={item.category.id}>
|
||||
{item.titles.map(title => (
|
||||
<CardTitle
|
||||
key={title.id}
|
||||
link={`/title/${title.id}`}
|
||||
name={title.title}
|
||||
titleType={title.type.text}
|
||||
image={title.poster?.url}
|
||||
year={title.releaseYear}
|
||||
ratings={title.ratings}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
)
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Credits;
|
|
@ -1,51 +0,0 @@
|
|||
import Link from 'next/link';
|
||||
import { DidYouKnow } from 'src/interfaces/shared/name';
|
||||
import styles from 'src/styles/modules/components/name/did-you-know.module.scss';
|
||||
|
||||
type Props = {
|
||||
data: DidYouKnow;
|
||||
};
|
||||
|
||||
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>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
|
||||
export default DidYouKnow;
|
|
@ -1,184 +0,0 @@
|
|||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import Name, { PersonalDetails } from 'src/interfaces/shared/name';
|
||||
import styles from 'src/styles/modules/components/name/info.module.scss';
|
||||
|
||||
type Props = {
|
||||
info: PersonalDetails;
|
||||
accolades: Name['accolades'];
|
||||
};
|
||||
|
||||
const PersonalDetails = ({ info, accolades }: Props) => {
|
||||
const {
|
||||
query: { nameId },
|
||||
} = useRouter();
|
||||
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
<section className={styles.accolades}>
|
||||
<h2 className='heading heading__secondary'>Accolades</h2>
|
||||
<div className={styles.accolades__container}>
|
||||
{accolades.awards && (
|
||||
<p>
|
||||
<span>
|
||||
Won {accolades.awards.wins} {accolades.awards.name}
|
||||
</span>
|
||||
<span> (out of {accolades.awards.nominations} nominations)</span>
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
{accolades.wins} wins and {accolades.nominations} nominations in total
|
||||
</p>
|
||||
<p>
|
||||
<Link href={`/name/${nameId}/awards`}>
|
||||
<a className='link'>View all awards</a>
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={styles.details}>
|
||||
<h2 className='heading heading__secondary'>Personal details</h2>
|
||||
<div className={styles.details__container}>
|
||||
{!!info.officialSites.length && (
|
||||
<p>
|
||||
<span>Official sites: </span>
|
||||
{info.officialSites.map((site, i) => (
|
||||
<span key={site.url}>
|
||||
{!!i && ', '}
|
||||
<a href={site.url} className='link' target='_blank' rel='noreferrer'>
|
||||
{site.name}
|
||||
</a>
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
{!!info.alsoKnownAs.length && (
|
||||
<p>
|
||||
<span>Also known as: </span>
|
||||
<span>{info.alsoKnownAs.join(', ')}</span>
|
||||
</p>
|
||||
)}
|
||||
{info.height && (
|
||||
<p>
|
||||
<span>Height: </span>
|
||||
<span>{info.height}</span>
|
||||
</p>
|
||||
)}
|
||||
{info.birth && (
|
||||
<p>
|
||||
<span>Born: </span>
|
||||
<span>{info.birth.date}</span>
|
||||
<span>
|
||||
{' '}
|
||||
in{' '}
|
||||
<Link href={`/search/name?birth_place=${info.birth.location}`}>
|
||||
<a className='link'>{info.birth.location}</a>
|
||||
</Link>
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
{info.death.date && (
|
||||
<p>
|
||||
<span>Died: </span>
|
||||
<span>{info.death.date}</span>
|
||||
{info.death.location && (
|
||||
<span>
|
||||
{' '}
|
||||
in{' '}
|
||||
<Link href={`/search/name?death_place=${info.death.location}`}>
|
||||
<a className='link'>{info.death.location}</a>
|
||||
</Link>
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{info.death.cause && (
|
||||
<p>
|
||||
<span>Death cause: </span>
|
||||
<span>{info.death.cause}</span>
|
||||
</p>
|
||||
)}
|
||||
{!!info.spouses?.length && (
|
||||
<p>
|
||||
<span>Spouses: </span>
|
||||
{info.spouses.map((spouse, i) => (
|
||||
<span key={spouse.name}>
|
||||
{!!i && ', '}
|
||||
{renderPersonNameWithLink(spouse)} {spouse.range}
|
||||
{spouse.attributes && ' (' + spouse.attributes.join(', ') + ')'}
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
{!!info.children?.length && (
|
||||
<p>
|
||||
<span>Children: </span>
|
||||
{info.children.map((child, i) => (
|
||||
<span key={child.name}>
|
||||
{!!i && ', '}
|
||||
{renderPersonNameWithLink(child)}
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
{!!info.parents?.length && (
|
||||
<p>
|
||||
<span>Parents: </span>
|
||||
{info.parents.map((parent, i) => (
|
||||
<span key={parent.name}>
|
||||
{!!i && ', '}
|
||||
{renderPersonNameWithLink(parent)}
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
{!!info.relatives?.length && (
|
||||
<p>
|
||||
<span>Relatives: </span>
|
||||
{info.relatives.map((relative, i) => (
|
||||
<span key={relative.name}>
|
||||
{!!i && ', '}
|
||||
{renderPersonNameWithLink(relative)} ({relative.relation})
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
{!!info.otherWorks?.length && (
|
||||
<p>
|
||||
<span>Other Works: </span>
|
||||
{info.otherWorks.map((work, i) => (
|
||||
<span key={work.text}>
|
||||
{!!i && ', '}
|
||||
<span dangerouslySetInnerHTML={{ __html: work.text }} />
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
{!!info.publicity.total && (
|
||||
<p>
|
||||
<span>Publicity Listings: </span>
|
||||
<span>{info.publicity.articles} Articles</span>,{' '}
|
||||
<span>{info.publicity.interviews} Interviews</span>,{' '}
|
||||
<span>{info.publicity.magazines} Magazines</span>,{' '}
|
||||
<span>{info.publicity.pictorials} Pictorials</span>,{' '}
|
||||
<span>{info.publicity.printBiographies} Print biographies</span>, and{' '}
|
||||
<span>{info.publicity.filmBiographies} Biographies</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PersonalDetails;
|
||||
|
||||
const renderPersonNameWithLink = (person: { name: string; id: string | null }) =>
|
||||
person.id ? (
|
||||
<Link href={`/name/${person.id}`}>
|
||||
<a className='link'>{person.name}</a>
|
||||
</Link>
|
||||
) : (
|
||||
<span>{person.name}</span>
|
||||
);
|
|
@ -1,34 +0,0 @@
|
|||
import type { KnownFor as KnownForType } from 'src/interfaces/shared/name';
|
||||
import { CardTitle } from 'src/components/card';
|
||||
import styles from 'src/styles/modules/components/name/known-for.module.scss';
|
||||
|
||||
type Props = { data: KnownForType };
|
||||
|
||||
const KnownFor = ({ data }: Props) => {
|
||||
if (!data.length) return null;
|
||||
|
||||
return (
|
||||
<section className={styles.knownFor}>
|
||||
<h2 className='heading heading__secondary'>Known For</h2>
|
||||
<ul className={styles.container}>
|
||||
{data.map(title => (
|
||||
<CardTitle
|
||||
key={title.id}
|
||||
link={`/title/${title.id}`}
|
||||
name={title.title}
|
||||
titleType={title.type.text}
|
||||
image={title.poster?.url}
|
||||
year={title.releaseYear}
|
||||
>
|
||||
<p className={styles.item__role}>{getRoles(title)}</p>
|
||||
</CardTitle>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const getRoles = (title: Props['data'][number]) =>
|
||||
(title.summary.characters ?? title.summary.jobs)?.join(', ');
|
||||
|
||||
export default KnownFor;
|
|
@ -1,6 +0,0 @@
|
|||
export { default as Basic } from './Basic';
|
||||
export { default as DidYouKnow } from './DidYouKnow';
|
||||
export { default as Info } from './Info';
|
||||
export { default as Credits } from './Credits';
|
||||
export { default as KnownFor } from './KnownFor';
|
||||
export { default as Bio } from './Bio';
|
|
@ -1,9 +1,10 @@
|
|||
import { Fragment } from 'react';
|
||||
import Image from 'next/future/image';
|
||||
import Link from 'next/link';
|
||||
import { CardBasic } from 'src/components/card';
|
||||
import { Basic } from 'src/interfaces/shared/title';
|
||||
import { formatNumber, formatTime } from 'src/utils/helpers';
|
||||
import styles from 'src/styles/modules/components/title/basic.module.scss';
|
||||
|
||||
import { formatNumber, formatTime, modifyIMDbImg } from '../../utils/helpers';
|
||||
import { Basic } from '../../interfaces/shared/title';
|
||||
import styles from '../../styles/modules/components/title/basic.module.scss';
|
||||
|
||||
type Props = {
|
||||
className: string;
|
||||
|
@ -18,92 +19,134 @@ const Basic = ({ data, className }: Props) => {
|
|||
: data.releaseYear?.start;
|
||||
|
||||
return (
|
||||
<CardBasic
|
||||
<section
|
||||
// role is valid but not known to jsx-a11y
|
||||
// aria-description={`basic info for '${data.title}'`}
|
||||
// style={{ backgroundImage: data.poster && `url(${data.poster?.url})` }}
|
||||
className={`${styles.container} ${className}`}
|
||||
image={data.poster?.url}
|
||||
title={data.title}
|
||||
>
|
||||
<ul className={styles.meta} aria-label='quick facts'>
|
||||
{data.status && data.status.id !== 'released' && (
|
||||
<li className={styles.meta__text}>{data.status.text}</li>
|
||||
)}
|
||||
<li className={styles.meta__text}>{data.type.name}</li>
|
||||
{data.releaseYear && <li className={styles.meta__text}>{releaseTime}</li>}
|
||||
{data.ceritficate && <li className={styles.meta__text}>{data.ceritficate}</li>}
|
||||
{data.runtime && <li className={styles.meta__text}>{formatTime(data.runtime)}</li>}
|
||||
</ul>
|
||||
<div className={styles.ratings}>
|
||||
{data.ratings.avg && (
|
||||
<>
|
||||
<p className={styles.rating}>
|
||||
<span className={styles.rating__num}>{data.ratings.avg}</span>
|
||||
<svg className={styles.rating__icon}>
|
||||
<use href='/svg/sprite.svg#icon-rating'></use>
|
||||
</svg>
|
||||
<span className={styles.rating__text}> Avg. rating</span>
|
||||
</p>
|
||||
<p className={styles.rating}>
|
||||
<span className={styles.rating__num}>{formatNumber(data.ratings.numVotes)}</span>
|
||||
<svg className={styles.rating__icon}>
|
||||
<use href='/svg/sprite.svg#icon-like-dislike'></use>
|
||||
</svg>
|
||||
<span className={styles.rating__text}> No. of votes</span>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{data.ranking && (
|
||||
<p className={styles.rating}>
|
||||
<span className={styles.rating__num}>{formatNumber(data.ranking.position)}</span>
|
||||
<svg className={styles.rating__icon}>
|
||||
<use href='/svg/sprite.svg#icon-graph-rising'></use>
|
||||
</svg>
|
||||
<span className={styles.rating__text}>
|
||||
{' '}
|
||||
Popularity (
|
||||
<span className={styles.rating__sub}>
|
||||
{data.ranking.direction === 'UP'
|
||||
? `\u2191${formatNumber(data.ranking.change)}`
|
||||
: data.ranking.direction === 'DOWN'
|
||||
? `\u2193${formatNumber(data.ranking.change)}`
|
||||
: ''}
|
||||
</span>
|
||||
)
|
||||
</span>
|
||||
</p>
|
||||
<div
|
||||
className={styles.imageContainer}
|
||||
style={{
|
||||
backgroundImage:
|
||||
data.poster && `url(${modifyIMDbImg(data.poster.url, 300)})`,
|
||||
}}
|
||||
>
|
||||
{data.poster ? (
|
||||
<Image
|
||||
className={styles.image}
|
||||
src={modifyIMDbImg(data.poster.url)}
|
||||
alt={data.poster.caption}
|
||||
priority
|
||||
fill
|
||||
sizes='300px'
|
||||
/>
|
||||
) : (
|
||||
<svg className={styles.image__NA}>
|
||||
<use href='/svg/sprite.svg#icon-image-slash' />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<h1 className={`${styles.title} heading heading__primary`}>
|
||||
{data.title}
|
||||
</h1>
|
||||
<ul className={styles.meta} aria-label='quick facts'>
|
||||
{data.status.id !== 'released' && (
|
||||
<li className={styles.meta__text}>{data.status.text}</li>
|
||||
)}
|
||||
<li className={styles.meta__text}>{data.type.name}</li>
|
||||
{data.releaseYear && (
|
||||
<li className={styles.meta__text}>{releaseTime}</li>
|
||||
)}
|
||||
{data.ceritficate && (
|
||||
<li className={styles.meta__text}>{data.ceritficate}</li>
|
||||
)}
|
||||
{data.runtime && (
|
||||
<li className={styles.meta__text}>{formatTime(data.runtime)}</li>
|
||||
)}
|
||||
</ul>
|
||||
<div className={styles.ratings}>
|
||||
{data.ratings.avg && (
|
||||
<>
|
||||
<p className={styles.rating}>
|
||||
<span className={styles.rating__num}>{data.ratings.avg}</span>
|
||||
<svg className={styles.rating__icon}>
|
||||
<use href='/svg/sprite.svg#icon-rating'></use>
|
||||
</svg>
|
||||
<span className={styles.rating__text}> Avg. rating</span>
|
||||
</p>
|
||||
<p className={styles.rating}>
|
||||
<span className={styles.rating__num}>
|
||||
{formatNumber(data.ratings.numVotes)}
|
||||
</span>
|
||||
<svg className={styles.rating__icon}>
|
||||
<use href='/svg/sprite.svg#icon-like-dislike'></use>
|
||||
</svg>
|
||||
<span className={styles.rating__text}> No. of votes</span>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{data.ranking && (
|
||||
<p className={styles.rating}>
|
||||
<span className={styles.rating__num}>
|
||||
{formatNumber(data.ranking.position)}
|
||||
</span>
|
||||
<svg className={styles.rating__icon}>
|
||||
<use href='/svg/sprite.svg#icon-graph-rising'></use>
|
||||
</svg>
|
||||
<span className={styles.rating__text}>
|
||||
{' '}
|
||||
Popularity (
|
||||
<span className={styles.rating__sub}>
|
||||
{data.ranking.direction === 'UP'
|
||||
? `\u2191${formatNumber(data.ranking.change)}`
|
||||
: data.ranking.direction === 'DOWN'
|
||||
? `\u2193${formatNumber(data.ranking.change)}`
|
||||
: ''}
|
||||
</span>
|
||||
)
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!!data.genres.length && (
|
||||
<p className={styles.genres}>
|
||||
<span className={styles.genres__heading}>Genres: </span>
|
||||
{data.genres.map((genre, i) => (
|
||||
<Fragment key={genre.id}>
|
||||
{i > 0 && ', '}
|
||||
<Link href={`/search/title?genres=${genre.id}`}>
|
||||
<a className={styles.link}>{genre.text}</a>
|
||||
</Link>
|
||||
</Fragment>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
<p className={styles.overview}>
|
||||
<span className={styles.overview__heading}>Plot: </span>
|
||||
<span className={styles.overview__text}>{data.plot || '-'}</span>
|
||||
</p>
|
||||
{data.primaryCrew.map(crewType => (
|
||||
<p className={styles.crewType} key={crewType.type.id}>
|
||||
<span className={styles.crewType__heading}>{`${crewType.type.category}: `}</span>
|
||||
{crewType.crew.map((crew, i) => (
|
||||
<Fragment key={crew.id}>
|
||||
{i > 0 && ', '}
|
||||
<Link href={`/name/${crew.id}`}>
|
||||
<a className={styles.link}>{crew.name}</a>
|
||||
</Link>
|
||||
</Fragment>
|
||||
))}
|
||||
</p>
|
||||
))}
|
||||
</CardBasic>
|
||||
{!!data.genres.length && (
|
||||
<p className={styles.genres}>
|
||||
<span className={styles.genres__heading}>Genres: </span>
|
||||
{data.genres.map((genre, i) => (
|
||||
<Fragment key={genre.id}>
|
||||
{i > 0 && ', '}
|
||||
<Link href={`/search/title?genres=${genre.id}`}>
|
||||
<a className={styles.link}>{genre.text}</a>
|
||||
</Link>
|
||||
</Fragment>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
{
|
||||
<p className={styles.overview}>
|
||||
<span className={styles.overview__heading}>Plot: </span>
|
||||
<span className={styles.overview__text}>{data.plot || '-'}</span>
|
||||
</p>
|
||||
}
|
||||
{data.primaryCrew.map(crewType => (
|
||||
<p className={styles.crewType} key={crewType.type.id}>
|
||||
<span className={styles.crewType__heading}>
|
||||
{`${crewType.type.category}: `}
|
||||
</span>
|
||||
{crewType.crew.map((crew, i) => (
|
||||
<Fragment key={crew.id}>
|
||||
{i > 0 && ', '}
|
||||
<Link href={`/name/${crew.id}`}>
|
||||
<a className={styles.link}>{crew.name}</a>
|
||||
</Link>
|
||||
</Fragment>
|
||||
))}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { CardCast } from 'src/components/card';
|
||||
import { Cast } from 'src/interfaces/shared/title';
|
||||
import styles from 'src/styles/modules/components/title/cast.module.scss';
|
||||
import Image from 'next/future/image';
|
||||
import Link from 'next/link';
|
||||
import { Cast } from '../../interfaces/shared/title';
|
||||
import { modifyIMDbImg } from '../../utils/helpers';
|
||||
|
||||
import styles from '../../styles/modules/components/title/cast.module.scss';
|
||||
|
||||
type Props = {
|
||||
className: string;
|
||||
|
@ -8,25 +11,46 @@ type Props = {
|
|||
};
|
||||
|
||||
const Cast = ({ className, cast }: Props) => {
|
||||
if (!cast.length) return null;
|
||||
if (!cast.length) return <></>;
|
||||
|
||||
return (
|
||||
<section className={`${className} ${styles.container}`}>
|
||||
<h2 className='heading heading__secondary'>Cast</h2>
|
||||
<ul className={styles.cast}>
|
||||
{cast.map(member => (
|
||||
<CardCast
|
||||
key={member.id}
|
||||
link={`/name/${member.id}`}
|
||||
name={member.name}
|
||||
image={member.image}
|
||||
characters={member.characters}
|
||||
attributes={member.attributes}
|
||||
/>
|
||||
<li key={member.id} className={styles.member}>
|
||||
<div className={styles.member__imgContainer}>
|
||||
{member.image ? (
|
||||
<Image
|
||||
src={modifyIMDbImg(member.image, 400)}
|
||||
alt=''
|
||||
fill
|
||||
className={styles.member__img}
|
||||
sizes='200px'
|
||||
/>
|
||||
) : (
|
||||
<svg className={styles.member__imgNA}>
|
||||
<use href='/svg/sprite.svg#icon-image-slash' />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.member__textContainer}>
|
||||
<p>
|
||||
<Link href={`/name/${member.id}`}>
|
||||
<a className={styles.member__name}>{member.name}</a>
|
||||
</Link>
|
||||
</p>
|
||||
<p className={styles.member__role}>
|
||||
{member.characters?.join(', ')}
|
||||
{member.attributes && (
|
||||
<span> ({member.attributes.join(', ')})</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Cast;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Link from 'next/link';
|
||||
import { DidYouKnow } from 'src/interfaces/shared/title';
|
||||
import styles from 'src/styles/modules/components/title/did-you-know.module.scss';
|
||||
import { Fragment } from 'react';
|
||||
import { DidYouKnow } from '../../interfaces/shared/title';
|
||||
import styles from '../../styles/modules/components/title/did-you-know.module.scss';
|
||||
|
||||
type Props = {
|
||||
data: DidYouKnow;
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Info } from 'src/interfaces/shared/title';
|
||||
import { formatMoney, formatTime } from 'src/utils/helpers';
|
||||
import styles from 'src/styles/modules/components/title/info.module.scss';
|
||||
import { NextRouter } from 'next/router';
|
||||
import { Info } from '../../interfaces/shared/title';
|
||||
import { formatMoney, formatTime } from '../../utils/helpers';
|
||||
|
||||
import styles from '../../styles/modules/components/title/info.module.scss';
|
||||
|
||||
type Props = {
|
||||
info: Info;
|
||||
className: string;
|
||||
router: NextRouter;
|
||||
};
|
||||
|
||||
const Info = ({ info, className }: Props) => {
|
||||
const router = useRouter();
|
||||
const Info = ({ info, className, router }: Props) => {
|
||||
const { titleId } = router.query;
|
||||
const { boxOffice, details, meta, keywords, technicalSpecs, accolades } =
|
||||
info;
|
||||
|
@ -19,7 +20,7 @@ const Info = ({ info, className }: Props) => {
|
|||
<div className={`${className} ${styles.info}`}>
|
||||
{meta.infoEpisode && (
|
||||
<section className={styles.episodeInfo}>
|
||||
<h2 className="heading heading__secondary">Episode info</h2>
|
||||
<h2 className='heading heading__secondary'>Episode info</h2>
|
||||
<div className={styles.episodeInfo__container}>
|
||||
{meta.infoEpisode.numSeason && (
|
||||
<p className={styles.series}>
|
||||
|
@ -49,14 +50,14 @@ const Info = ({ info, className }: Props) => {
|
|||
{meta.infoEpisode.prevId && (
|
||||
<p>
|
||||
<Link href={`/title/${meta.infoEpisode.prevId}`}>
|
||||
<a className="link">Go to previous episode</a>
|
||||
<a className='link'>Go to previous episode</a>
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
{meta.infoEpisode.nextId && (
|
||||
<p>
|
||||
<Link href={`/title/${meta.infoEpisode.nextId}`}>
|
||||
<a className="link">Go to next episode</a>
|
||||
<a className='link'>Go to next episode</a>
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
|
@ -65,7 +66,7 @@ const Info = ({ info, className }: Props) => {
|
|||
)}
|
||||
{meta.infoSeries && (
|
||||
<section className={styles.seriesInfo}>
|
||||
<h2 className="heading heading__secondary">Series info</h2>
|
||||
<h2 className='heading heading__secondary'>Series info</h2>
|
||||
<div className={styles.seriesInfo__container}>
|
||||
<p>
|
||||
<span>Total Seasons: </span>
|
||||
|
@ -81,19 +82,19 @@ const Info = ({ info, className }: Props) => {
|
|||
</p>
|
||||
<p>
|
||||
<Link href={`/title/${titleId}/episodes`}>
|
||||
<a className="link">See all Episodes</a>
|
||||
<a className='link'>See all Episodes</a>
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
<section className={styles.accolades}>
|
||||
<h2 className="heading heading__secondary">Accolades</h2>
|
||||
<h2 className='heading heading__secondary'>Accolades</h2>
|
||||
<div className={styles.accolades__container}>
|
||||
{accolades.topRating && (
|
||||
<p>
|
||||
<Link href={`/chart/top`}>
|
||||
<a className="link">Top rated (#{accolades.topRating})</a>
|
||||
<a className='link'>Top rated (#{accolades.topRating})</a>
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
|
@ -111,21 +112,24 @@ const Info = ({ info, className }: Props) => {
|
|||
</p>
|
||||
<p>
|
||||
<Link href={`/title/${titleId}/awards`}>
|
||||
<a className="link">View all awards</a>
|
||||
<a className='link'>View all awards</a>
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
{!!keywords.total && (
|
||||
<section className={styles.keywords}>
|
||||
<h2 className="heading heading__secondary">Keywords</h2>
|
||||
<h2 className='heading heading__secondary'>Keywords</h2>
|
||||
<ul className={styles.keywords__container}>
|
||||
{keywords.list.map(word => (
|
||||
<li className={styles.keywords__item} key={word}>
|
||||
<Link
|
||||
href={`/search/keyword/?keywords=${word.replace(/\s/g, '-')}`}
|
||||
href={`/search/keyword/?keywords=${word.replaceAll(
|
||||
' ',
|
||||
'-'
|
||||
)}`}
|
||||
>
|
||||
<a className="link">{word}</a>
|
||||
<a className='link'>{word}</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
|
@ -134,7 +138,7 @@ const Info = ({ info, className }: Props) => {
|
|||
)}
|
||||
{!!Object.keys(details).length && (
|
||||
<section className={styles.details}>
|
||||
<h2 className="heading heading__secondary">Details</h2>
|
||||
<h2 className='heading heading__secondary'>Details</h2>
|
||||
<div className={styles.details__container}>
|
||||
{details.releaseDate && (
|
||||
<p>
|
||||
|
@ -155,7 +159,7 @@ const Info = ({ info, className }: Props) => {
|
|||
<Link
|
||||
href={`/search/title/?country_of_origin=${country.id}`}
|
||||
>
|
||||
<a className="link">{country.text}</a>
|
||||
<a className='link'>{country.text}</a>
|
||||
</Link>
|
||||
</span>
|
||||
))}
|
||||
|
@ -167,7 +171,7 @@ const Info = ({ info, className }: Props) => {
|
|||
{details.officialSites.sites.map((site, i) => (
|
||||
<span key={site.url}>
|
||||
{!!i && ', '}
|
||||
<a href={site.url} className="link">
|
||||
<a href={site.url} className='link'>
|
||||
{site.name}
|
||||
</a>
|
||||
</span>
|
||||
|
@ -181,7 +185,7 @@ const Info = ({ info, className }: Props) => {
|
|||
<span key={lang.id}>
|
||||
{!!i && ', '}
|
||||
<Link href={`/search/title/?primary_language=${lang.id}`}>
|
||||
<a className="link">{lang.text}</a>
|
||||
<a className='link'>{lang.text}</a>
|
||||
</Link>
|
||||
</span>
|
||||
))}
|
||||
|
@ -200,7 +204,7 @@ const Info = ({ info, className }: Props) => {
|
|||
<span key={loc}>
|
||||
{!!i && ', '}
|
||||
<Link href={`/search/title/?locations=${loc}`}>
|
||||
<a className="link">{loc}</a>
|
||||
<a className='link'>{loc}</a>
|
||||
</Link>
|
||||
</span>
|
||||
))}
|
||||
|
@ -213,7 +217,7 @@ const Info = ({ info, className }: Props) => {
|
|||
<span key={co.id}>
|
||||
{!!i && ', '}
|
||||
<Link href={`/company/${co.id}`}>
|
||||
<a className="link">{co.name}</a>
|
||||
<a className='link'>{co.name}</a>
|
||||
</Link>
|
||||
</span>
|
||||
))}
|
||||
|
@ -224,7 +228,7 @@ const Info = ({ info, className }: Props) => {
|
|||
)}
|
||||
{!!Object.keys(boxOffice).length && (
|
||||
<section className={styles.boxoffice}>
|
||||
<h2 className="heading heading__secondary">Box office</h2>
|
||||
<h2 className='heading heading__secondary'>Box office</h2>
|
||||
<div className={styles.boxoffice__container}>
|
||||
{boxOffice.budget && (
|
||||
<p>
|
||||
|
@ -276,7 +280,7 @@ const Info = ({ info, className }: Props) => {
|
|||
)}
|
||||
{!!Object.keys(technicalSpecs).length && (
|
||||
<section className={styles.technical}>
|
||||
<h2 className="heading heading__secondary">Technical specs</h2>
|
||||
<h2 className='heading heading__secondary'>Technical specs</h2>
|
||||
<div className={styles.technical__container}>
|
||||
{technicalSpecs.runtime && (
|
||||
<p>
|
||||
|
@ -292,7 +296,7 @@ const Info = ({ info, className }: Props) => {
|
|||
<span key={color.id}>
|
||||
{!!i && ', '}
|
||||
<Link href={`/search/title/?colors=${color.id}`}>
|
||||
<a className="link">{color.name}</a>
|
||||
<a className='link'>{color.name}</a>
|
||||
</Link>
|
||||
</span>
|
||||
))}
|
||||
|
@ -307,7 +311,7 @@ const Info = ({ info, className }: Props) => {
|
|||
<span key={sound.id}>
|
||||
{!!i && ', '}
|
||||
<Link href={`/search/title/?sound_mixes=${sound.id}`}>
|
||||
<a className="link">{sound.name}</a>
|
||||
<a className='link'>{sound.name}</a>
|
||||
</Link>
|
||||
</span>
|
||||
))}
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
import Image from 'next/future/image';
|
||||
import Link from 'next/link';
|
||||
import { Media } from 'src/interfaces/shared';
|
||||
import { getProxiedIMDbImgUrl, modifyIMDbImg } from 'src/utils/helpers';
|
||||
import styles from 'src/styles/modules/components/media/media.module.scss';
|
||||
import { NextRouter } from 'next/router';
|
||||
import { Media } from '../../interfaces/shared/title';
|
||||
import { modifyIMDbImg } from '../../utils/helpers';
|
||||
|
||||
import styles from '../../styles/modules/components/title/media.module.scss';
|
||||
|
||||
type Props = {
|
||||
className: string;
|
||||
media: Media;
|
||||
router: NextRouter;
|
||||
};
|
||||
|
||||
// TODO: refactor this component.
|
||||
|
||||
const Media = ({ className, media }: Props) => {
|
||||
const Media = ({ className, media, router }: Props) => {
|
||||
return (
|
||||
<div className={`${className} ${styles.media}`}>
|
||||
{(media.trailer || !!media.videos.total) && (
|
||||
|
@ -20,20 +21,21 @@ const Media = ({ className, media }: Props) => {
|
|||
|
||||
<div className={styles.videos__container}>
|
||||
{media.trailer && (
|
||||
<div className={styles.trailer}>
|
||||
<div key={router.asPath} className={styles.trailer}>
|
||||
<video
|
||||
aria-label='trailer video'
|
||||
// it's a relatively new tag. hence jsx-all1 complains
|
||||
aria-description={media.trailer.caption}
|
||||
controls
|
||||
playsInline
|
||||
poster={getProxiedIMDbImgUrl(modifyIMDbImg(media.trailer.thumbnail))}
|
||||
poster={modifyIMDbImg(media.trailer.thumbnail)}
|
||||
className={styles.trailer__video}
|
||||
preload='none'
|
||||
>
|
||||
{media.trailer.urls.map(source => (
|
||||
<source
|
||||
key={source.url}
|
||||
type={source.mimeType}
|
||||
src={getProxiedIMDbImgUrl(source.url)}
|
||||
src={source.url}
|
||||
data-res={source.resolution}
|
||||
/>
|
||||
))}
|
||||
|
@ -74,7 +76,9 @@ const Media = ({ className, media }: Props) => {
|
|||
fill
|
||||
sizes='400px'
|
||||
/>
|
||||
<figcaption className={styles.image__caption}>{image.caption.plainText}</figcaption>
|
||||
<figcaption className={styles.image__caption}>
|
||||
{image.caption.plainText}
|
||||
</figcaption>
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
|
@ -1,6 +1,8 @@
|
|||
import { CardTitle } from 'src/components/card';
|
||||
import { MoreLikeThis } from 'src/interfaces/shared/title';
|
||||
import styles from 'src/styles/modules/components/title/more-like-this.module.scss';
|
||||
import Image from 'next/future/image';
|
||||
import Link from 'next/link';
|
||||
import { MoreLikeThis } from '../../interfaces/shared/title';
|
||||
import { formatNumber, modifyIMDbImg } from '../../utils/helpers';
|
||||
import styles from '../../styles/modules/components/title/more-like-this.module.scss';
|
||||
|
||||
type Props = {
|
||||
className: string;
|
||||
|
@ -8,22 +10,52 @@ type Props = {
|
|||
};
|
||||
|
||||
const MoreLikeThis = ({ className, data }: Props) => {
|
||||
if (!data.length) return null;
|
||||
if (!data.length) return <></>;
|
||||
|
||||
return (
|
||||
<section className={`${className} ${styles.morelikethis}`}>
|
||||
<h2 className='heading heading__secondary'>More like this</h2>
|
||||
<ul className={styles.container}>
|
||||
{data.map(title => (
|
||||
<CardTitle
|
||||
key={title.id}
|
||||
link={`/title/${title.id}`}
|
||||
name={title.title}
|
||||
titleType={title.type.text}
|
||||
image={title.poster?.url}
|
||||
year={title.releaseYear}
|
||||
ratings={title.ratings}
|
||||
/>
|
||||
<li key={title.id}>
|
||||
<Link href={`/title/${title.id}`}>
|
||||
<a className={styles.item}>
|
||||
<div className={styles.item__imgContainer}>
|
||||
{title.poster ? (
|
||||
<Image
|
||||
src={modifyIMDbImg(title.poster.url, 400)}
|
||||
alt=''
|
||||
fill
|
||||
className={styles.item__img}
|
||||
sizes='200px'
|
||||
/>
|
||||
) : (
|
||||
<svg className={styles.item__imgNA}>
|
||||
<use href='/svg/sprite.svg#icon-image-slash' />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.item__textContainer}>
|
||||
<h3 className={`heading ${styles.item__heading}`}>
|
||||
{title.title}
|
||||
</h3>
|
||||
{title.ratings.avg && (
|
||||
<p className={styles.item__rating}>
|
||||
<span className={styles.item__ratingNum}>
|
||||
{title.ratings.avg}
|
||||
</span>
|
||||
<svg className={styles.item__ratingIcon}>
|
||||
<use href='/svg/sprite.svg#icon-rating'></use>
|
||||
</svg>
|
||||
<span>
|
||||
({formatNumber(title.ratings.numVotes)} votes)
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import { useRouter } from 'next/router';
|
||||
import { NextRouter } from 'next/router';
|
||||
import Link from 'next/link';
|
||||
import { Reviews } from 'src/interfaces/shared/title';
|
||||
import { formatNumber } from 'src/utils/helpers';
|
||||
import styles from 'src/styles/modules/components/title/reviews.module.scss';
|
||||
import { Reviews } from '../../interfaces/shared/title';
|
||||
import { formatNumber } from '../../utils/helpers';
|
||||
import styles from '../../styles/modules/components/title/reviews.module.scss';
|
||||
|
||||
type Props = {
|
||||
reviews: Reviews;
|
||||
router: NextRouter;
|
||||
};
|
||||
|
||||
const Reviews = ({ reviews }: Props) => {
|
||||
const router = useRouter();
|
||||
const Reviews = ({ reviews, router }: Props) => {
|
||||
const { titleId } = router.query;
|
||||
|
||||
return (
|
||||
<section className={styles.reviews}>
|
||||
<h2 className="heading heading__secondary">Reviews</h2>
|
||||
<h2 className='heading heading__secondary'>Reviews</h2>
|
||||
|
||||
{reviews.featuredReview && (
|
||||
<article className={styles.reviews__reviewContainer}>
|
||||
|
@ -38,7 +38,7 @@ const Reviews = ({ reviews }: Props) => {
|
|||
{' '}
|
||||
by{' '}
|
||||
<Link href={`/user/${reviews.featuredReview.reviewer.id}`}>
|
||||
<a className="link">{reviews.featuredReview.reviewer.name}</a>
|
||||
<a className='link'>{reviews.featuredReview.reviewer.name}</a>
|
||||
</Link>
|
||||
</span>
|
||||
<span> on {reviews.featuredReview.date}.</span>
|
||||
|
@ -58,21 +58,21 @@ const Reviews = ({ reviews }: Props) => {
|
|||
<div className={styles.reviews__stats}>
|
||||
<p>
|
||||
<Link href={`/title/${titleId}/reviews`}>
|
||||
<a className="link">
|
||||
<a className='link'>
|
||||
{formatNumber(reviews.numUserReviews)} User reviews
|
||||
</a>
|
||||
</Link>
|
||||
</p>
|
||||
<p>
|
||||
<Link href={`/title/${titleId}/externalreviews`}>
|
||||
<a className="link">
|
||||
<a className='link'>
|
||||
{formatNumber(reviews.numCriticReviews)} Critic reviews
|
||||
</a>
|
||||
</Link>
|
||||
</p>
|
||||
<p>
|
||||
<Link href={`/title/${titleId}/criticreviews`}>
|
||||
<a className="link"> {reviews.metacriticScore} Metascore</a>
|
||||
<a className='link'> {reviews.metacriticScore} Metascore</a>
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
export { default as Basic } from './Basic';
|
||||
export { default as Cast } from './Cast';
|
||||
export { default as DidYouKnow } from './DidYouKnow';
|
||||
export { default as Info } from './Info';
|
||||
export { default as MoreLikeThis } from './MoreLikeThis';
|
||||
export { default as Reviews } from './Reviews';
|
|
@ -1,13 +1,10 @@
|
|||
import React, { useState, createContext, ReactNode } from 'react';
|
||||
import { isLocalStorageAvailable } from 'src/utils/helpers';
|
||||
|
||||
const getInitialTheme = () => {
|
||||
// for server-side rendering, as window isn't availabe there
|
||||
if (typeof window === 'undefined') return 'light';
|
||||
|
||||
const userPrefersTheme = (
|
||||
isLocalStorageAvailable() ? window.localStorage.getItem('theme') : null
|
||||
) as 'light' | 'dark' | null;
|
||||
const userPrefersTheme = window.localStorage.getItem('theme') || null;
|
||||
const browserPrefersDarkTheme = window.matchMedia(
|
||||
'(prefers-color-scheme: dark)'
|
||||
).matches;
|
||||
|
@ -28,7 +25,7 @@ const updateMetaTheme = () => {
|
|||
|
||||
const initialContext = {
|
||||
theme: '',
|
||||
setTheme: (theme: ReturnType<typeof getInitialTheme>) => { },
|
||||
setTheme: (theme: string) => {},
|
||||
};
|
||||
|
||||
export const themeContext = createContext(initialContext);
|
||||
|
@ -36,9 +33,9 @@ export const themeContext = createContext(initialContext);
|
|||
const ThemeProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [curTheme, setCurTheme] = useState(getInitialTheme);
|
||||
|
||||
const setTheme = (theme: typeof curTheme) => {
|
||||
const setTheme = (theme: string) => {
|
||||
setCurTheme(theme);
|
||||
if (isLocalStorageAvailable()) window.localStorage.setItem('theme', theme);
|
||||
window.localStorage.setItem('theme', theme);
|
||||
document.documentElement.dataset.theme = theme;
|
||||
updateMetaTheme();
|
||||
};
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
/**
|
||||
* for showing progress bar. could've used nprogress package, but didn't feel like it
|
||||
* @returns isPageLoading: as the name suggests.
|
||||
* @returns key: a unique key(in reality, a part of url) telling whether the page has changed or not
|
||||
*/
|
||||
const useIsPageLoading = () => {
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleStart = useCallback(() => setIsLoading(true), []);
|
||||
const handleEnd = useCallback(() => setIsLoading(false), []);
|
||||
|
||||
useEffect(() => {
|
||||
router.events.on('routeChangeStart', handleStart);
|
||||
router.events.on('routeChangeComplete', handleEnd);
|
||||
router.events.on('routeChangeError', handleEnd);
|
||||
|
||||
return () => {
|
||||
router.events.off('routeChangeStart', handleStart);
|
||||
router.events.off('routeChangeComplete', handleEnd);
|
||||
router.events.off('routeChangeError', handleEnd);
|
||||
};
|
||||
}, [router, handleStart, handleEnd]);
|
||||
|
||||
return { isPageLoading: isLoading, key: router.asPath };
|
||||
};
|
||||
|
||||
export default useIsPageLoading;
|
|
@ -1,86 +0,0 @@
|
|||
import {
|
||||
ResultMetaTitleTypes,
|
||||
ResultMetaTypes,
|
||||
} from 'src/interfaces/shared/search';
|
||||
|
||||
export default interface RawFind {
|
||||
props: {
|
||||
pageProps: {
|
||||
findPageMeta: {
|
||||
searchTerm: string;
|
||||
includeAdult: false;
|
||||
isExactMatch: boolean;
|
||||
searchType?: ResultMetaTypes;
|
||||
titleSearchType?: ResultMetaTitleTypes[];
|
||||
};
|
||||
nameResults: {
|
||||
results: Array<{
|
||||
id: string;
|
||||
displayNameText: string;
|
||||
knownForJobCategory: string | 0;
|
||||
knownForTitleText: string | 0;
|
||||
knownForTitleYear: string | 0;
|
||||
avatarImageModel?: {
|
||||
url: string;
|
||||
// maxHeight: number;
|
||||
// maxWidth: number;
|
||||
caption: string;
|
||||
};
|
||||
akaName?: string;
|
||||
}>;
|
||||
// nextCursor?: string;
|
||||
// hasExactMatches?: boolean;
|
||||
};
|
||||
titleResults: {
|
||||
results: Array<{
|
||||
id: string;
|
||||
titleNameText: string;
|
||||
titleReleaseText?: string;
|
||||
titleTypeText: string;
|
||||
titlePosterImageModel?: {
|
||||
url: string;
|
||||
// maxHeight: number;
|
||||
// maxWidth: number;
|
||||
caption: string;
|
||||
};
|
||||
topCredits: Array<string>;
|
||||
imageType: string;
|
||||
seriesId?: string;
|
||||
seriesNameText?: string;
|
||||
seriesReleaseText?: string;
|
||||
seriesTypeText?: string;
|
||||
seriesSeasonText?: string;
|
||||
seriesEpisodeText?: string;
|
||||
}>;
|
||||
// nextCursor?: string;
|
||||
// hasExactMatches?: boolean;
|
||||
};
|
||||
companyResults: {
|
||||
results: Array<{
|
||||
id: string;
|
||||
companyName: string;
|
||||
countryText: string;
|
||||
typeText: string | 0;
|
||||
}>;
|
||||
// nextCursor?: string;
|
||||
// hasExactMatches?: boolean;
|
||||
};
|
||||
keywordResults: {
|
||||
results: Array<{
|
||||
id: string;
|
||||
keywordText: string;
|
||||
numTitles: number;
|
||||
}>;
|
||||
// nextCursor?: string;
|
||||
// hasExactMatches?: boolean;
|
||||
};
|
||||
resultsSectionOrder: Array<string>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// const x: RawFind<'tt'> = {
|
||||
// props: {pageProps: {findPageMeta: {
|
||||
// titleSearchType: ['MOVIE']
|
||||
// }}}
|
||||
// }
|
File diff suppressed because it is too large
Load diff
|
@ -18,7 +18,7 @@ export default interface RawTitle {
|
|||
restrictionReason: Array<string>;
|
||||
unrestrictedTotal: number;
|
||||
};
|
||||
} | null;
|
||||
};
|
||||
canHaveEpisodes: boolean;
|
||||
series?: {
|
||||
episodeNumber: {
|
||||
|
@ -125,7 +125,7 @@ export default interface RawTitle {
|
|||
runtime: {
|
||||
value: number;
|
||||
};
|
||||
description?: {
|
||||
description: {
|
||||
value: string;
|
||||
language: string;
|
||||
};
|
||||
|
@ -516,11 +516,9 @@ export default interface RawTitle {
|
|||
canRate: {
|
||||
isRatable: boolean;
|
||||
};
|
||||
titleGenres: {
|
||||
titleCardGenres: {
|
||||
genres: Array<{
|
||||
genre: {
|
||||
text: string;
|
||||
};
|
||||
text: string;
|
||||
}>;
|
||||
};
|
||||
canHaveEpisodes: boolean;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { AppError as AppErrorClass } from 'src/utils/helpers';
|
||||
|
||||
export type AppError = Omit<InstanceType<typeof AppErrorClass>, 'name'>;
|
||||
export type AppError = {
|
||||
message: string;
|
||||
statusCode: number;
|
||||
stack?: any;
|
||||
};
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
import type Name from './name';
|
||||
|
||||
export type Media = Name['media']; // exactly the same in title and name
|
||||
|
||||
// forcefully makes array of individual elements of T, where t is any conditional type.
|
||||
export type ToArray<T> = T extends any ? T[] : never;
|
|
@ -1,39 +0,0 @@
|
|||
import list from 'src/utils/fetchers/list';
|
||||
|
||||
// for full title
|
||||
type List = Awaited<ReturnType<typeof list>>;
|
||||
export type { List as default };
|
||||
|
||||
type DataTitle = {
|
||||
image: string | null;
|
||||
name: string;
|
||||
url: string | null;
|
||||
year: string;
|
||||
certificate: string;
|
||||
runtime: string;
|
||||
genre: string;
|
||||
plot: string;
|
||||
rating: string;
|
||||
metascore: string;
|
||||
otherInfo: string[][];
|
||||
};
|
||||
|
||||
type DataName = {
|
||||
image: string | null;
|
||||
name: string;
|
||||
url: string | null;
|
||||
job: string | null;
|
||||
knownFor: string | null;
|
||||
knownForLink: string | null;
|
||||
about: string;
|
||||
};
|
||||
|
||||
type DataImage = string;
|
||||
|
||||
export type DataKind = 'images' | 'titles' | 'names';
|
||||
|
||||
export type Data<T extends DataKind> = T extends 'images'
|
||||
? DataImage
|
||||
: T extends 'names'
|
||||
? DataName
|
||||
: DataTitle;
|
|
@ -1,16 +0,0 @@
|
|||
import cleanName from 'src/utils/cleaners/name';
|
||||
|
||||
type Name = ReturnType<typeof cleanName>;
|
||||
export type { Name as default };
|
||||
|
||||
export type Basic = Name['basic'];
|
||||
|
||||
export type Media = Name['media'];
|
||||
|
||||
export type Credits = Name['credits'];
|
||||
|
||||
export type DidYouKnow = Name['didYouKnow'];
|
||||
|
||||
export type PersonalDetails = Name['personalDetails'];
|
||||
|
||||
export type KnownFor = Name['knownFor'];
|
|
@ -1,28 +0,0 @@
|
|||
import cleanFind from 'src/utils/cleaners/find';
|
||||
import { resultTitleTypes, resultTypes } from 'src/utils/constants/find';
|
||||
|
||||
type BasicSearch = ReturnType<typeof cleanFind>;
|
||||
export type { BasicSearch as default };
|
||||
|
||||
export type Titles = BasicSearch['titles'];
|
||||
export type People = BasicSearch['people'];
|
||||
export type Companies = BasicSearch['companies'];
|
||||
export type Keywords = BasicSearch['keywords'];
|
||||
|
||||
// q=babylon&s=tt&ttype=ft&exact=true
|
||||
export type FindQueryParams = {
|
||||
q: string;
|
||||
exact?: 'true';
|
||||
s?: QueryTypes;
|
||||
ttype?: QueryTitleTypes;
|
||||
};
|
||||
|
||||
export type ResultMetaTypes = typeof resultTypes.types[number]['id'] | null;
|
||||
|
||||
export type ResultMetaTitleTypes =
|
||||
| typeof resultTitleTypes.types[number]['id']
|
||||
| null;
|
||||
|
||||
export type QueryTypes = typeof resultTypes.types[number]['val'];
|
||||
|
||||
export type QueryTitleTypes = typeof resultTitleTypes.types[number]['val'];
|
|
@ -1,5 +1,7 @@
|
|||
import cleanTitle from 'src/utils/cleaners/title';
|
||||
import title from 'src/utils/fetchers/title';
|
||||
import cleanTitle from '../../utils/cleaners/title';
|
||||
import title from '../../utils/fetchers/title';
|
||||
|
||||
export type AxiosTitleRes = Awaited<ReturnType<typeof title>>;
|
||||
|
||||
// for full title
|
||||
type Title = ReturnType<typeof cleanTitle>;
|
||||
|
|
52
src/layouts/Footer.tsx
Normal file
52
src/layouts/Footer.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { FC } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import styles from '../styles/modules/layout/footer.module.scss';
|
||||
|
||||
const Footer: FC = () => {
|
||||
const { pathname } = useRouter();
|
||||
const className = (link: string) =>
|
||||
pathname === link ? styles.nav__linkActive : styles.nav__link;
|
||||
|
||||
return (
|
||||
<footer id='footer' className={styles.footer}>
|
||||
<nav aria-label='primary navigation' className={styles.nav}>
|
||||
<ul className={styles.list}>
|
||||
<li className={styles.nav__item}>
|
||||
<Link href='/about'>
|
||||
<a className={className('/about')}>About</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.nav__item}>
|
||||
<Link href='/privacy'>
|
||||
<a className={className('/privacy')}>Privacy</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.nav__item}>
|
||||
<Link href='/contact'>
|
||||
<a className={className('/contact')}>Contact</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.nav__item}>
|
||||
<a href='#' className={styles.nav__link}>
|
||||
Back to top
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<p className={styles.licence}>
|
||||
Licensed under
|
||||
<a
|
||||
className={styles.nav__link}
|
||||
href='https://www.gnu.org/licenses/agpl-3.0-standalone.html'
|
||||
>
|
||||
GNU AGPLv3
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
|
@ -1,22 +1,37 @@
|
|||
import { ReactNode } from 'react';
|
||||
// import dynamic from 'next/dynamic';
|
||||
import Link from 'next/link';
|
||||
import ThemeToggler from 'src/components/buttons/ThemeToggler';
|
||||
import styles from 'src/styles/modules/layout/header.module.scss';
|
||||
import styles from '../styles/modules/layout/header.module.scss';
|
||||
import ThemeToggler from '../components/buttons/ThemeToggler';
|
||||
|
||||
type Props = { full?: boolean; originalPath?: string };
|
||||
// const ThemeToggler = dynamic(
|
||||
// () => import('../components/buttons/ThemeToggler'),
|
||||
// { ssr: false }
|
||||
// );
|
||||
|
||||
const Header = ({ full, originalPath }: Props) => {
|
||||
type Props = { full?: boolean; children?: ReactNode };
|
||||
|
||||
const Header = (props: Props) => {
|
||||
return (
|
||||
<header id='header' className={`${styles.header} ${full ? styles.header__about : ''}`}>
|
||||
<header
|
||||
id='header'
|
||||
className={`${styles.header} ${props.full ? styles.header__about : ''}`}
|
||||
>
|
||||
<div className={styles.topbar}>
|
||||
<Link href='/find'>
|
||||
<Link href='/about'>
|
||||
<a aria-label='go to homepage' className={styles.logo}>
|
||||
<svg className={styles.logo__icon} role='img' aria-hidden>
|
||||
<svg
|
||||
className={styles.logo__icon}
|
||||
focusable='false'
|
||||
role='img'
|
||||
aria-hidden='true'
|
||||
>
|
||||
<use href='/svg/sprite.svg#icon-logo'></use>
|
||||
</svg>
|
||||
<span className={styles.logo__text}>libremdb</span>
|
||||
</a>
|
||||
</Link>
|
||||
{full && (
|
||||
{props.full && (
|
||||
<nav className={styles.nav}>
|
||||
<ul className={styles.nav__list}>
|
||||
<li className={styles.nav__item}>
|
||||
|
@ -37,40 +52,27 @@ const Header = ({ full, originalPath }: Props) => {
|
|||
</ul>
|
||||
</nav>
|
||||
)}
|
||||
<div className={styles.misc}>
|
||||
<a href={`https://www.imdb.com${originalPath ?? ''}`} target='_blank' rel='noreferrer'>
|
||||
<span className='visually-hidden'>View on IMDb (opens in new tab)</span>
|
||||
<svg className='icon' role='img' aria-hidden>
|
||||
<use href='/svg/sprite.svg#icon-external-link'></use>
|
||||
</svg>
|
||||
</a>
|
||||
<Link href='/find'>
|
||||
<a>
|
||||
<span className='visually-hidden'>Search</span>
|
||||
<svg className='icon' role='img' aria-hidden>
|
||||
<use href='/svg/sprite.svg#icon-search'></use>
|
||||
</svg>
|
||||
</a>
|
||||
</Link>
|
||||
<ThemeToggler className={styles.themeToggler} />
|
||||
</div>
|
||||
<ThemeToggler className={styles.themeToggler} />
|
||||
</div>
|
||||
{full && (
|
||||
{props.full && (
|
||||
<div className={styles.hero}>
|
||||
<h1 className={`heading heading__primary ${styles.hero__text}`}>
|
||||
A free & open source IMDb front-end
|
||||
</h1>
|
||||
<p className={styles.hero__more}>
|
||||
inspired by projects like{' '}
|
||||
inspired by projects like
|
||||
<a href='https://codeberg.org/teddit/teddit' className='link'>
|
||||
teddit
|
||||
</a>
|
||||
,{' '}
|
||||
,
|
||||
<a href='https://github.com/zedeus/nitter' className='link'>
|
||||
nitter
|
||||
</a>
|
||||
, and{' '}
|
||||
<a href='https://github.com/digitalblossom/alternative-frontends' className='link'>
|
||||
, and
|
||||
<a
|
||||
href='https://github.com/digitalblossom/alternative-frontends'
|
||||
className='link'
|
||||
>
|
||||
many others
|
||||
</a>
|
||||
.
|
|
@ -1,18 +1,17 @@
|
|||
import { ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
import Footer from './Footer';
|
||||
import Header from './Header';
|
||||
|
||||
type Props = {
|
||||
full?: true;
|
||||
children: ReactNode;
|
||||
full?: boolean;
|
||||
children: React.ReactNode;
|
||||
className: string;
|
||||
originalPath?: string;
|
||||
};
|
||||
|
||||
const Layout = ({ full, children, className, originalPath }: Props) => {
|
||||
const Layout = ({ full, children, className }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<Header full={full} originalPath={originalPath} />
|
||||
<Header full={full} />
|
||||
<main id='main' className={`main ${className}`}>
|
||||
{children}
|
||||
</main>
|
|
@ -1,7 +1,7 @@
|
|||
import ErrorInfo from 'src/components/error/ErrorInfo';
|
||||
import ErrorInfo from '../components/Error/ErrorInfo';
|
||||
|
||||
const Error404 = () => {
|
||||
return <ErrorInfo message='Not found, sorry.' statusCode={404} />;
|
||||
return <ErrorInfo />;
|
||||
};
|
||||
|
||||
export default Error404;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import ErrorInfo from 'src/components/error/ErrorInfo';
|
||||
import ErrorInfo from '../components/Error/ErrorInfo';
|
||||
|
||||
const Error500 = () => {
|
||||
return <ErrorInfo message='Server messed up, sorry.' statusCode={500} />;
|
||||
return <ErrorInfo message='server messed up, sorry.' statusCode={500} />;
|
||||
};
|
||||
export default Error500;
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
import type { GetServerSideProps, InferGetServerSidePropsType } from 'next';
|
||||
import ErrorInfo from 'src/components/error/ErrorInfo';
|
||||
|
||||
const error = {
|
||||
statusCode: 404,
|
||||
message: 'Not found, sorry.',
|
||||
} as const;
|
||||
|
||||
type Props = InferGetServerSidePropsType<typeof getServerSideProps>;
|
||||
|
||||
const Error404 = ({ originalPath }: Props) => {
|
||||
return <ErrorInfo {...error} originalPath={originalPath} />;
|
||||
};
|
||||
|
||||
export default Error404;
|
||||
|
||||
type Data = { originalPath: string };
|
||||
type Params = { error: string[] };
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<Data, Params> = async ctx => {
|
||||
ctx.res.statusCode = error.statusCode;
|
||||
ctx.res.statusMessage = error.message;
|
||||
|
||||
return { props: { originalPath: ctx.resolvedUrl } };
|
||||
};
|
|
@ -1,22 +1,38 @@
|
|||
import { AppProps } from 'next/app';
|
||||
import ProgressBar from 'src/components/loaders/ProgressBar';
|
||||
import ErrorBoundary from 'src/components/error/ErrorBoundary';
|
||||
import ThemeProvider from 'src/context/theme-context';
|
||||
import usePageLoading from 'src/hooks/usePageLoading';
|
||||
import 'src/styles/main.scss';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { AppProps } from 'next/app';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import ProgressBar from '../components/loaders/ProgressBar';
|
||||
import ThemeProvider from '../context/theme-context';
|
||||
|
||||
import '../styles/main.scss';
|
||||
|
||||
const ModifiedApp = ({ Component, pageProps }: AppProps) => {
|
||||
const { isPageLoading, key } = usePageLoading();
|
||||
// for showing progress bar
|
||||
// could've used nprogress package, but didn't feel like it
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleStart = useCallback(() => setIsLoading(true), []);
|
||||
const handleEnd = useCallback(() => setIsLoading(false), []);
|
||||
|
||||
useEffect(() => {
|
||||
router.events.on('routeChangeStart', handleStart);
|
||||
router.events.on('routeChangeComplete', handleEnd);
|
||||
router.events.on('routeChangeError', handleEnd);
|
||||
|
||||
return () => {
|
||||
router.events.off('routeChangeStart', handleStart);
|
||||
router.events.off('routeChangeComplete', handleEnd);
|
||||
router.events.off('routeChangeError', handleEnd);
|
||||
};
|
||||
}, [router, handleStart, handleEnd]);
|
||||
//
|
||||
|
||||
return (
|
||||
<ThemeProvider>
|
||||
{isPageLoading && <ProgressBar />}
|
||||
<ErrorBoundary>
|
||||
<Component
|
||||
{...pageProps}
|
||||
key={key} /* passing key to force react to remount components */
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
{isLoading && <ProgressBar />}
|
||||
<Component {...pageProps} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,25 +3,15 @@ import Document, { Html, Head, Main, NextScript } from 'next/document';
|
|||
// for preventing Flash of inAccurate coloR Theme(fart)
|
||||
// chris coyier came up with that acronym(https://css-tricks.com/flash-of-inaccurate-color-theme-fart/)
|
||||
const setInitialTheme = `
|
||||
(() => {
|
||||
document.documentElement.dataset.js = true;
|
||||
const isLocalStorageAvailable = () => {
|
||||
try {
|
||||
window.localStorage.getItem('test');
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let theme = 'light';
|
||||
let themeColor = '#ffe5ef';
|
||||
const userPrefersTheme = isLocalStorageAvailable() ? window.localStorage.getItem('theme') : null;
|
||||
const browserPrefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (userPrefersTheme) theme = userPrefersTheme;
|
||||
else if (browserPrefersDarkTheme) theme = 'dark';
|
||||
if(theme === 'dark') themeColor = '#141c2e';
|
||||
document.documentElement.dataset.theme = theme;
|
||||
document.querySelector('meta[name="theme-color"]').setAttribute('content', themeColor);
|
||||
document.documentElement.dataset.js = true;
|
||||
document.documentElement.dataset.theme = (() => {
|
||||
const userPrefersTheme = window.localStorage.getItem('theme') || null;
|
||||
const browserPrefersDarkTheme = window.matchMedia(
|
||||
'(prefers-color-scheme: dark)'
|
||||
).matches;
|
||||
if (userPrefersTheme) return userPrefersTheme;
|
||||
else if (browserPrefersDarkTheme) return 'dark';
|
||||
else return 'light';
|
||||
})();
|
||||
`;
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
/* eslint-disable react/no-unescaped-entities */
|
||||
import Link from 'next/link';
|
||||
import Meta from 'src/components/meta/Meta';
|
||||
import Layout from 'src/components/layout';
|
||||
import styles from 'src/styles/modules/pages/about/about.module.scss';
|
||||
import Meta from '../../components/Meta/Meta';
|
||||
import Layout from '../../layouts/Layout';
|
||||
|
||||
import styles from '../../styles/modules/pages/about/about.module.scss';
|
||||
|
||||
const About = () => {
|
||||
return (
|
||||
|
@ -89,20 +91,16 @@ const About = () => {
|
|||
<p className={styles.faq__description}>
|
||||
Replace `imdb.com` in any IMDb URL with any of the instances.
|
||||
For example: `
|
||||
<a
|
||||
href='https://imdb.com/title/tt1049413'
|
||||
className='link'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
<a href='https://imdb.com/title/tt1049413' className='link'>
|
||||
imdb.com/title/tt1049413
|
||||
</a>
|
||||
` to `
|
||||
<Link href='/title/tt1049413'>
|
||||
<a className='link'>
|
||||
{process.env.NEXT_PUBLIC_URL || ''}/title/tt1049413
|
||||
</a>
|
||||
</Link>
|
||||
<a
|
||||
href='https://libremdb.iket.me/title/tt1049413'
|
||||
className='link'
|
||||
>
|
||||
libremdb.iket.me/title/tt1049413
|
||||
</a>
|
||||
` . To avoid changing the URLs manually, you can use extensions
|
||||
like{' '}
|
||||
<a
|
||||
|
@ -135,21 +133,22 @@ const About = () => {
|
|||
</details>
|
||||
<details className={styles.faq}>
|
||||
<summary className={styles.faq__summary}>
|
||||
Is content served from third-parties, like Amazon?
|
||||
I see connection being made to some Amazon domains.
|
||||
</summary>
|
||||
<p className={styles.faq__description}>
|
||||
Nope, libremdb proxies all image and video requests through the
|
||||
instance to avoid exposing your IP address, browser information
|
||||
and other personally identifiable metadata (
|
||||
<a
|
||||
href='https://github.com/httpjamesm'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='link'
|
||||
>
|
||||
Contributor
|
||||
</a>
|
||||
).
|
||||
For now, images and videos are directly served from Amazon. If I
|
||||
have enough time in the future, I'll implement a way to serve
|
||||
the images from libremdb instead.
|
||||
</p>
|
||||
</details>
|
||||
<details className={styles.faq}>
|
||||
<summary className={styles.faq__summary}>
|
||||
Will Amazon track me then?
|
||||
</summary>
|
||||
<p className={styles.faq__description}>
|
||||
They may log your IP address, useragent, and other such
|
||||
identifiers. I'd recommend using a VPN, or accessing the website
|
||||
through TOR for mitigating this risk.
|
||||
</p>
|
||||
</details>
|
||||
<details className={styles.faq}>
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
type ResponseData = { status: false; message: string };
|
||||
|
||||
export default async function handler(_: NextApiRequest, res: NextApiResponse<ResponseData>) {
|
||||
res.status(400).json({ status: false, message: 'Not found' });
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import Find, { type FindQueryParams } from 'src/interfaces/shared/search';
|
||||
import basicSearch from 'src/utils/fetchers/basicSearch';
|
||||
import getOrSetApiCache from 'src/utils/getOrSetApiCache';
|
||||
import { findKey } from 'src/utils/constants/keys';
|
||||
import { AppError, cleanQueryStr } from 'src/utils/helpers';
|
||||
|
||||
type ResponseData =
|
||||
| { status: true; data: { title: null | string; results: null | Find } }
|
||||
| { status: false; message: string };
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<ResponseData>) {
|
||||
try {
|
||||
if (req.method !== 'GET') throw new AppError('Invalid method', 400);
|
||||
|
||||
const queryObj = req.query as FindQueryParams | Record<string, never>;
|
||||
const query = queryObj.q?.trim();
|
||||
|
||||
if (!query) {
|
||||
return res.status(200).json({ status: true, data: { title: null, results: null } });
|
||||
}
|
||||
|
||||
const entries = Object.entries(queryObj);
|
||||
const queryStr = cleanQueryStr(entries);
|
||||
const results = await getOrSetApiCache(findKey(queryStr), basicSearch, queryStr);
|
||||
|
||||
res.status(200).json({ status: true, data: { title: query, results } });
|
||||
} catch (error: any) {
|
||||
const { message = 'Not found', statusCode = 404 } = error;
|
||||
res.status(statusCode).json({ status: false, message });
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import type List from 'src/interfaces/shared/list';
|
||||
import list from 'src/utils/fetchers/list';
|
||||
import getOrSetApiCache from 'src/utils/getOrSetApiCache';
|
||||
import { listKey } from 'src/utils/constants/keys';
|
||||
import { AppError } from 'src/utils/helpers';
|
||||
|
||||
type ResponseData = { status: true; data: List } | { status: false; message: string };
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<ResponseData>) {
|
||||
try {
|
||||
if (req.method !== 'GET') throw new AppError('Invalid method', 400);
|
||||
|
||||
const listId = req.query.listId as string;
|
||||
const pageNum = req.query.page as string | undefined;
|
||||
|
||||
const data = await getOrSetApiCache(listKey(listId, pageNum), list, listId, pageNum);
|
||||
res.status(200).json({ status: true, data });
|
||||
} catch (error: any) {
|
||||
const { message = 'Not found', statusCode = 404 } = error;
|
||||
res.status(statusCode).json({ status: false, message });
|
||||
}
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { AxiosRequestHeaders } from 'axios';
|
||||
import redis from 'src/utils/redis';
|
||||
import axiosInstance from 'src/utils/axiosInstance';
|
||||
import { mediaKey } from 'src/utils/constants/keys';
|
||||
|
||||
const dontCacheMedia =
|
||||
process.env.USE_REDIS_FOR_API_ONLY === 'true' || process.env.USE_REDIS !== 'true';
|
||||
|
||||
const ttl = process.env.REDIS_CACHE_TTL_MEDIA ?? 30 * 60;
|
||||
|
||||
const getCleanReqHeaders = (headers: NextApiRequest['headers']) => {
|
||||
const cleanHeaders: AxiosRequestHeaders = {};
|
||||
|
||||
if (headers.accept) cleanHeaders.accept = headers.accept;
|
||||
if (headers.range) cleanHeaders.range = headers.range;
|
||||
if (headers['accept-encoding'])
|
||||
cleanHeaders['accept-encoding'] = headers['accept-encoding'].toString();
|
||||
|
||||
return cleanHeaders;
|
||||
};
|
||||
|
||||
const resHeadersArr = ['content-range', 'content-length', 'content-type', 'accept-ranges'];
|
||||
|
||||
// checks if a url is pointing towards a video/image from imdb
|
||||
const regex =
|
||||
/^https:\/\/((m\.)?media-amazon\.com|imdb-video\.media-imdb\.com).*\.(jpg|jpeg|png|mp4|gif|webp).*$/;
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const mediaUrl = req.query.url as string | undefined;
|
||||
const requestHeaders = getCleanReqHeaders(req.headers);
|
||||
|
||||
// 1. returning if query is illegal
|
||||
if (!mediaUrl || !regex.test(mediaUrl))
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invalid query',
|
||||
});
|
||||
|
||||
// 2. sending streamed response if redis, or redis for media isn't enabled
|
||||
if (dontCacheMedia) {
|
||||
const mediaRes = await axiosInstance.get(mediaUrl, {
|
||||
responseType: 'stream',
|
||||
headers: requestHeaders,
|
||||
});
|
||||
|
||||
// chromium browsers want a 206 response with specific headers. so, we gotta pass them on.
|
||||
res.statusCode = mediaRes.status;
|
||||
resHeadersArr.forEach(key => {
|
||||
const val = mediaRes.headers[key];
|
||||
if (val) res.setHeader(key, val);
|
||||
});
|
||||
mediaRes.data.pipe(res);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. else if resourced is cached, sending it
|
||||
const cachedMedia = await redis.getBuffer(mediaKey(mediaUrl));
|
||||
|
||||
if (cachedMedia) {
|
||||
res.setHeader('x-cached', 'true');
|
||||
res.send(cachedMedia);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. else getting, caching and sending response
|
||||
const { data } = await axiosInstance(mediaUrl, {
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
|
||||
// saving in redis for 30 minutes
|
||||
await redis.setex(mediaKey(mediaUrl), ttl, Buffer.from(data));
|
||||
|
||||
// sending media
|
||||
res.setHeader('x-cached', 'false');
|
||||
res.send(data);
|
||||
|
||||
// sending token response on any error
|
||||
} catch {
|
||||
res.status(404);
|
||||
res.json({
|
||||
success: false,
|
||||
message: 'something went wrong',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
responseLimit: false,
|
||||
},
|
||||
};
|
|
@ -1,22 +0,0 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import type Name from 'src/interfaces/shared/name';
|
||||
import name from 'src/utils/fetchers/name';
|
||||
import getOrSetApiCache from 'src/utils/getOrSetApiCache';
|
||||
import { nameKey } from 'src/utils/constants/keys';
|
||||
import { AppError } from 'src/utils/helpers';
|
||||
|
||||
type ResponseData = { status: true; data: Name } | { status: false; message: string };
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<ResponseData>) {
|
||||
try {
|
||||
if (req.method !== 'GET') throw new AppError('Invalid method', 400);
|
||||
|
||||
const nameId = req.query.nameId as string;
|
||||
|
||||
const data = await getOrSetApiCache(nameKey(nameId), name, nameId);
|
||||
res.status(200).json({ status: true, data });
|
||||
} catch (error: any) {
|
||||
const { message = 'Not found', statusCode = 404 } = error;
|
||||
res.status(statusCode).json({ status: false, message });
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import type Title from 'src/interfaces/shared/title';
|
||||
import title from 'src/utils/fetchers/title';
|
||||
import getOrSetApiCache from 'src/utils/getOrSetApiCache';
|
||||
import { titleKey } from 'src/utils/constants/keys';
|
||||
import { AppError } from 'src/utils/helpers';
|
||||
|
||||
type ResponseData = { status: true; data: Title } | { status: false; message: string };
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<ResponseData>) {
|
||||
try {
|
||||
if (req.method !== 'GET') throw new AppError('Invalid method', 400);
|
||||
|
||||
const titleId = req.query.titleId as string;
|
||||
const data = await getOrSetApiCache(titleKey(titleId), title, titleId);
|
||||
res.status(200).json({ status: true, data });
|
||||
} catch (error: any) {
|
||||
const { message = 'Not found', statusCode = 404 } = error;
|
||||
res.status(statusCode).json({ status: false, message });
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import Meta from 'src/components/meta/Meta';
|
||||
import Layout from 'src/components/layout';
|
||||
import styles from 'src/styles/modules/pages/contact/contact.module.scss';
|
||||
import Meta from '../../components/Meta/Meta';
|
||||
import Layout from '../../layouts/Layout';
|
||||
|
||||
import styles from '../../styles/modules/pages/contact/contact.module.scss';
|
||||
|
||||
const Contact = () => {
|
||||
return (
|
||||
|
@ -14,50 +15,30 @@ const Contact = () => {
|
|||
<h1 className={`heading heading__primary ${styles.contact__heading}`}>
|
||||
Contact
|
||||
</h1>
|
||||
|
||||
<div className={styles.list}>
|
||||
<div className={styles.item}>
|
||||
<p className={styles.item__text}>
|
||||
For any issues, questions, bugs, or requests regarding the
|
||||
service, you can go to{' '}
|
||||
<a href='https://github.com/zyachel/libremdb' className='link'>
|
||||
GitHub
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p className={styles.item__text}>
|
||||
Alternatively, you can visit{' '}
|
||||
<a
|
||||
href='https://codeberg.org/zyachel/libremdb'
|
||||
className='link'
|
||||
>
|
||||
the repository on Codeberg
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
{process.env.NEXT_PUBLIC_INSTANCE_MAIN_URL && (
|
||||
<div className={styles.item}>
|
||||
<p className={styles.item__text}>
|
||||
If you have some questions related to this instance,{' '}
|
||||
<a
|
||||
href={process.env.NEXT_PUBLIC_INSTANCE_MAIN_URL}
|
||||
className='link'
|
||||
>
|
||||
contact instance maintainer(s)
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.item}>
|
||||
<p className={styles.item__text}>
|
||||
In case you wish to contact me(the dev) personally,{' '}
|
||||
<a href='https://iket.me/contact/' className='link'>
|
||||
here you go
|
||||
</a>
|
||||
<span aria-label='smily text emoji'> :)</span>
|
||||
</p>
|
||||
</div>
|
||||
<p className={styles.item}>
|
||||
You can use{' '}
|
||||
<a href='https://github.com/zyachel/libremdb' className='link'>
|
||||
GitHub
|
||||
</a>{' '}
|
||||
or{' '}
|
||||
<a href='https://codeberg.org/zyachel/libremdb' className='link'>
|
||||
Codeberg
|
||||
</a>{' '}
|
||||
for general issues, questions, or requests.
|
||||
</p>
|
||||
<p className={styles.item}>
|
||||
In case you wish to contact me personally, I'm reachable via{' '}
|
||||
<a className='link' href='https://matrix.to/#/@ninal:matrix.org'>
|
||||
[matrix]
|
||||
</a>{' '}
|
||||
and{' '}
|
||||
<a className='link' href='mailto:aricla@protonmail.com'>
|
||||
email
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
|
||||
import Layout from 'src/components/layout';
|
||||
import ErrorInfo from 'src/components/error/ErrorInfo';
|
||||
import Meta from 'src/components/meta/Meta';
|
||||
import Results from 'src/components/find';
|
||||
import Form from 'src/components/forms/find';
|
||||
import Find, { FindQueryParams } from 'src/interfaces/shared/search';
|
||||
import { AppError } from 'src/interfaces/shared/error';
|
||||
import basicSearch from 'src/utils/fetchers/basicSearch';
|
||||
import getOrSetApiCache from 'src/utils/getOrSetApiCache';
|
||||
import { cleanQueryStr } from 'src/utils/helpers';
|
||||
import { findKey } from 'src/utils/constants/keys';
|
||||
import styles from 'src/styles/modules/pages/find/find.module.scss';
|
||||
|
||||
type Props = InferGetServerSidePropsType<typeof getServerSideProps>;
|
||||
|
||||
const getMetadata = (title: string | null) => ({
|
||||
title: title || 'Search',
|
||||
description: title
|
||||
? `results for '${title}'`
|
||||
: 'Search for anything on libremdb, a free & open source IMDb front-end',
|
||||
});
|
||||
|
||||
const BasicSearch = ({ data: { title, results }, error, originalPath }: Props) => {
|
||||
if (error) return <ErrorInfo {...error} originalPath={originalPath} />;
|
||||
|
||||
let layoutClassName = styles.find;
|
||||
if (!title) layoutClassName += ' ' + styles.find__home;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta {...getMetadata(title)} />
|
||||
<Layout className={layoutClassName} originalPath={originalPath}>
|
||||
{title && ( // only showing when user has searched for something
|
||||
<Results results={results} title={title} className={styles.results} />
|
||||
)}
|
||||
<Form className={styles.form} />
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: use generics for passing in queryParams(to components) for better type-checking.
|
||||
type Data = (
|
||||
| { data: { title: string; results: Find }; error: null }
|
||||
| { data: { title: null; results: null }; error: null }
|
||||
| { data: { title: string; results: null }; error: AppError }
|
||||
) & {
|
||||
originalPath: string;
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<Data, FindQueryParams> = async ctx => {
|
||||
// sample query str: find/?q=babylon&s=tt&ttype=ft&exact=true
|
||||
const queryObj = ctx.query as FindQueryParams;
|
||||
const query = queryObj.q?.trim();
|
||||
const originalPath = ctx.resolvedUrl;
|
||||
|
||||
if (!query) return { props: { data: { title: null, results: null }, error: null, originalPath } };
|
||||
|
||||
try {
|
||||
const entries = Object.entries(queryObj);
|
||||
const queryStr = cleanQueryStr(entries);
|
||||
|
||||
const res = await getOrSetApiCache(findKey(queryStr), basicSearch, queryStr);
|
||||
|
||||
return {
|
||||
props: { data: { title: query, results: res }, error: null, originalPath },
|
||||
};
|
||||
} catch (error: any) {
|
||||
const { message, statusCode } = error;
|
||||
ctx.res.statusCode = statusCode;
|
||||
ctx.res.statusMessage = message;
|
||||
|
||||
return {
|
||||
props: {
|
||||
error: { message, statusCode },
|
||||
data: { title: query, results: null },
|
||||
originalPath,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default BasicSearch;
|
|
@ -1,54 +0,0 @@
|
|||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
|
||||
import Meta from 'src/components/meta/Meta';
|
||||
import Layout from 'src/components/layout';
|
||||
import ErrorInfo from 'src/components/error/ErrorInfo';
|
||||
import { Data, Meta as ListMeta, Pagination } from 'src/components/list';
|
||||
import { AppError } from 'src/interfaces/shared/error';
|
||||
import TList from 'src/interfaces/shared/list';
|
||||
import getOrSetApiCache from 'src/utils/getOrSetApiCache';
|
||||
import list from 'src/utils/fetchers/list';
|
||||
import { listKey } from 'src/utils/constants/keys';
|
||||
import styles from 'src/styles/modules/pages/list/list.module.scss';
|
||||
|
||||
type Props = InferGetServerSidePropsType<typeof getServerSideProps>;
|
||||
|
||||
const List = ({ data, error, originalPath }: Props) => {
|
||||
if (error) return <ErrorInfo {...error} originalPath={originalPath} />;
|
||||
|
||||
const description = data.description || `List created by ${data.meta.by.name} (${data.meta.num} ${data.meta.type}).`
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title={data.title} description={description} />
|
||||
<Layout className={styles.list} originalPath={originalPath}>
|
||||
<ListMeta title={data.title} description={data.description} meta={data.meta} />
|
||||
{/* @ts-expect-error don't have time to fix it. just a type fluff. */}
|
||||
<Data data={data.data} />
|
||||
<Pagination pagination={data.pagination} />
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type TData = ({ data: TList; error: null } | { error: AppError; data: null }) & {
|
||||
originalPath: string;
|
||||
};
|
||||
type Params = { listId: string };
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<TData, Params> = async ctx => {
|
||||
const listId = ctx.params!.listId;
|
||||
const pageNum = (ctx.query.page as string | undefined) ?? '1';
|
||||
const originalPath = ctx.resolvedUrl;
|
||||
try {
|
||||
const data = await getOrSetApiCache(listKey(listId, pageNum), list, listId, pageNum);
|
||||
|
||||
return { props: { data, error: null, originalPath } };
|
||||
} catch (error: any) {
|
||||
const { message = 'Internal server error', statusCode = 500 } = error;
|
||||
ctx.res.statusCode = statusCode;
|
||||
ctx.res.statusMessage = message;
|
||||
return { props: { error: { message, statusCode }, data: null, originalPath } };
|
||||
}
|
||||
};
|
||||
|
||||
export default List;
|
|
@ -1,67 +0,0 @@
|
|||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
|
||||
import Meta from 'src/components/meta/Meta';
|
||||
import Layout from 'src/components/layout';
|
||||
import ErrorInfo from 'src/components/error/ErrorInfo';
|
||||
import Media from 'src/components/media/Media';
|
||||
import { Basic, Credits, DidYouKnow, Info, Bio, KnownFor } from 'src/components/name';
|
||||
import Name from 'src/interfaces/shared/name';
|
||||
import { AppError } from 'src/interfaces/shared/error';
|
||||
import name from 'src/utils/fetchers/name';
|
||||
import getOrSetApiCache from 'src/utils/getOrSetApiCache';
|
||||
import { getProxiedIMDbImgUrl } from 'src/utils/helpers';
|
||||
import { nameKey } from 'src/utils/constants/keys';
|
||||
import styles from 'src/styles/modules/pages/name/name.module.scss';
|
||||
|
||||
type Props = InferGetServerSidePropsType<typeof getServerSideProps>;
|
||||
|
||||
const NameInfo = ({ data, error, originalPath }: Props) => {
|
||||
if (error) return <ErrorInfo {...error} originalPath={originalPath} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta
|
||||
title={data.basic.name}
|
||||
description={data.basic.bio.short + '...'}
|
||||
imgUrl={data.basic.poster?.url && getProxiedIMDbImgUrl(data.basic.poster.url)}
|
||||
/>
|
||||
<Layout className={styles.name} originalPath={originalPath}>
|
||||
<Basic data={data.basic} className={styles.basic} />
|
||||
<Media className={styles.media} media={data.media} />
|
||||
<div className={styles.textarea}>
|
||||
<KnownFor data={data.knownFor} />
|
||||
<Bio bio={data.basic.bio.full} />
|
||||
</div>
|
||||
<div className={styles.infoarea}>
|
||||
<Info info={data.personalDetails} accolades={data.accolades} />
|
||||
<DidYouKnow data={data.didYouKnow} />
|
||||
</div>
|
||||
<Credits className={styles.credits} data={data.credits} />
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type Data = ({ data: Name; error: null } | { error: AppError; data: null }) & {
|
||||
originalPath: string;
|
||||
};
|
||||
type Params = { nameId: string };
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<Data, Params> = async ctx => {
|
||||
const nameId = ctx.params!.nameId;
|
||||
const originalPath = ctx.resolvedUrl;
|
||||
|
||||
try {
|
||||
const data = await getOrSetApiCache(nameKey(nameId), name, nameId);
|
||||
|
||||
return { props: { data, error: null, originalPath } };
|
||||
} catch (error: any) {
|
||||
const { message, statusCode } = error;
|
||||
|
||||
ctx.res.statusCode = statusCode;
|
||||
ctx.res.statusMessage = message;
|
||||
|
||||
return { props: { error: { message, statusCode }, data: null, originalPath } };
|
||||
}
|
||||
};
|
||||
|
||||
export default NameInfo;
|
|
@ -1,7 +1,7 @@
|
|||
import Meta from 'src/components/meta/Meta';
|
||||
import Layout from 'src/components/layout';
|
||||
import packageInfo from 'src/../package.json';
|
||||
import styles from 'src/styles/modules/pages/privacy/privacy.module.scss';
|
||||
import Meta from '../../components/Meta/Meta';
|
||||
import Layout from '../../layouts/Layout';
|
||||
|
||||
import styles from '../../styles/modules/pages/privacy/privacy.module.scss';
|
||||
|
||||
const Privacy = () => {
|
||||
return (
|
||||
|
@ -16,15 +16,15 @@ const Privacy = () => {
|
|||
Privacy Policy
|
||||
</h1>
|
||||
<div className={styles.list}>
|
||||
<section className={styles.item}>
|
||||
<div className={styles.item}>
|
||||
<h2
|
||||
className={`heading heading__secondary ${styles.item__heading}`}
|
||||
>
|
||||
Information collected
|
||||
</h2>
|
||||
<p className={styles.item__text}>No information is collected.</p>
|
||||
</section>
|
||||
<section className={styles.item}>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<h2
|
||||
className={`heading heading__secondary ${styles.item__heading}`}
|
||||
>
|
||||
|
@ -40,40 +40,25 @@ const Privacy = () => {
|
|||
prefrences, either turn off JavaScript or disable access to
|
||||
Local Storage for libremdb.
|
||||
</p>
|
||||
</section>
|
||||
<section className={styles.item}>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<h2
|
||||
className={`heading heading__secondary ${styles.item__heading}`}
|
||||
>
|
||||
Instance information
|
||||
Information collected by other services
|
||||
</h2>
|
||||
{process.env.NEXT_PUBLIC_INSTANCE_NAME &&
|
||||
process.env.NEXT_PUBLIC_INSTANCE_MAIN_URL && (
|
||||
<p className={styles.item__text}>
|
||||
Operated by:
|
||||
<a
|
||||
className='link'
|
||||
href={process.env.NEXT_PUBLIC_INSTANCE_MAIN_URL}
|
||||
>
|
||||
{process.env.NEXT_PUBLIC_INSTANCE_NAME}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
<p className={styles.item__text}>
|
||||
Version:
|
||||
<a
|
||||
className='link'
|
||||
href={`https://github.com/zyachel/libremdb/tree/v${packageInfo.version}`}
|
||||
>
|
||||
{packageInfo.version}
|
||||
</a>
|
||||
libremdb connects to 'media-amazon.com' and 'media-imdb.com' for
|
||||
fetching images and videos. So, Amazon might log your IP
|
||||
address, and other information(such as http headers) sent by
|
||||
your browser.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer className={styles.metadata}>
|
||||
<p>
|
||||
Privacy policy last updated on <time>31 october, 2022.</time>
|
||||
Last updated on <time>10 september, 2022.</time>
|
||||
</p>
|
||||
<p>
|
||||
You can see the full revision history of this privacy policy on
|
||||
|
|
|
@ -1,22 +1,34 @@
|
|||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
|
||||
import Meta from 'src/components/meta/Meta';
|
||||
import Layout from 'src/components/layout';
|
||||
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 getOrSetApiCache from 'src/utils/getOrSetApiCache';
|
||||
import title from 'src/utils/fetchers/title';
|
||||
import { getProxiedIMDbImgUrl } from 'src/utils/helpers';
|
||||
import { titleKey } from 'src/utils/constants/keys';
|
||||
import styles from 'src/styles/modules/pages/title/title.module.scss';
|
||||
// external
|
||||
import { GetServerSideProps, GetStaticProps, GetStaticPaths } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
// local
|
||||
import Meta from '../../../components/Meta/Meta'
|
||||
import Layout from '../../../layouts/Layout'
|
||||
import title from '../../../utils/fetchers/title'
|
||||
// components
|
||||
import ErrorInfo from '../../../components/Error/ErrorInfo'
|
||||
import Basic from '../../../components/title/Basic'
|
||||
import Media from '../../../components/title/Media'
|
||||
import Cast from '../../../components/title/Cast'
|
||||
import DidYouKnow from '../../../components/title/DidYouKnow'
|
||||
import Info from '../../../components/title/Info'
|
||||
import Reviews from '../../../components/title/Reviews'
|
||||
import MoreLikeThis from '../../../components/title/MoreLikeThis'
|
||||
// misc
|
||||
import Title from '../../../interfaces/shared/title'
|
||||
import { AppError } from '../../../interfaces/shared/error'
|
||||
// styles
|
||||
import styles from '../../../styles/modules/pages/title/title.module.scss'
|
||||
import Head from 'next/head'
|
||||
|
||||
type Props = InferGetServerSidePropsType<typeof getServerSideProps>;
|
||||
type Props = { data: Title; error: null } | { error: AppError; data: null }
|
||||
|
||||
// TO-DO: make a wrapper page component to display errors, if present in props
|
||||
const TitleInfo = ({ data, error, originalPath }: Props) => {
|
||||
if (error) return <ErrorInfo {...error} originalPath={originalPath} />;
|
||||
const TitleInfo = ({ data, error }: Props) => {
|
||||
const router = useRouter()
|
||||
|
||||
if (error)
|
||||
return <ErrorInfo message={error.message} statusCode={error.statusCode} />
|
||||
|
||||
const info = {
|
||||
meta: data.meta,
|
||||
|
@ -25,54 +37,55 @@ const TitleInfo = ({ data, error, originalPath }: Props) => {
|
|||
boxOffice: data.boxOffice,
|
||||
technicalSpecs: data.technicalSpecs,
|
||||
accolades: data.accolades,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta
|
||||
title={`${data.basic.title} (${data.basic.releaseYear?.start || data.basic.type.name})`}
|
||||
description={data.basic.plot ?? undefined}
|
||||
imgUrl={data.basic.poster?.url && getProxiedIMDbImgUrl(data.basic.poster.url)}
|
||||
title={`${data.basic.title} (${
|
||||
data.basic.releaseYear?.start || data.basic.type.name
|
||||
})`}
|
||||
description={data.basic.plot || undefined}
|
||||
/>
|
||||
<Layout className={styles.title} originalPath={originalPath}>
|
||||
<Head>
|
||||
<meta
|
||||
title="og:image"
|
||||
content={data.basic.poster?.url || '/icon-512.png'}
|
||||
/>
|
||||
</Head>
|
||||
<Layout className={styles.title}>
|
||||
<Basic data={data.basic} className={styles.basic} />
|
||||
<Media className={styles.media} media={data.media} />
|
||||
<Media className={styles.media} media={data.media} router={router} />
|
||||
<Cast className={styles.cast} cast={data.cast} />
|
||||
<div className={styles.textarea}>
|
||||
<DidYouKnow data={data.didYouKnow} />
|
||||
<Reviews reviews={data.reviews} />
|
||||
<Reviews reviews={data.reviews} router={router} />
|
||||
</div>
|
||||
<Info className={styles.infoarea} info={info} />
|
||||
<Info className={styles.infoarea} info={info} router={router} />
|
||||
<MoreLikeThis className={styles.related} data={data.moreLikeThis} />
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
// TO-DO: make a getServerSideProps wrapper for handling errors
|
||||
type Data = ({ data: Title; error: null } | { error: AppError; data: null }) & {
|
||||
originalPath: string;
|
||||
};
|
||||
type Params = { titleId: string };
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<Data, Params> = async ctx => {
|
||||
const titleId = ctx.params!.titleId;
|
||||
const originalPath = ctx.resolvedUrl;
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const titleId = ctx.params!.titleId as string
|
||||
|
||||
try {
|
||||
const data = await getOrSetApiCache(titleKey(titleId), title, titleId);
|
||||
const data = await title(titleId)
|
||||
|
||||
return { props: { data, error: null, originalPath } };
|
||||
return { props: { data, error: null } }
|
||||
} catch (error: any) {
|
||||
const { message, statusCode } = error;
|
||||
ctx.res.statusCode = statusCode;
|
||||
ctx.res.statusMessage = message;
|
||||
const { message, statusCode } = error
|
||||
ctx.res.statusCode = statusCode
|
||||
ctx.res.statusMessage = message
|
||||
|
||||
return { props: { error: { message, statusCode }, data: null, originalPath } };
|
||||
return { props: { error: { message, statusCode }, data: null } }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default TitleInfo;
|
||||
export default TitleInfo
|
||||
|
||||
// could've used getStaticProps instead of getServerSideProps, but meh.
|
||||
/*
|
||||
|
|
|
@ -78,21 +78,3 @@
|
|||
background-size: 100% $height;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
// CHECK IF BROWSER IS SAFARI(it's the new IE)
|
||||
////////////////////////////////////////////////////////////////
|
||||
|
||||
@mixin fix-for-safari {
|
||||
@supports (-webkit-appearance: none) and (stroke-color: transparent) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
//
|
||||
////////////////////////////////////////////////////////////////
|
||||
@mixin focus-rules {
|
||||
outline: 3px solid var(--clr-highlight);
|
||||
outline-offset: 0.2em;
|
||||
}
|
|
@ -21,8 +21,8 @@ $breakpoints: (
|
|||
);
|
||||
|
||||
// 1. colors
|
||||
// $clr-primary: hsl(240, 31%, 25%);
|
||||
// $clr-secondary: hsl(344, 79%, 40%);
|
||||
// $clr-tertiary: hsl(176, 43%, 46%);
|
||||
// $clr-quatenary: hsl(204, 4%, 23%);
|
||||
// $clr-quintenary: hsl(0, 0%, 100%);
|
||||
$clr-primary: hsl(240, 31%, 25%);
|
||||
$clr-secondary: hsl(344, 79%, 40%);
|
||||
$clr-tertiary: hsl(176, 43%, 46%);
|
||||
$clr-quatenary: hsl(204, 4%, 23%);
|
||||
$clr-quintenary: hsl(0, 0%, 100%);
|
||||
|
|
|
@ -22,17 +22,19 @@ $_light: (
|
|||
// 4.2 for borders, primarily
|
||||
fill-muted: hsl(0, 0%, 80%),
|
||||
// shadows on cards
|
||||
shadow: 0 0 0.5em hsla(0, 0%, 0%, 0.2),
|
||||
shadow: 0 0 1rem hsla(0, 0%, 0%, 0.2),
|
||||
// keyboard, focus hightlight
|
||||
highlight: hsl(176, 43%, 46%),
|
||||
// for gradient behind hero text on about page.
|
||||
gradient:
|
||||
(
|
||||
radial-gradient(at 23% 32%, hsla(344, 79%, 40%, 0.15) 0px, transparent 70%),
|
||||
radial-gradient(
|
||||
at 23% 32%,
|
||||
hsla(344, 79%, 40%, 0.15) 0px,
|
||||
transparent 70%
|
||||
),
|
||||
radial-gradient(at 72% 55%, hsla(344, 79%, 40%, 0.2) 0px, transparent 50%)
|
||||
),
|
||||
// changes color of native html elemnts, either 'light' or 'dark' must be set.
|
||||
scheme: light
|
||||
)
|
||||
);
|
||||
|
||||
$_dark: (
|
||||
|
@ -52,7 +54,6 @@ $_dark: (
|
|||
radial-gradient(at 23% 32%, hsla(344, 79%, 40%, 0.04) 0px, transparent 70%),
|
||||
radial-gradient(at 72% 55%, hsla(344, 79%, 40%, 0.05) 0px, transparent 50%),
|
||||
),
|
||||
scheme: dark,
|
||||
);
|
||||
|
||||
$themes: (
|
||||
|
|
|
@ -9,17 +9,11 @@ body {
|
|||
#__next {
|
||||
display: grid;
|
||||
grid-template-rows: min-content 1fr min-content;
|
||||
|
||||
&:has(span[role='progressbar']) {
|
||||
cursor: progress;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--clr-text);
|
||||
background-color: var(--clr-bg);
|
||||
color-scheme: var(--clr-scheme);
|
||||
accent-color: var(--clr-fill);
|
||||
}
|
||||
|
||||
// restricting to 1600px width
|
||||
|
@ -28,20 +22,3 @@ body {
|
|||
width: min(100%, var(--max-width));
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// KEYBOARD NAVIGATION
|
||||
////////////////////////////////////////////////////////
|
||||
:focus {
|
||||
@include helper.focus-rules;
|
||||
}
|
||||
|
||||
@supports selector(:focus-visible) {
|
||||
:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
@include helper.focus-rules;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
@forward './reset';
|
||||
@forward './helpers';
|
||||
// @forward './helpers';
|
||||
@forward './root';
|
||||
@forward './base';
|
||||
@forward './fonts';
|
||||
|
|
|
@ -24,9 +24,4 @@
|
|||
@include helper.bp('bp-700') {
|
||||
@include helper.typescale('mobile');
|
||||
}
|
||||
|
||||
// not using any external fonts on webkit because of many issues
|
||||
@include helper.fix-for-safari {
|
||||
--ff-accent: var(--ff-base);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ body {
|
|||
color: var(--clr-text-accent);
|
||||
font-family: var(--ff-accent);
|
||||
font-weight: var(--fw-medium);
|
||||
line-height: 1.2;
|
||||
|
||||
&__primary {
|
||||
font-size: var(--fs-1);
|
||||
|
|
|
@ -1,97 +0,0 @@
|
|||
@use '../../../abstracts' as helper;
|
||||
|
||||
.container {
|
||||
margin-inline: auto;
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: minmax(25rem, 30rem) 1fr;
|
||||
|
||||
@include helper.bp('bp-900') {
|
||||
grid-template-columns: none;
|
||||
grid-template-rows: 30rem min-content;
|
||||
}
|
||||
|
||||
@include helper.bp('bp-700') {
|
||||
grid-template-rows: 25rem min-content;
|
||||
}
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
display: flex; // for bringing out image__NA out of blur
|
||||
|
||||
position: relative;
|
||||
height: auto;
|
||||
width: auto;
|
||||
overflow: hidden;
|
||||
|
||||
background-size: cover;
|
||||
background-position: top;
|
||||
place-items: center;
|
||||
|
||||
@include helper.bp('bp-900') {
|
||||
padding: var(--spacer-2);
|
||||
isolation: isolate;
|
||||
|
||||
// for adding layer of color on top of background image
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
var(--clr-bg-accent) 10%,
|
||||
transparent
|
||||
);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
|
||||
@include helper.bp('bp-900') {
|
||||
z-index: 1;
|
||||
object-fit: contain;
|
||||
|
||||
outline: 3px solid var(--clr-fill);
|
||||
outline-offset: 5px;
|
||||
|
||||
max-height: 100%;
|
||||
margin: auto;
|
||||
|
||||
// overrriding nex/future/image defaults
|
||||
height: initial !important;
|
||||
width: initial !important;
|
||||
position: relative !important;
|
||||
}
|
||||
}
|
||||
|
||||
.imageNA {
|
||||
z-index: 1;
|
||||
fill: var(--clr-fill-muted);
|
||||
}
|
||||
|
||||
.info {
|
||||
padding: var(--spacer-2) var(--spacer-4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacer-2);
|
||||
|
||||
@include helper.bp('bp-900') {
|
||||
// text-align: center;
|
||||
// align-items: center;
|
||||
}
|
||||
@include helper.bp('bp-450') {
|
||||
gap: var(--spacer-1);
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
line-height: 1;
|
||||
|
||||
@include helper.bp('bp-900') {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue