Compare commits
92 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
410cc70259 | ||
![]() |
258a82f2ac | ||
![]() |
e2a335f98d | ||
![]() |
246f1155d5 | ||
![]() |
19f1700a55 | ||
![]() |
9fdd731136 | ||
![]() |
4dffbbc0ec | ||
![]() |
264442448f | ||
![]() |
2b00d5406a | ||
![]() |
97f1432ac5 | ||
![]() |
60fb23fc5b | ||
![]() |
12eaa741ab | ||
![]() |
40eb8a372b | ||
![]() |
e91c313f12 | ||
![]() |
5fa5e9e2c2 | ||
![]() |
27322a4c8c | ||
![]() |
21a1c83d95 | ||
![]() |
b07cb713d8 | ||
![]() |
5628d6b75d | ||
![]() |
38ed0c6217 | ||
![]() |
c610ef4d1b | ||
![]() |
736d680243 | ||
![]() |
0aea2f47da | ||
![]() |
23eeae3558 | ||
![]() |
bb6405cb05 | ||
![]() |
c53c88db9b | ||
![]() |
8599ae2c5a | ||
![]() |
8d9b6630a5 | ||
![]() |
be80244eb3 | ||
![]() |
a0f3ba095a | ||
![]() |
11aea1d489 | ||
![]() |
3ef41d9a6d | ||
![]() |
7dea9eac14 | ||
![]() |
86737c51ee | ||
![]() |
75732e0086 | ||
![]() |
18ca98fd4a | ||
![]() |
8ce02d0236 | ||
![]() |
cbce2cac34 | ||
![]() |
1eeaab259d | ||
![]() |
505ff4d839 | ||
![]() |
20418b4c1f | ||
![]() |
68072b5f68 | ||
![]() |
c79dc2a481 | ||
![]() |
4dde7bde77 | ||
![]() |
2c5d2f86e4 | ||
![]() |
78b8a9afc3 | ||
![]() |
5cc2ef02ce | ||
![]() |
71d1d5b34e | ||
![]() |
feffb7d8f6 | ||
![]() |
182b3c1072 | ||
![]() |
a32785ce00 | ||
![]() |
cfa8c53d11 | ||
![]() |
5d45990798 | ||
![]() |
0cff34a766 | ||
![]() |
81eaf2fd5e | ||
![]() |
57b050f196 | ||
![]() |
64f3896258 | ||
![]() |
b4bcdb7152 | ||
![]() |
80b0ca6bf0 | ||
![]() |
78b14ec079 | ||
![]() |
c2df20e6ad | ||
![]() |
2afb5b1da6 | ||
![]() |
dd75df01eb | ||
![]() |
28d8331ae9 | ||
![]() |
7501b69078 | ||
![]() |
5dc6f60cae | ||
![]() |
1658769a30 | ||
![]() |
5fd0d92187 | ||
![]() |
6f664d2164 | ||
![]() |
31218adac1 | ||
![]() |
0b1081c485 | ||
![]() |
7a717aa212 | ||
![]() |
a410bc4264 | ||
![]() |
fda79adc52 | ||
![]() |
0213de9403 | ||
![]() |
6ae71d7907 | ||
![]() |
eac51d2465 | ||
![]() |
672ee27dcc | ||
![]() |
cc7968074b | ||
![]() |
cdd73c6123 | ||
![]() |
44d3a33fb3 | ||
![]() |
720f2b6acb | ||
![]() |
9bce8a2dd5 | ||
![]() |
1983f6b1fb | ||
![]() |
dba2ba5aa4 | ||
![]() |
b7ee6863e5 | ||
![]() |
2c8d138cbd | ||
![]() |
59a314b2bd | ||
![]() |
261a37576b | ||
![]() |
0c76f485f9 | ||
![]() |
a2fc2322a3 | ||
![]() |
478b45977d |
146 changed files with 6547 additions and 1618 deletions
|
@ -1,9 +1,43 @@
|
|||
# required fields
|
||||
# used for meta tags. e.g: 'https://libremdb.iket.me' don't add end slash.
|
||||
NEXT_PUBLIC_URL=
|
||||
################################################################################
|
||||
### PLEASE FILL/ENABLE REQUIRED VARS AT LEAST BEFORE RUNNING THE APPLICATION ###
|
||||
################################################################################
|
||||
|
||||
# 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=
|
||||
################################################################################
|
||||
### 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.
|
||||
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=
|
29
.github/workflows/release.yml
vendored
29
.github/workflows/release.yml
vendored
|
@ -1,29 +0,0 @@
|
|||
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,8 +28,15 @@ yarn-error.log*
|
|||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
next-env.d.ts
|
||||
|
||||
#just dev stuff
|
||||
dev/*
|
||||
yarn.lock
|
||||
|
||||
# other lockfiles
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
|
||||
# docker
|
||||
docker-compose.yml
|
||||
dump.rdb
|
13
.prettierrc
13
.prettierrc
|
@ -1,6 +1,9 @@
|
|||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"arrowParens": "avoid",
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
|
|
36
.versionrc
Normal file
36
.versionrc
Normal file
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"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,54 +1,105 @@
|
|||
# [2.0.0](https://github.com/zyachel/libremdb/compare/v0.1.2...v2.0.0) (2022-10-31)
|
||||
# Changelog
|
||||
|
||||
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.
|
||||
|
||||
### 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))
|
||||
## [3.2.0](https://github.com/zyachel/libremdb/compare/v3.1.1...v3.2.0) (2023-10-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 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))
|
||||
* **list:** add list route ([97f1432](https://github.com/zyachel/libremdb/commit/97f1432ac5d23206229d806b7cb3e04af6dec36f))
|
||||
|
||||
|
||||
### 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)
|
||||
## [3.1.1](https://github.com/zyachel/libremdb/compare/v3.1.0...v3.1.1) (2023-10-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 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))
|
||||
* **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))
|
||||
|
||||
|
||||
### 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)
|
||||
## [3.1.0](https://github.com/zyachel/libremdb/compare/v3.0.0...v3.1.0) (2023-05-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add review section ([30dac07](https://github.com/zyachel/libremdb/commit/30dac07ba33dbe4331a5c9fa6cd2c332100868df))
|
||||
* **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))
|
||||
|
||||
|
||||
|
||||
## [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
Normal file
35
Dockerfile
Normal file
|
@ -0,0 +1,35 @@
|
|||
# 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.png" title="screenshot (desktop screen, light mode)" width="1500" /> | <img src="./public/img/misc/preview2.png" title="screenshot (mobile screen, dark mode)" width="385" /> |
|
||||
| <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" /> |
|
||||
|
||||
---
|
||||
|
||||
|
@ -32,23 +32,28 @@ 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/) |
|
||||
| [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) |
|
||||
| [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/)
|
||||
| 2. Onion | | |
|
||||
| [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) |
|
||||
| [ld.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion](http://ld.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion) | US | Operated by [~vern](https://vern.cc) |
|
||||
| 3. I2P | | |
|
||||
| [vernz3ubrntql4wrgyrssd6u3qzi36zrhz2agbo6vibzbs5olk2q.b32.i2p](http://vernz3ubrntql4wrgyrssd6u3qzi36zrhz2agbo6vibzbs5olk2q.b32.i2p) | Canada | Operated by [~vern](https://vern.cc) |
|
||||
| [vernz3ubrntql4wrgyrssd6u3qzi36zrhz2agbo6vibzbs5olk2q.b32.i2p](http://vernz3ubrntql4wrgyrssd6u3qzi36zrhz2agbo6vibzbs5olk2q.b32.i2p) | US | Operated by [~vern](https://vern.cc) |
|
||||
|
||||
---
|
||||
|
||||
|
@ -64,15 +69,12 @@ Inspired by projects like [teddit](https://codeberg.org/teddit/teddit), [nitter]
|
|||
- It doesn't have all routes.
|
||||
I'll implement more with time :)
|
||||
|
||||
- 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.
|
||||
- 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)).
|
||||
|
||||
- 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.
|
||||
|
||||
|
@ -87,7 +89,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:
|
||||
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.
|
||||
None. libremdb proxies images anonymously through the instance for maximum privacy ([Contributor](https://github.com/httpjamesm)).
|
||||
|
||||
---
|
||||
|
||||
|
@ -108,13 +110,13 @@ Inspired by projects like [teddit](https://codeberg.org/teddit/teddit), [nitter]
|
|||
|
||||
- [ ] lists
|
||||
- [ ] moviemeter
|
||||
- [ ] person info(includes directors and actors)
|
||||
- [x] person info(includes directors and actors)
|
||||
- [ ] company info
|
||||
- [ ] user info
|
||||
|
||||
- [ ] use redis, or any other caching strategy
|
||||
- [X] use redis, or any other caching strategy
|
||||
- [x] implement a better installation method
|
||||
- [ ] serve images and videos from libremdb itself
|
||||
- [x] serve images and videos from libremdb itself
|
||||
|
||||
---
|
||||
|
||||
|
@ -128,27 +130,36 @@ 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. Clone and set up the repo.
|
||||
2. Install redis(optional).
|
||||
You can install redis from [here](https://redis.io).
|
||||
|
||||
3. 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
|
||||
# optional configuration
|
||||
# change the configuration file to your liking.
|
||||
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
|
||||
### Docker (Local)
|
||||
|
||||
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.
|
||||
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).
|
||||
|
||||
---
|
||||
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
|
||||
|
||||
|
@ -160,13 +171,15 @@ 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)
|
||||
|
|
47
docker-compose.example.yml
Normal file
47
docker-compose.example.yml
Normal file
|
@ -0,0 +1,47 @@
|
|||
# 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
5
next-env.d.ts
vendored
|
@ -1,5 +0,0 @@
|
|||
/// <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: '/about',
|
||||
destination: '/find',
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
|
@ -20,6 +20,7 @@ const nextConfig = {
|
|||
},
|
||||
isrMemoryCacheSize: 20 * 1024 * 1024,
|
||||
},
|
||||
poweredByHeader: false,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
15
package.json
15
package.json
|
@ -1,11 +1,15 @@
|
|||
{
|
||||
"name": "libremdb",
|
||||
"version": "2.0.0",
|
||||
"version": "3.2.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",
|
||||
|
@ -15,10 +19,11 @@
|
|||
"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.0"
|
||||
"sharp": "^0.31.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.7.3",
|
||||
|
@ -26,11 +31,11 @@
|
|||
"@types/react-dom": "18.0.6",
|
||||
"eslint": "8.22.0",
|
||||
"eslint-config-next": "12.2.5",
|
||||
"sass": "^1.54.4",
|
||||
"sass": "^1.62.1",
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.5.0",
|
||||
"pnpm": ">=7.0.0"
|
||||
"pnpm": ">=8.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
1431
pnpm-lock.yaml
generated
1431
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
BIN
public/img/misc/preview.jpg
Normal file
BIN
public/img/misc/preview.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 96 KiB |
Binary file not shown.
Before Width: | Height: | Size: 805 KiB |
BIN
public/img/misc/preview2.jpg
Normal file
BIN
public/img/misc/preview2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 126 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.6 MiB |
|
@ -15,42 +15,19 @@
|
|||
</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>
|
||||
|
@ -60,14 +37,10 @@
|
|||
<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-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 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>
|
||||
<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 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>
|
||||
<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: 13 KiB After Width: | Height: | Size: 6.5 KiB |
|
@ -1,48 +0,0 @@
|
|||
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,7 +1,6 @@
|
|||
import { useContext } from 'react';
|
||||
import { themeContext } from '../../context/theme-context';
|
||||
|
||||
import styles from '../../styles/modules/components/buttons/themeToggler.module.scss';
|
||||
import { themeContext } from 'src/context/theme-context';
|
||||
import styles from 'src/styles/modules/components/buttons/themeToggler.module.scss';
|
||||
|
||||
type Props = {
|
||||
className: string;
|
||||
|
@ -17,9 +16,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'
|
||||
|
|
29
src/components/card/Card.tsx
Normal file
29
src/components/card/Card.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
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;
|
45
src/components/card/CardBasic.tsx
Normal file
45
src/components/card/CardBasic.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
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;
|
51
src/components/card/CardCast.tsx
Normal file
51
src/components/card/CardCast.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
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;
|
40
src/components/card/CardResult.tsx
Normal file
40
src/components/card/CardResult.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
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;
|
63
src/components/card/CardTitle.tsx
Normal file
63
src/components/card/CardTitle.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
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;
|
5
src/components/card/index.tsx
Normal file
5
src/components/card/index.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
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';
|
45
src/components/error/ErrorBoundary.tsx
Normal file
45
src/components/error/ErrorBoundary.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
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;
|
71
src/components/error/ErrorInfo.tsx
Normal file
71
src/components/error/ErrorInfo.tsx
Normal file
|
@ -0,0 +1,71 @@
|
|||
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;
|
13
src/components/find/Company.tsx
Normal file
13
src/components/find/Company.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
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;
|
12
src/components/find/Keyword.tsx
Normal file
12
src/components/find/Keyword.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
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;
|
20
src/components/find/Person.tsx
Normal file
20
src/components/find/Person.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
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;
|
37
src/components/find/Title.tsx
Normal file
37
src/components/find/Title.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
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;
|
92
src/components/find/index.tsx
Normal file
92
src/components/find/index.tsx
Normal file
|
@ -0,0 +1,92 @@
|
|||
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;
|
121
src/components/forms/find/index.tsx
Normal file
121
src/components/forms/find/index.tsx
Normal file
|
@ -0,0 +1,121 @@
|
|||
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;
|
52
src/components/layout/Footer.tsx
Normal file
52
src/components/layout/Footer.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
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,37 +1,22 @@
|
|||
import { ReactNode } from 'react';
|
||||
// import dynamic from 'next/dynamic';
|
||||
import Link from 'next/link';
|
||||
import styles from '../styles/modules/layout/header.module.scss';
|
||||
import ThemeToggler from '../components/buttons/ThemeToggler';
|
||||
import ThemeToggler from 'src/components/buttons/ThemeToggler';
|
||||
import styles from 'src/styles/modules/layout/header.module.scss';
|
||||
|
||||
// const ThemeToggler = dynamic(
|
||||
// () => import('../components/buttons/ThemeToggler'),
|
||||
// { ssr: false }
|
||||
// );
|
||||
type Props = { full?: boolean; originalPath?: string };
|
||||
|
||||
type Props = { full?: boolean; children?: ReactNode };
|
||||
|
||||
const Header = (props: Props) => {
|
||||
const Header = ({ full, originalPath }: Props) => {
|
||||
return (
|
||||
<header
|
||||
id='header'
|
||||
className={`${styles.header} ${props.full ? styles.header__about : ''}`}
|
||||
>
|
||||
<header id='header' className={`${styles.header} ${full ? styles.header__about : ''}`}>
|
||||
<div className={styles.topbar}>
|
||||
<Link href='/about'>
|
||||
<Link href='/find'>
|
||||
<a aria-label='go to homepage' className={styles.logo}>
|
||||
<svg
|
||||
className={styles.logo__icon}
|
||||
focusable='false'
|
||||
role='img'
|
||||
aria-hidden='true'
|
||||
>
|
||||
<svg className={styles.logo__icon} role='img' aria-hidden>
|
||||
<use href='/svg/sprite.svg#icon-logo'></use>
|
||||
</svg>
|
||||
<span className={styles.logo__text}>libremdb</span>
|
||||
</a>
|
||||
</Link>
|
||||
{props.full && (
|
||||
{full && (
|
||||
<nav className={styles.nav}>
|
||||
<ul className={styles.nav__list}>
|
||||
<li className={styles.nav__item}>
|
||||
|
@ -52,27 +37,40 @@ const Header = (props: Props) => {
|
|||
</ul>
|
||||
</nav>
|
||||
)}
|
||||
<ThemeToggler className={styles.themeToggler} />
|
||||
<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>
|
||||
</div>
|
||||
{props.full && (
|
||||
{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,17 +1,18 @@
|
|||
import React from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import Footer from './Footer';
|
||||
import Header from './Header';
|
||||
|
||||
type Props = {
|
||||
full?: boolean;
|
||||
children: React.ReactNode;
|
||||
full?: true;
|
||||
children: ReactNode;
|
||||
className: string;
|
||||
originalPath?: string;
|
||||
};
|
||||
|
||||
const Layout = ({ full, children, className }: Props) => {
|
||||
const Layout = ({ full, children, className, originalPath }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<Header full={full} />
|
||||
<Header full={full} originalPath={originalPath} />
|
||||
<main id='main' className={`main ${className}`}>
|
||||
{children}
|
||||
</main>
|
23
src/components/list/Data.tsx
Normal file
23
src/components/list/Data.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
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];
|
22
src/components/list/Images.tsx
Normal file
22
src/components/list/Images.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
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;
|
35
src/components/list/Meta.tsx
Normal file
35
src/components/list/Meta.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
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;
|
57
src/components/list/Names.tsx
Normal file
57
src/components/list/Names.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
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>
|
||||
);
|
||||
};
|
20
src/components/list/OptionalLink.tsx
Normal file
20
src/components/list/OptionalLink.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
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;
|
33
src/components/list/Pagination.tsx
Normal file
33
src/components/list/Pagination.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
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;
|
79
src/components/list/Titles.tsx
Normal file
79
src/components/list/Titles.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
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>
|
||||
);
|
||||
};
|
3
src/components/list/index.ts
Normal file
3
src/components/list/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
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 '../../styles/modules/components/loaders/progress-bar.module.scss';
|
||||
import styles from 'src/styles/modules/components/loaders/progress-bar.module.scss';
|
||||
|
||||
const ProgressBar = () => {
|
||||
return <span className={styles.progress} role='progressbar'></span>;
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
import Image from 'next/future/image';
|
||||
import Link from 'next/link';
|
||||
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';
|
||||
import { Media } from 'src/interfaces/shared';
|
||||
import { getProxiedIMDbImgUrl, modifyIMDbImg } from 'src/utils/helpers';
|
||||
import styles from 'src/styles/modules/components/media/media.module.scss';
|
||||
|
||||
type Props = {
|
||||
className: string;
|
||||
media: Media;
|
||||
router: NextRouter;
|
||||
};
|
||||
|
||||
const Media = ({ className, media, router }: Props) => {
|
||||
// TODO: refactor this component.
|
||||
|
||||
const Media = ({ className, media }: Props) => {
|
||||
return (
|
||||
<div className={`${className} ${styles.media}`}>
|
||||
{(media.trailer || !!media.videos.total) && (
|
||||
|
@ -21,21 +20,20 @@ const Media = ({ className, media, router }: Props) => {
|
|||
|
||||
<div className={styles.videos__container}>
|
||||
{media.trailer && (
|
||||
<div key={router.asPath} className={styles.trailer}>
|
||||
<div 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={modifyIMDbImg(media.trailer.thumbnail)}
|
||||
poster={getProxiedIMDbImgUrl(modifyIMDbImg(media.trailer.thumbnail))}
|
||||
className={styles.trailer__video}
|
||||
preload='none'
|
||||
>
|
||||
{media.trailer.urls.map(source => (
|
||||
<source
|
||||
key={source.url}
|
||||
type={source.mimeType}
|
||||
src={source.url}
|
||||
src={getProxiedIMDbImgUrl(source.url)}
|
||||
data-res={source.resolution}
|
||||
/>
|
||||
))}
|
||||
|
@ -76,9 +74,7 @@ const Media = ({ className, media, router }: 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,4 +1,5 @@
|
|||
import Head from 'next/head';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
|
@ -6,11 +7,15 @@ 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' />
|
||||
|
@ -30,10 +35,7 @@ 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={`${process.env.NEXT_PUBLIC_URL}/${imgUrl}`}
|
||||
/>
|
||||
<meta property='og:image' content={url.toString()} />
|
||||
</Head>
|
||||
);
|
||||
};
|
59
src/components/name/Basic.tsx
Normal file
59
src/components/name/Basic.tsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
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;
|
12
src/components/name/Bio.tsx
Normal file
12
src/components/name/Bio.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
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;
|
43
src/components/name/Credits.tsx
Normal file
43
src/components/name/Credits.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
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;
|
51
src/components/name/DidYouKnow.tsx
Normal file
51
src/components/name/DidYouKnow.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
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;
|
184
src/components/name/Info.tsx
Normal file
184
src/components/name/Info.tsx
Normal file
|
@ -0,0 +1,184 @@
|
|||
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>
|
||||
);
|
34
src/components/name/KnownFor.tsx
Normal file
34
src/components/name/KnownFor.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
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;
|
6
src/components/name/index.ts
Normal file
6
src/components/name/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
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,10 +1,9 @@
|
|||
import { Fragment } from 'react';
|
||||
import Image from 'next/future/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { formatNumber, formatTime, modifyIMDbImg } from '../../utils/helpers';
|
||||
import { Basic } from '../../interfaces/shared/title';
|
||||
import styles from '../../styles/modules/components/title/basic.module.scss';
|
||||
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';
|
||||
|
||||
type Props = {
|
||||
className: string;
|
||||
|
@ -19,134 +18,92 @@ const Basic = ({ data, className }: Props) => {
|
|||
: data.releaseYear?.start;
|
||||
|
||||
return (
|
||||
<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})` }}
|
||||
<CardBasic
|
||||
className={`${styles.container} ${className}`}
|
||||
image={data.poster?.url}
|
||||
title={data.title}
|
||||
>
|
||||
<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>
|
||||
<ul className={styles.meta} aria-label='quick facts'>
|
||||
{data.status && data.status.id !== 'released' && (
|
||||
<li className={styles.meta__text}>{data.status.text}</li>
|
||||
)}
|
||||
</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 && (
|
||||
<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}>
|
||||
{formatNumber(data.ranking.position)}
|
||||
</span>
|
||||
<span className={styles.rating__num}>{data.ratings.avg}</span>
|
||||
<svg className={styles.rating__icon}>
|
||||
<use href='/svg/sprite.svg#icon-graph-rising'></use>
|
||||
<use href='/svg/sprite.svg#icon-rating'></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>
|
||||
<span className={styles.rating__text}> Avg. rating</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 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>
|
||||
)}
|
||||
{
|
||||
<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>
|
||||
|
||||
{!!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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
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';
|
||||
import { CardCast } from 'src/components/card';
|
||||
import { Cast } from 'src/interfaces/shared/title';
|
||||
import styles from 'src/styles/modules/components/title/cast.module.scss';
|
||||
|
||||
type Props = {
|
||||
className: string;
|
||||
|
@ -11,46 +8,25 @@ type Props = {
|
|||
};
|
||||
|
||||
const Cast = ({ className, cast }: Props) => {
|
||||
if (!cast.length) return <></>;
|
||||
if (!cast.length) return null;
|
||||
|
||||
return (
|
||||
<section className={`${className} ${styles.container}`}>
|
||||
<h2 className='heading heading__secondary'>Cast</h2>
|
||||
<ul className={styles.cast}>
|
||||
{cast.map(member => (
|
||||
<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>
|
||||
<CardCast
|
||||
key={member.id}
|
||||
link={`/name/${member.id}`}
|
||||
name={member.name}
|
||||
image={member.image}
|
||||
characters={member.characters}
|
||||
attributes={member.attributes}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Cast;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import Link from 'next/link';
|
||||
import { Fragment } from 'react';
|
||||
import { DidYouKnow } from '../../interfaces/shared/title';
|
||||
import styles from '../../styles/modules/components/title/did-you-know.module.scss';
|
||||
import { DidYouKnow } from 'src/interfaces/shared/title';
|
||||
import styles from 'src/styles/modules/components/title/did-you-know.module.scss';
|
||||
|
||||
type Props = {
|
||||
data: DidYouKnow;
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
import Link from 'next/link';
|
||||
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';
|
||||
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';
|
||||
|
||||
type Props = {
|
||||
info: Info;
|
||||
className: string;
|
||||
router: NextRouter;
|
||||
};
|
||||
|
||||
const Info = ({ info, className, router }: Props) => {
|
||||
const Info = ({ info, className }: Props) => {
|
||||
const router = useRouter();
|
||||
const { titleId } = router.query;
|
||||
const { boxOffice, details, meta, keywords, technicalSpecs, accolades } =
|
||||
info;
|
||||
|
@ -20,7 +19,7 @@ const Info = ({ info, className, router }: 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}>
|
||||
|
@ -50,14 +49,14 @@ const Info = ({ info, className, router }: 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>
|
||||
)}
|
||||
|
@ -66,7 +65,7 @@ const Info = ({ info, className, router }: 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>
|
||||
|
@ -82,19 +81,19 @@ const Info = ({ info, className, router }: 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>
|
||||
)}
|
||||
|
@ -112,24 +111,21 @@ const Info = ({ info, className, router }: 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.replaceAll(
|
||||
' ',
|
||||
'-'
|
||||
)}`}
|
||||
href={`/search/keyword/?keywords=${word.replace(/\s/g, '-')}`}
|
||||
>
|
||||
<a className='link'>{word}</a>
|
||||
<a className="link">{word}</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
|
@ -138,7 +134,7 @@ const Info = ({ info, className, router }: 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>
|
||||
|
@ -159,7 +155,7 @@ const Info = ({ info, className, router }: Props) => {
|
|||
<Link
|
||||
href={`/search/title/?country_of_origin=${country.id}`}
|
||||
>
|
||||
<a className='link'>{country.text}</a>
|
||||
<a className="link">{country.text}</a>
|
||||
</Link>
|
||||
</span>
|
||||
))}
|
||||
|
@ -171,7 +167,7 @@ const Info = ({ info, className, router }: 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>
|
||||
|
@ -185,7 +181,7 @@ const Info = ({ info, className, router }: 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>
|
||||
))}
|
||||
|
@ -204,7 +200,7 @@ const Info = ({ info, className, router }: Props) => {
|
|||
<span key={loc}>
|
||||
{!!i && ', '}
|
||||
<Link href={`/search/title/?locations=${loc}`}>
|
||||
<a className='link'>{loc}</a>
|
||||
<a className="link">{loc}</a>
|
||||
</Link>
|
||||
</span>
|
||||
))}
|
||||
|
@ -217,7 +213,7 @@ const Info = ({ info, className, router }: 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>
|
||||
))}
|
||||
|
@ -228,7 +224,7 @@ const Info = ({ info, className, router }: 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>
|
||||
|
@ -280,7 +276,7 @@ const Info = ({ info, className, router }: 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>
|
||||
|
@ -296,7 +292,7 @@ const Info = ({ info, className, router }: 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>
|
||||
))}
|
||||
|
@ -311,7 +307,7 @@ const Info = ({ info, className, router }: 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,8 +1,6 @@
|
|||
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';
|
||||
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';
|
||||
|
||||
type Props = {
|
||||
className: string;
|
||||
|
@ -10,52 +8,22 @@ type Props = {
|
|||
};
|
||||
|
||||
const MoreLikeThis = ({ className, data }: Props) => {
|
||||
if (!data.length) return <></>;
|
||||
if (!data.length) return null;
|
||||
|
||||
return (
|
||||
<section className={`${className} ${styles.morelikethis}`}>
|
||||
<h2 className='heading heading__secondary'>More like this</h2>
|
||||
<ul className={styles.container}>
|
||||
{data.map(title => (
|
||||
<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>
|
||||
<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>
|
||||
</section>
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import { NextRouter } from 'next/router';
|
||||
import { useRouter } from 'next/router';
|
||||
import Link from 'next/link';
|
||||
import { Reviews } from '../../interfaces/shared/title';
|
||||
import { formatNumber } from '../../utils/helpers';
|
||||
import styles from '../../styles/modules/components/title/reviews.module.scss';
|
||||
import { Reviews } from 'src/interfaces/shared/title';
|
||||
import { formatNumber } from 'src/utils/helpers';
|
||||
import styles from 'src/styles/modules/components/title/reviews.module.scss';
|
||||
|
||||
type Props = {
|
||||
reviews: Reviews;
|
||||
router: NextRouter;
|
||||
};
|
||||
|
||||
const Reviews = ({ reviews, router }: Props) => {
|
||||
const Reviews = ({ reviews }: Props) => {
|
||||
const router = useRouter();
|
||||
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, router }: 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, router }: 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>
|
||||
|
|
6
src/components/title/index.ts
Normal file
6
src/components/title/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
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,10 +1,13 @@
|
|||
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 = window.localStorage.getItem('theme') || null;
|
||||
const userPrefersTheme = (
|
||||
isLocalStorageAvailable() ? window.localStorage.getItem('theme') : null
|
||||
) as 'light' | 'dark' | null;
|
||||
const browserPrefersDarkTheme = window.matchMedia(
|
||||
'(prefers-color-scheme: dark)'
|
||||
).matches;
|
||||
|
@ -25,7 +28,7 @@ const updateMetaTheme = () => {
|
|||
|
||||
const initialContext = {
|
||||
theme: '',
|
||||
setTheme: (theme: string) => {},
|
||||
setTheme: (theme: ReturnType<typeof getInitialTheme>) => { },
|
||||
};
|
||||
|
||||
export const themeContext = createContext(initialContext);
|
||||
|
@ -33,9 +36,9 @@ export const themeContext = createContext(initialContext);
|
|||
const ThemeProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [curTheme, setCurTheme] = useState(getInitialTheme);
|
||||
|
||||
const setTheme = (theme: string) => {
|
||||
const setTheme = (theme: typeof curTheme) => {
|
||||
setCurTheme(theme);
|
||||
window.localStorage.setItem('theme', theme);
|
||||
if (isLocalStorageAvailable()) window.localStorage.setItem('theme', theme);
|
||||
document.documentElement.dataset.theme = theme;
|
||||
updateMetaTheme();
|
||||
};
|
||||
|
|
31
src/hooks/usePageLoading.ts
Normal file
31
src/hooks/usePageLoading.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
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;
|
86
src/interfaces/misc/rawFind.ts
Normal file
86
src/interfaces/misc/rawFind.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
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']
|
||||
// }}}
|
||||
// }
|
1092
src/interfaces/misc/rawName.ts
Normal file
1092
src/interfaces/misc/rawName.ts
Normal file
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,9 +516,11 @@ export default interface RawTitle {
|
|||
canRate: {
|
||||
isRatable: boolean;
|
||||
};
|
||||
titleCardGenres: {
|
||||
titleGenres: {
|
||||
genres: Array<{
|
||||
text: string;
|
||||
genre: {
|
||||
text: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
canHaveEpisodes: boolean;
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
export type AppError = {
|
||||
message: string;
|
||||
statusCode: number;
|
||||
stack?: any;
|
||||
};
|
||||
import { AppError as AppErrorClass } from 'src/utils/helpers';
|
||||
|
||||
export type AppError = Omit<InstanceType<typeof AppErrorClass>, 'name'>;
|
||||
|
|
6
src/interfaces/shared/index.ts
Normal file
6
src/interfaces/shared/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
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;
|
39
src/interfaces/shared/list.ts
Normal file
39
src/interfaces/shared/list.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
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;
|
16
src/interfaces/shared/name.ts
Normal file
16
src/interfaces/shared/name.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
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'];
|
28
src/interfaces/shared/search.ts
Normal file
28
src/interfaces/shared/search.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
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,7 +1,5 @@
|
|||
import cleanTitle from '../../utils/cleaners/title';
|
||||
import title from '../../utils/fetchers/title';
|
||||
|
||||
export type AxiosTitleRes = Awaited<ReturnType<typeof title>>;
|
||||
import cleanTitle from 'src/utils/cleaners/title';
|
||||
import title from 'src/utils/fetchers/title';
|
||||
|
||||
// for full title
|
||||
type Title = ReturnType<typeof cleanTitle>;
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
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,7 +1,7 @@
|
|||
import ErrorInfo from '../components/Error/ErrorInfo';
|
||||
import ErrorInfo from 'src/components/error/ErrorInfo';
|
||||
|
||||
const Error404 = () => {
|
||||
return <ErrorInfo />;
|
||||
return <ErrorInfo message='Not found, sorry.' statusCode={404} />;
|
||||
};
|
||||
|
||||
export default Error404;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import ErrorInfo from '../components/Error/ErrorInfo';
|
||||
import ErrorInfo from 'src/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;
|
||||
|
|
25
src/pages/[...error]/index.tsx
Normal file
25
src/pages/[...error]/index.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
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,38 +1,22 @@
|
|||
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';
|
||||
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';
|
||||
|
||||
const ModifiedApp = ({ Component, pageProps }: AppProps) => {
|
||||
// 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]);
|
||||
//
|
||||
const { isPageLoading, key } = usePageLoading();
|
||||
|
||||
return (
|
||||
<ThemeProvider>
|
||||
{isLoading && <ProgressBar />}
|
||||
<Component {...pageProps} />
|
||||
{isPageLoading && <ProgressBar />}
|
||||
<ErrorBoundary>
|
||||
<Component
|
||||
{...pageProps}
|
||||
key={key} /* passing key to force react to remount components */
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,15 +3,25 @@ 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;
|
||||
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';
|
||||
(() => {
|
||||
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);
|
||||
})();
|
||||
`;
|
||||
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
/* eslint-disable react/no-unescaped-entities */
|
||||
import Link from 'next/link';
|
||||
import Meta from '../../components/Meta/Meta';
|
||||
import Layout from '../../layouts/Layout';
|
||||
|
||||
import styles from '../../styles/modules/pages/about/about.module.scss';
|
||||
import Meta from 'src/components/meta/Meta';
|
||||
import Layout from 'src/components/layout';
|
||||
import styles from 'src/styles/modules/pages/about/about.module.scss';
|
||||
|
||||
const About = () => {
|
||||
return (
|
||||
|
@ -91,16 +89,20 @@ 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'>
|
||||
<a
|
||||
href='https://imdb.com/title/tt1049413'
|
||||
className='link'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
imdb.com/title/tt1049413
|
||||
</a>
|
||||
` to `
|
||||
<a
|
||||
href='https://libremdb.iket.me/title/tt1049413'
|
||||
className='link'
|
||||
>
|
||||
libremdb.iket.me/title/tt1049413
|
||||
</a>
|
||||
<Link href='/title/tt1049413'>
|
||||
<a className='link'>
|
||||
{process.env.NEXT_PUBLIC_URL || ''}/title/tt1049413
|
||||
</a>
|
||||
</Link>
|
||||
` . To avoid changing the URLs manually, you can use extensions
|
||||
like{' '}
|
||||
<a
|
||||
|
@ -133,22 +135,21 @@ const About = () => {
|
|||
</details>
|
||||
<details className={styles.faq}>
|
||||
<summary className={styles.faq__summary}>
|
||||
I see connection being made to some Amazon domains.
|
||||
Is content served from third-parties, like Amazon?
|
||||
</summary>
|
||||
<p className={styles.faq__description}>
|
||||
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.
|
||||
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>
|
||||
).
|
||||
</p>
|
||||
</details>
|
||||
<details className={styles.faq}>
|
||||
|
|
7
src/pages/api/[...error].ts
Normal file
7
src/pages/api/[...error].ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
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' });
|
||||
}
|
32
src/pages/api/find.ts
Normal file
32
src/pages/api/find.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
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 });
|
||||
}
|
||||
}
|
23
src/pages/api/list/[listId].ts
Normal file
23
src/pages/api/list/[listId].ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
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 });
|
||||
}
|
||||
}
|
94
src/pages/api/media_proxy.ts
Normal file
94
src/pages/api/media_proxy.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
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,
|
||||
},
|
||||
};
|
22
src/pages/api/name/[nameId].ts
Normal file
22
src/pages/api/name/[nameId].ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
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 });
|
||||
}
|
||||
}
|
21
src/pages/api/title/[titleId].ts
Normal file
21
src/pages/api/title/[titleId].ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
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,7 +1,6 @@
|
|||
import Meta from '../../components/Meta/Meta';
|
||||
import Layout from '../../layouts/Layout';
|
||||
|
||||
import styles from '../../styles/modules/pages/contact/contact.module.scss';
|
||||
import Meta from 'src/components/meta/Meta';
|
||||
import Layout from 'src/components/layout';
|
||||
import styles from 'src/styles/modules/pages/contact/contact.module.scss';
|
||||
|
||||
const Contact = () => {
|
||||
return (
|
||||
|
@ -15,30 +14,50 @@ const Contact = () => {
|
|||
<h1 className={`heading heading__primary ${styles.contact__heading}`}>
|
||||
Contact
|
||||
</h1>
|
||||
|
||||
<div className={styles.list}>
|
||||
<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 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>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
|
|
84
src/pages/find/index.tsx
Normal file
84
src/pages/find/index.tsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
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;
|
54
src/pages/list/[listId]/index.tsx
Normal file
54
src/pages/list/[listId]/index.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
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;
|
67
src/pages/name/[nameId]/index.tsx
Normal file
67
src/pages/name/[nameId]/index.tsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
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 '../../components/Meta/Meta';
|
||||
import Layout from '../../layouts/Layout';
|
||||
|
||||
import styles from '../../styles/modules/pages/privacy/privacy.module.scss';
|
||||
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';
|
||||
|
||||
const Privacy = () => {
|
||||
return (
|
||||
|
@ -16,15 +16,15 @@ const Privacy = () => {
|
|||
Privacy Policy
|
||||
</h1>
|
||||
<div className={styles.list}>
|
||||
<div className={styles.item}>
|
||||
<section className={styles.item}>
|
||||
<h2
|
||||
className={`heading heading__secondary ${styles.item__heading}`}
|
||||
>
|
||||
Information collected
|
||||
</h2>
|
||||
<p className={styles.item__text}>No information is collected.</p>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
</section>
|
||||
<section className={styles.item}>
|
||||
<h2
|
||||
className={`heading heading__secondary ${styles.item__heading}`}
|
||||
>
|
||||
|
@ -40,25 +40,40 @@ const Privacy = () => {
|
|||
prefrences, either turn off JavaScript or disable access to
|
||||
Local Storage for libremdb.
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
</section>
|
||||
<section className={styles.item}>
|
||||
<h2
|
||||
className={`heading heading__secondary ${styles.item__heading}`}
|
||||
>
|
||||
Information collected by other services
|
||||
Instance information
|
||||
</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}>
|
||||
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.
|
||||
Version:
|
||||
<a
|
||||
className='link'
|
||||
href={`https://github.com/zyachel/libremdb/tree/v${packageInfo.version}`}
|
||||
>
|
||||
{packageInfo.version}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer className={styles.metadata}>
|
||||
<p>
|
||||
Last updated on <time>10 september, 2022.</time>
|
||||
Privacy policy last updated on <time>31 october, 2022.</time>
|
||||
</p>
|
||||
<p>
|
||||
You can see the full revision history of this privacy policy on
|
||||
|
|
|
@ -1,34 +1,22 @@
|
|||
// 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'
|
||||
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';
|
||||
|
||||
type Props = { data: Title; error: null } | { error: AppError; data: null }
|
||||
type Props = InferGetServerSidePropsType<typeof getServerSideProps>;
|
||||
|
||||
// TO-DO: make a wrapper page component to display errors, if present in props
|
||||
const TitleInfo = ({ data, error }: Props) => {
|
||||
const router = useRouter()
|
||||
|
||||
if (error)
|
||||
return <ErrorInfo message={error.message} statusCode={error.statusCode} />
|
||||
const TitleInfo = ({ data, error, originalPath }: Props) => {
|
||||
if (error) return <ErrorInfo {...error} originalPath={originalPath} />;
|
||||
|
||||
const info = {
|
||||
meta: data.meta,
|
||||
|
@ -37,55 +25,54 @@ const TitleInfo = ({ data, error }: 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}
|
||||
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)}
|
||||
/>
|
||||
<Head>
|
||||
<meta
|
||||
title="og:image"
|
||||
content={data.basic.poster?.url || '/icon-512.png'}
|
||||
/>
|
||||
</Head>
|
||||
<Layout className={styles.title}>
|
||||
<Layout className={styles.title} originalPath={originalPath}>
|
||||
<Basic data={data.basic} className={styles.basic} />
|
||||
<Media className={styles.media} media={data.media} router={router} />
|
||||
<Media className={styles.media} media={data.media} />
|
||||
<Cast className={styles.cast} cast={data.cast} />
|
||||
<div className={styles.textarea}>
|
||||
<DidYouKnow data={data.didYouKnow} />
|
||||
<Reviews reviews={data.reviews} router={router} />
|
||||
<Reviews reviews={data.reviews} />
|
||||
</div>
|
||||
<Info className={styles.infoarea} info={info} router={router} />
|
||||
<Info className={styles.infoarea} info={info} />
|
||||
<MoreLikeThis className={styles.related} data={data.moreLikeThis} />
|
||||
</Layout>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// TO-DO: make a getServerSideProps wrapper for handling errors
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const titleId = ctx.params!.titleId as string
|
||||
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;
|
||||
|
||||
try {
|
||||
const data = await title(titleId)
|
||||
const data = await getOrSetApiCache(titleKey(titleId), title, titleId);
|
||||
|
||||
return { props: { data, error: null } }
|
||||
return { props: { data, error: null, originalPath } };
|
||||
} 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 } }
|
||||
return { props: { error: { message, statusCode }, data: null, originalPath } };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default TitleInfo
|
||||
export default TitleInfo;
|
||||
|
||||
// could've used getStaticProps instead of getServerSideProps, but meh.
|
||||
/*
|
||||
|
|
|
@ -78,3 +78,21 @@
|
|||
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,19 +22,17 @@ $_light: (
|
|||
// 4.2 for borders, primarily
|
||||
fill-muted: hsl(0, 0%, 80%),
|
||||
// shadows on cards
|
||||
shadow: 0 0 1rem hsla(0, 0%, 0%, 0.2),
|
||||
shadow: 0 0 0.5em 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: (
|
||||
|
@ -54,6 +52,7 @@ $_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,11 +9,17 @@ 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
|
||||
|
@ -22,3 +28,20 @@ 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,4 +24,9 @@
|
|||
@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,6 +7,7 @@ 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);
|
||||
|
|
97
src/styles/modules/components/card/card-basic.module.scss
Normal file
97
src/styles/modules/components/card/card-basic.module.scss
Normal file
|
@ -0,0 +1,97 @@
|
|||
@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