Compare commits

...

52 commits
v2.3.0 ... main

Author SHA1 Message Date
zyachel
410cc70259
Merge pull request #60 from SudoVanilla/patch-1
Add SudoVanilla's Instance
2023-11-25 21:55:53 +00:00
Korbs
258a82f2ac
Add that SudoVanilla's Instance uses Cloudflare 2023-11-25 12:26:47 -05:00
Korbs
e2a335f98d
Add SudoVanilla's Instance 2023-11-25 12:26:16 -05:00
zyachel
246f1155d5 Merge branch 'main' of codeberg.org:zyachel/libremdb into nipos-main 2023-10-30 01:37:55 +05:30
zyachel
19f1700a55 feat(api): add api endpoints for dynamic routes
Squashed commit of the following:

commit 9fdd731136
Author: zyachel <aricla@protonmail.com>
Date:   Mon Oct 30 01:25:32 2023 +0530

    feat(api): add a catch-all route

commit 4dffbbc0ec
Author: zyachel <aricla@protonmail.com>
Date:   Mon Oct 30 01:24:10 2023 +0530

    fix(api): refactor all endpoints a bit

    disallow methods other that GET
    properly type return types
    add type guards where needed
    make all endpoints
    return same response format for consistency

commit 264442448f
Author: Niklas Poslovski <niklas.poslovski@nikisoft.one>
Date:   Sun Oct 29 19:00:44 2023 +0100

    Add API endpoints for all routes that contain IMDB data
2023-10-30 01:34:28 +05:30
zyachel
9fdd731136 feat(api): add a catch-all route 2023-10-30 01:25:32 +05:30
zyachel
4dffbbc0ec fix(api): refactor all endpoints a bit
disallow methods other that GET
properly type return types
add type guards where needed
make all endpoints
return same response format for consistency
2023-10-30 01:24:32 +05:30
Niklas Poslovski
264442448f Add API endpoints for all routes that contain IMDB data 2023-10-29 19:00:44 +01:00
zyachel
2b00d5406a chore(release): 3.2.0 2023-10-29 00:50:34 +05:30
zyachel
97f1432ac5 feat(list): add list route
adds ability to see titles, names, and images lists

closes https://github.com/zyachel/libremdb/issues/6
2023-10-29 00:49:55 +05:30
zyachel
60fb23fc5b refactor(name): remove console statement 2023-10-29 00:49:55 +05:30
zyachel
12eaa741ab refactor: general refactor
make barrel files .ts instead of .tsx
move layouts to components directory
2023-10-29 00:49:51 +05:30
zyachel
40eb8a372b chore(release): 3.1.1 2023-10-14 15:32:18 +05:30
zyachel
e91c313f12 fix(name): fix route crash for some ids
sometimes we don't get genres, causing the crash.

fix https://codeberg.org/zyachel/libremdb/issues/20
2023-10-14 15:22:59 +05:30
zyachel
5fa5e9e2c2 docs(readme): update instances list
add a new instance from @ButteredCats

closes https://github.com/zyachel/libremdb/issues/58
2023-10-03 00:28:53 +05:30
zyachel
27322a4c8c docs(readme): update instances list
remove instance from fascinated.cc as cert is broken
add a new instance by openxng.com

closes https://github.com/zyachel/libremdb/issues/54
closes https://github.com/zyachel/libremdb/issues/56
2023-09-02 21:51:15 +05:30
zyachel
21a1c83d95 fix(title): fix a crash in title route 2023-07-09 19:14:59 +05:30
zyachel
b07cb713d8 docs(instances): update instances list
add a new instance by @toyboatcash, and remove esmail's instance

closes https://github.com/zyachel/libremdb/issues/53, closes
https://github.com/zyachel/libremdb/issues/47
2023-07-09 19:11:26 +05:30
tuxsudo
5628d6b75d
Add libremdb.tux.pizza instance 2023-06-25 19:26:35 +00:00
zyachel
38ed0c6217 fix(name): fix name route crash
this commit fixes a crash in name route caused by upstream

closes https://github.com/zyachel/libremdb/issues/51
2023-06-18 14:31:40 +05:30
zyachel
c610ef4d1b fix(media proxy): fix 304 response code with body error
was accidently sending a 304 with body. introduced in c53c88d
2023-06-03 22:20:56 +05:30
zyachel
736d680243 fix(card): fix long attributes in cards under 'Known For' section
makes the attributes scrollable instead
2023-06-03 22:18:36 +05:30
zyachel
0aea2f47da fix(error): fix incorrect 'view on IMDb' link on error page
the error was due to a faulty logic. 'useRouter' was being used to detect pathname, which doesn't
keep original url on 404 page.
this commit fixes that.
this commit also makes it easy to go to
IMDb by adding a clear link on error page.

closes https://github.com/zyachel/libremdb/issues/50
2023-06-03 22:12:54 +05:30
NoPlagiarism
23eeae3558 Add WhateverItWorks instance 2023-05-26 00:33:25 +05:00
zyachel
bb6405cb05 chore(release): 3.1.0 2023-05-21 18:30:11 +05:30
zyachel
c53c88db9b feat(cache): implement caching of routes 2023-05-21 18:15:03 +05:30
zyachel
8599ae2c5a fix(form): fix hydration error
was due to nested anchor tags
2023-05-21 18:13:44 +05:30
zyachel
8d9b6630a5 fix(name): fix a couple of crashes in name and title route 2023-05-21 18:12:23 +05:30
zyachel
be80244eb3 docs(instances): remove dead instances
this commit removes instances that are either unreachable, or haven't been updated in a long time.

closes https://github.com/zyachel/libremdb/issues/46
2023-05-21 14:40:54 +05:30
zyachel
a0f3ba095a docs(instances): update instances list
add a new instance by @RealFascinated

closes https://github.com/zyachel/libremdb/issues/44
2023-05-07 08:42:25 +05:30
zyachel
11aea1d489 docs(instances): update instances list
add a new instance by nerdyfam.tech

resolves https://github.com/zyachel/libremdb/issues/42#issuecomment-1524052255
2023-04-29 11:30:21 +05:30
zyachel
3ef41d9a6d docs(instances): update instances list
add a new instance by @xbdmHQ

close https://github.com/zyachel/libremdb/issues/43
2023-04-26 22:18:59 +05:30
zyachel
7dea9eac14 build(dependencies): update dependencies and use pnpm v8
this commit also fixes an accidental lockfile mismatch

close https://github.com/zyachel/libremdb/issues/42
2023-04-26 22:12:22 +05:30
zyachel
86737c51ee chore(release): 3.0.0 2023-04-15 21:48:38 +05:30
zyachel
75732e0086 feat(route): add name route
adds much needed route

fix https://github.com/zyachel/libremdb/issues/39, https://github.com/zyachel/libremdb/issues/36,
https://codeberg.org/zyachel/libremdb/issues/11
2023-04-15 21:45:30 +05:30
zyachel
18ca98fd4a refactor: make components more modular
would help in implementing name route

also did some stylistic changes
2023-04-15 20:58:09 +05:30
zyachel
8ce02d0236 fix(title): fix title page crash
due to upstream change in data, title page was broken. this commit fixes thate

BREAKING CHANGE: older versions won't work, at least for title route
2023-04-15 20:49:28 +05:30
Valère
cbce2cac34
docs: Add Hostux intance (#40) 2023-04-09 07:47:16 +00:00
kuanhulio
1eeaab259d
build: harden docker implementation (#38)
* harden docker implementation

* fix caching error

---------

Co-authored-by: Nullnet Services Administrator <admin@nullnet.services>
2023-03-20 08:16:32 +05:30
zyachel
505ff4d839 refactor: replace relative paths with absolute ones in import statements 2023-01-28 22:09:27 +05:30
zyachel
20418b4c1f build(dockerfile): replace yarn with pnpm 2023-01-28 22:08:28 +05:30
zyachel
68072b5f68 ci: use locally generated changelog instead of github actions
this commit replaces the previous workflow of bumping version, generating changelog, and tagging release using github actions with a local one(using
`commit-and-tag-version`)
2023-01-28 22:06:06 +05:30
zyachel
c79dc2a481 docs: compress screenshots 2023-01-28 21:45:41 +05:30
zyachel
4dde7bde77 docs: update instances list
add a new instance, and fill missing info of another one

fix https://github.com/zyachel/libremdb/issues/32
2023-01-28 19:20:29 +05:30
zyachel
2c5d2f86e4 feat: add info related to the current instance
this commit adds additional info about instance like release tag and instance maintainer contact.

fix: https://codeberg.org/zyachel/libremdb/issues/8
2023-01-28 19:06:21 +05:30
Conventional Changelog Action
78b8a9afc3 chore(release): v2.4.0 [skip ci] 2023-01-22 15:49:21 +00:00
zyachel
5cc2ef02ce feat: add error boundary
makes crashes graceful
2023-01-22 21:14:46 +05:30
zyachel
71d1d5b34e fix: fix app crash
this commit fixes a crash due to unavailability of localStorage

https://github.com/zyachel/libremdb/issues/31
2023-01-22 21:13:09 +05:30
Conventional Changelog Action
feffb7d8f6 chore(release): v2.3.1 [skip ci] 2023-01-15 16:58:54 +00:00
zyachel
182b3c1072 docs: add userscript to automatic redirectors list 2023-01-15 22:28:16 +05:30
zyachel
a32785ce00 fix: fix unseekable videos on webkit-based browsers
this commit fixes videos not being able to fast-forward/rewind for lack of http headers and 206
status code

fix https://github.com/zyachel/libremdb/issues/26
2023-01-14 16:36:20 +05:30
zyachel
cfa8c53d11 docs: update instances list
add a new instance by lunar.icu

fix: https://github.com/zyachel/libremdb/issues/28
2022-12-31 22:40:29 +05:30
134 changed files with 5363 additions and 1893 deletions

View file

@ -24,7 +24,20 @@ NEXT_TELEMETRY_DISABLED=1
################################################################################
### 3. REDIS CONFIG(optional if you don't need redis)
################################################################################
## if you want to use redis to speed up the media proxy, set this to true
## 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
# 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=

View file

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

View file

@ -4,5 +4,6 @@
"arrowParens": "avoid",
"semi": true,
"singleQuote": true,
"jsxSingleQuote": true
"jsxSingleQuote": true,
"printWidth": 100
}

36
.versionrc Normal file
View 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
}
]
}

View file

@ -1,3 +1,79 @@
# 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.
## [3.2.0](https://github.com/zyachel/libremdb/compare/v3.1.1...v3.2.0) (2023-10-28)
### Features
* **list:** add list route ([97f1432](https://github.com/zyachel/libremdb/commit/97f1432ac5d23206229d806b7cb3e04af6dec36f))
## [3.1.1](https://github.com/zyachel/libremdb/compare/v3.1.0...v3.1.1) (2023-10-14)
### Bug Fixes
* **card:** fix long attributes in cards under 'Known For' section ([736d680](https://github.com/zyachel/libremdb/commit/736d6802430a3f4f364915f3df93fc548a51ebf1))
* **error:** fix incorrect 'view on IMDb' link on error page ([0aea2f4](https://github.com/zyachel/libremdb/commit/0aea2f47dad6eb78e319ea1abd8c444f2cba4424))
* **media proxy:** fix 304 response code with body error ([c610ef4](https://github.com/zyachel/libremdb/commit/c610ef4d1be39c122715a0eb200155537e7d6abf))
* **name:** fix name route crash ([38ed0c6](https://github.com/zyachel/libremdb/commit/38ed0c62177532b93f61af4172ffa6e5b9995bdc))
* **name:** fix route crash for some ids ([e91c313](https://github.com/zyachel/libremdb/commit/e91c313f127632f1bd44d190af71bc841bbe87b7))
* **title:** fix a crash in title route ([21a1c83](https://github.com/zyachel/libremdb/commit/21a1c83d95b703fa08cdb96c206626f22d5366c9))
## [3.1.0](https://github.com/zyachel/libremdb/compare/v3.0.0...v3.1.0) (2023-05-21)
### Features
* **cache:** implement caching of routes ([c53c88d](https://github.com/zyachel/libremdb/commit/c53c88db9bf98258547e2ca512f864800821cb1f))
### Bug Fixes
* **form:** fix hydration error ([8599ae2](https://github.com/zyachel/libremdb/commit/8599ae2c5ac11f2818f56c9f7de7666a38b4386c))
* **name:** fix a couple of crashes in name and title route ([8d9b663](https://github.com/zyachel/libremdb/commit/8d9b6630a576b7e8331eb5431cd90d02733b4917))
## [3.0.0](https://github.com/zyachel/libremdb/compare/v2.4.0...v3.0.0) (2023-04-15)
### ⚠ BREAKING CHANGES
* **title:** older versions won't work, at least for title route
### Features
* add info related to the current instance ([2c5d2f8](https://github.com/zyachel/libremdb/commit/2c5d2f86e46a52223f07d573b152bad5174ee2d9))
* **route:** add name route ([75732e0](https://github.com/zyachel/libremdb/commit/75732e00869f9777e87e767a48648996345f02f7))
### Bug Fixes
* **title:** fix title page crash ([8ce02d0](https://github.com/zyachel/libremdb/commit/8ce02d02364c8e1f03a8b16594bc20ee6766a8c6))
# [2.4.0](https://github.com/zyachel/libremdb/compare/v2.3.1...v2.4.0) (2023-01-22)
### Bug Fixes
* fix app crash ([71d1d5b](https://github.com/zyachel/libremdb/commit/71d1d5b34e2866729ae0c96c59ea51e8d1a3dcca))
### Features
* add error boundary ([5cc2ef0](https://github.com/zyachel/libremdb/commit/5cc2ef02cec0b31c5d449e189a054fbef5801f60))
## [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)
@ -27,45 +103,3 @@
### Bug Fixes
* **title:** fix site crash ([dd75df0](https://github.com/zyachel/libremdb/commit/dd75df01eb7c03d8945a8bd20ed231a66bd88b8f))
# [2.2.0](https://github.com/zyachel/libremdb/compare/v2.1.0...v2.2.0) (2022-11-13)
### Bug Fixes
* **redis:** fix logs being polluted when redis is disabled ([5fd0d92](https://github.com/zyachel/libremdb/commit/5fd0d9218707797999fe49e256244bb5cb8d2f66))
### Features
* force a certain language when getting data ([1658769](https://github.com/zyachel/libremdb/commit/1658769a30eae5e642c7c2a54aecf88aec4fd274))
# [2.1.0](https://github.com/zyachel/libremdb/compare/v2.0.0...v2.1.0) (2022-11-13)
### Bug Fixes
* bypass response limit for media proxy endpoint ([9bce8a2](https://github.com/zyachel/libremdb/commit/9bce8a2dd50736ee969da783c3b29bfb9fa215f4))
* change to poster for og:image ([261a375](https://github.com/zyachel/libremdb/commit/261a37576b65474ef8867baa622f28a75906f1f2))
* remove "information collected by other services" in privacy ([6ae71d7](https://github.com/zyachel/libremdb/commit/6ae71d7907f3634773d973c7840b4bfb6aa7ea4d))
* remove double space in inspiration credit ([478b459](https://github.com/zyachel/libremdb/commit/478b45977d672e111d0a645f4e429087d869e65e))
### Features
* add "og:image" property for social media embeds ([a2fc232](https://github.com/zyachel/libremdb/commit/a2fc2322a3e668241473d402442435b4df837df8))
* cache media proxy data in redis for 30 mins ([2c8d138](https://github.com/zyachel/libremdb/commit/2c8d138cbd7a9d040d23bbc2d209133d0e15b41b))
* docker support for easy deployment ([b7ee686](https://github.com/zyachel/libremdb/commit/b7ee6863e5536ceb48538fde9a2fc56e2f1535bb))
* fetch images from media proxy on frontend ([dba2ba5](https://github.com/zyachel/libremdb/commit/dba2ba5aa4c04b0cb177ce058257a3a5338e7a21))
* IP ratelimit for media proxy ([720f2b6](https://github.com/zyachel/libremdb/commit/720f2b6acb39fa7f6d1149f79e46c2dbc591af7a))
* make redis cache optional ([7a717aa](https://github.com/zyachel/libremdb/commit/7a717aa212ee1284f1ec377873e232d2717c11c0))
* media proxy for anonymous loads ([59a314b](https://github.com/zyachel/libremdb/commit/59a314b2bd632faa2ceac7e430be381b23547e89))
* proxy videos and add more descriptive error messages ([1983f6b](https://github.com/zyachel/libremdb/commit/1983f6b1fb0380642c6488a0347a7073eea20338))
* update information in FAQ ([44d3a33](https://github.com/zyachel/libremdb/commit/44d3a33fb3366adafd8a629a4b11211bf7479dc8))

View file

@ -4,8 +4,9 @@
FROM node:lts-alpine AS deps
WORKDIR /opt/app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
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
@ -15,13 +16,13 @@ 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 yarn build
RUN pnpm build
# Production image, copy all the files and run next
FROM node:lts-alpine AS runner
FROM gcr.io/distroless/nodejs18-debian11 AS runner
ARG X_TAG
WORKDIR /opt/app
ENV NODE_ENV=production
@ -31,4 +32,4 @@ 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/.bin/next", "start"]
CMD ["./node_modules/next/dist/bin/next", "start"]

View file

@ -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" /> |
---
@ -38,16 +38,22 @@ Inspired by projects like [teddit](https://codeberg.org/teddit/teddit), [nitter]
| 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) | 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) | &mdash; | 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) | US | 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) | US | Operated by [~vern](https://vern.cc) |
| [vernz3ubrntql4wrgyrssd6u3qzi36zrhz2agbo6vibzbs5olk2q.b32.i2p](http://vernz3ubrntql4wrgyrssd6u3qzi36zrhz2agbo6vibzbs5olk2q.b32.i2p) | US | Operated by [~vern](https://vern.cc) |
---
@ -68,6 +74,7 @@ Inspired by projects like [teddit](https://codeberg.org/teddit/teddit), [nitter]
- 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.
@ -103,11 +110,11 @@ 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
- [x] serve images and videos from libremdb itself
@ -171,6 +178,8 @@ There's a [docker image](https://github.com/PussTheCat-org/docker-libremdb-quay)
- [LibRedirect](https://github.com/libredirect/libredirect/)
- [Privacy Redirector](https://github.com/dybdeskarphet/privacy-redirector)
### Similar projects
- [Teddit](https://codeberg.org/teddit/teddit)

View file

@ -3,21 +3,45 @@
version: '3'
services:
frontend:
libremdb:
container_name: libremdb
build:
context: .
network: host
dockerfile: Dockerfile
ports:
- "3000:3000"
env_file: .env.local
env_file: .env.local.example
depends_on:
- redis
- libremdb-redis
restart: always
redis:
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
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:

View file

@ -2,21 +2,14 @@
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
async rewrites() {
return {
afterFiles: [
{
source: '/',
destination: '/find',
},
],
fallback: [
{
source: '/:path*',
destination: '/404',
},
],
};
async redirects() {
return [
{
source: '/',
destination: '/find',
permanent: true,
},
];
},
images: {
domains: ['m.media-amazon.com'],

View file

@ -1,11 +1,15 @@
{
"name": "libremdb",
"version": "2.3.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,11 +19,11 @@
"dependencies": {
"axios": "^0.27.2",
"cheerio": "1.0.0-rc.12",
"ioredis": "^5.2.3",
"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",
@ -27,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"
}
}
}

1396
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

View file

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

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

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

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

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

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

View 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';

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

View file

@ -1,46 +1,69 @@
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';
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.
const ErrorInfo = ({ message = 'Not found, sorry.', statusCode = 404 }) => {
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={`${message} (${statusCode})`}
description="you encountered an error page!"
/>
<Layout className={styles.error}>
<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"
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.
<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>
<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>
<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>
</>
);

View file

@ -1,22 +1,13 @@
import { Companies } from '../../interfaces/shared/search';
import Link from 'next/link';
import { CardResult } from 'src/components/card';
import { Companies } from 'src/interfaces/shared/search';
import styles from '../../styles/modules/components/find/company.module.scss';
type Props = { company: Companies[number] };
type Props = {
company: Companies[0];
};
const Company = ({ company }: Props) => {
return (
<li className={styles.company}>
<Link href={`name/${company.id}`}>
<a className={`heading ${styles.heading}`}>{company.name}</a>
</Link>
{company.country && <p>{company.country}</p>}
{!!company.type && <p>{company.type}</p>}
</li>
);
};
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;

View file

@ -1,21 +1,12 @@
import { Keywords } from '../../interfaces/shared/search';
import Link from 'next/link';
import { CardResult } from 'src/components/card';
import { Keywords } from 'src/interfaces/shared/search';
import styles from '../../styles/modules/components/find/keyword.module.scss';
type Props = { keyword: Keywords[number] };
type Props = {
keyword: Keywords[0];
};
const Keyword = ({ keyword }: Props) => {
return (
<li className={styles.keyword}>
<Link href={`name/${keyword.id}`}>
<a className={`heading ${styles.heading}`}>{keyword.text}</a>
</Link>
{keyword.numTitles && <p>{keyword.numTitles} titles</p>}
</li>
);
};
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;

View file

@ -1,44 +1,19 @@
import { People } from '../../interfaces/shared/search';
import Image from 'next/future/image';
import Link from 'next/link';
import { modifyIMDbImg } from '../../utils/helpers';
import styles from '../../styles/modules/components/find/person.module.scss';
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[0];
};
type Props = { person: People[number] };
const Person = ({ person }: Props) => {
return (
<li className={styles.person}>
<div className={styles.imgContainer} style={{ position: 'relative' }}>
{person.image ? (
<Image
src={modifyIMDbImg(person.image.url, 400)}
alt={person.image.caption}
fill
className={styles.img}
/>
) : (
<svg className={styles.imgNA}>
<use href="/svg/sprite.svg#icon-image-slash" />
</svg>
)}
</div>
<div className={styles.info}>
<Link href={`name/${person.id}`}>
<a className={`heading ${styles.heading}`}>{person.name}</a>
</Link>
{person.aka && <p>{person.aka}</p>}
{person.jobCateogry && <p>{person.jobCateogry}</p>}
{(person.knownForTitle || person.knownInYear) && (
<ul className={styles.basicInfo} aria-label="quick facts">
{person.knownForTitle && <li>{person.knownForTitle}</li>}
{person.knownInYear && <li>{person.knownInYear}</li>}
</ul>
)}
</div>
</li>
<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>
);
};

View file

@ -1,59 +1,36 @@
import { Titles } from '../../interfaces/shared/search';
import Image from 'next/future/image';
import Link from 'next/link';
import { modifyIMDbImg } from '../../utils/helpers';
import { CardResult } from 'src/components/card';
import { Titles } from 'src/interfaces/shared/search';
import styles from 'src/styles/modules/components/find/title.module.scss';
import styles from '../../styles/modules/components/find/title.module.scss';
type Props = {
title: Titles[0];
};
type Props = { title: Titles[number] };
const Title = ({ title }: Props) => {
return (
<li className={styles.title}>
<div className={styles.imgContainer}>
{title.image ? (
<Image
src={modifyIMDbImg(title.image.url, 400)}
alt={title.image.caption}
fill
className={styles.img}
/>
) : (
<svg className={styles.imgNA}>
<use href="/svg/sprite.svg#icon-image-slash" />
</svg>
)}
</div>
<div className={styles.info}>
<Link href={`/title/${title.id}`}>
<a className={`heading ${styles.heading}`}>{title.name}</a>
</Link>
<ul aria-label="quick facts" className={styles.basicInfo}>
{title.type && <li>{title.type}</li>}
{title.sAndE && <li>{title.sAndE}</li>}
{title.releaseYear && <li>{title.releaseYear}</li>}
<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>
{!!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>
)}
</div>
</li>
)}
</CardResult>
);
};

View file

@ -1,11 +1,10 @@
import Find from '../../interfaces/shared/search';
import Company from './Company';
import Person from './Person';
import Title from './Title';
import styles from '../../styles/modules/components/find/results.module.scss';
import Keyword from './Keyword';
import { getResTitleTypeHeading } from '../../utils/helpers';
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;
@ -13,18 +12,16 @@ type Props = {
title: string;
};
const resultsExist = (results: Props['results']) => {
if (
!results ||
(!results.people.length &&
!results.keywords.length &&
!results.companies.length &&
!results.titles.length)
)
return false;
return true;
};
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) => {
@ -35,7 +32,7 @@ const Results = ({ results, className, title }: Props) => {
</h1>
);
const { titles, people, keywords, companies, meta } = results!;
const { titles, people, keywords, companies, meta } = results;
const titlesSectionHeading = getResTitleTypeHeading(
meta.type,
meta.titleType
@ -43,11 +40,11 @@ const Results = ({ results, className, title }: Props) => {
return (
<article className={`${className} ${styles.results}`}>
<h1 className="heading heading__primary">Results for '{title}'</h1>
<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">
<h2 className='heading heading__secondary'>
{titlesSectionHeading}
</h2>
<ul className={styles.titles__list}>
@ -59,7 +56,7 @@ const Results = ({ results, className, title }: Props) => {
)}
{!!people.length && (
<section className={styles.people}>
<h2 className="heading heading__secondary">People</h2>
<h2 className='heading heading__secondary'>People</h2>
<ul className={styles.people__list}>
{people.map(person => (
<Person person={person} key={person.id} />
@ -69,7 +66,7 @@ const Results = ({ results, className, title }: Props) => {
)}
{!!companies.length && (
<section className={styles.people}>
<h2 className="heading heading__secondary">Companies</h2>
<h2 className='heading heading__secondary'>Companies</h2>
<ul className={styles.people__list}>
{companies.map(company => (
<Company company={company} key={company.id} />
@ -79,7 +76,7 @@ const Results = ({ results, className, title }: Props) => {
)}
{!!keywords.length && (
<section className={styles.people}>
<h2 className="heading heading__secondary">Keywords</h2>
<h2 className='heading heading__secondary'>Keywords</h2>
<ul className={styles.people__list}>
{keywords.map(keyword => (
<Keyword keyword={keyword} key={keyword.id} />

View file

@ -1,46 +1,20 @@
import { useRouter } from 'next/router';
import { ChangeEventHandler, FormEventHandler, useRef, useState } from 'react';
import { cleanQueryStr } from '../../../utils/helpers';
import { resultTypes, resultTitleTypes } from '../../../utils/constants/find';
import styles from '../../../styles/modules/components/form/find.module.scss';
import { QueryTypes } from '../../../interfaces/shared/search';
/**
* helper function to render similar radio btns. saves from boilerplate.
* @param data radio btn obj
* @param parentClass class under which radio input and label will be
* @returns JSX array of radios
*/
const renderRadioBtns = (
data: typeof resultTypes | typeof resultTitleTypes,
parentClass: string
) => {
return data.types.map(({ name, val }) => (
<p className={parentClass} 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>
));
};
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;
};
// MAIN FUNCTION
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'. below is the logic for disabling/enabling titleTypes.
// 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;
@ -61,59 +35,58 @@ const Form = ({ className }: Props) => {
const queryStr = cleanQueryStr(entries);
if (query) router.push(`/find?${queryStr}`);
else setIsDisabled(false);
formEl.reset();
};
return (
<form
action="/find"
action='/find'
onSubmit={submitHandler}
ref={formRef}
className={`${className} ${styles.form}`}
>
<p className="heading heading__primary">Search</p>
<p className='heading heading__primary'>Search</p>
<p className={styles.searchbar}>
<svg
className={`icon ${styles.searchbar__icon}`}
focusable="false"
aria-hidden="true"
role="img"
focusable='false'
aria-hidden='true'
role='img'
>
<use href="/svg/sprite.svg#icon-search"></use>
<use href='/svg/sprite.svg#icon-search'></use>
</svg>
<input
id="searchbar"
type="search"
name="q"
placeholder="movies, people..."
id='searchbar'
type='search'
name='q'
placeholder='movies, people...'
className={styles.searchbar__input}
required
minLength={2}
/>
<label className="visually-hidden" htmlFor="searchbar">
<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>
{renderRadioBtns(resultTypes, styles.type)}
<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>
{renderRadioBtns(resultTitleTypes, styles.titleType)}
<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" />
<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}>
<button type='reset' className={styles.button}>
Clear
</button>
<button type="submit" className={styles.button}>
<button type='submit' className={styles.button}>
Submit
</button>
</div>
@ -121,4 +94,28 @@ const Form = ({ className }: Props) => {
);
};
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;

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

View file

@ -1,22 +1,14 @@
import { ReactNode } from 'react';
import { useRouter } from 'next/router';
import Link from 'next/link';
import ThemeToggler from '../components/buttons/ThemeToggler';
import ThemeToggler from 'src/components/buttons/ThemeToggler';
import styles from 'src/styles/modules/layout/header.module.scss';
import styles from '../styles/modules/layout/header.module.scss';
type Props = { full?: boolean; children?: ReactNode };
const Header = (props: Props) => {
const { asPath: path } = useRouter();
type Props = { full?: boolean; originalPath?: string };
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='/'>
<Link href='/find'>
<a aria-label='go to homepage' className={styles.logo}>
<svg className={styles.logo__icon} role='img' aria-hidden>
<use href='/svg/sprite.svg#icon-logo'></use>
@ -24,7 +16,7 @@ const Header = (props: Props) => {
<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}>
@ -46,14 +38,8 @@ const Header = (props: Props) => {
</nav>
)}
<div className={styles.misc}>
<a
href={`https://www.imdb.com${path}`}
target='_blank'
rel='noreferrer'
>
<span className='visually-hidden'>
View on IMDb (opens in new tab)
</span>
<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>
@ -69,7 +55,7 @@ const Header = (props: Props) => {
<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
@ -84,10 +70,7 @@ const Header = (props: Props) => {
nitter
</a>
, and{' '}
<a
href='https://github.com/digitalblossom/alternative-frontends'
className='link'
>
<a href='https://github.com/digitalblossom/alternative-frontends' className='link'>
many others
</a>
.

View file

@ -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>

View 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];

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

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

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

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

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

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

View file

@ -0,0 +1,3 @@
export { default as Data } from './Data';
export { default as Meta } from './Meta';
export { default as Pagination } from './Pagination';

View file

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

View file

@ -1,15 +1,16 @@
import Image from 'next/future/image';
import Link from 'next/link';
import { Media } from '../../interfaces/shared/title';
import { getProxiedIMDbImgUrl, 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;
};
// TODO: refactor this component.
const Media = ({ className, media }: Props) => {
return (
<div className={`${className} ${styles.media}`}>
@ -22,13 +23,9 @@ const Media = ({ className, media }: Props) => {
<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={getProxiedIMDbImgUrl(
modifyIMDbImg(media.trailer.thumbnail)
)}
poster={getProxiedIMDbImgUrl(modifyIMDbImg(media.trailer.thumbnail))}
className={styles.trailer__video}
preload='none'
>
@ -77,9 +74,7 @@ const Media = ({ className, media }: Props) => {
fill
sizes='400px'
/>
<figcaption className={styles.image__caption}>
{image.caption.plainText}
</figcaption>
<figcaption className={styles.image__caption}>{image.caption.plainText}</figcaption>
</figure>
))}
</div>

View file

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

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

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

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

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

View 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>
);

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

View 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';

View file

@ -1,15 +1,9 @@
import { Fragment } from 'react';
import Image from 'next/future/image';
import Link from 'next/link';
import {
formatNumber,
formatTime,
getProxiedIMDbImgUrl,
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;
@ -24,135 +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(${getProxiedIMDbImgUrl(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 && 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>
);
};

View file

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

View file

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

View file

@ -1,9 +1,8 @@
import Link from 'next/link';
import { useRouter } 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 { 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;

View file

@ -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>

View file

@ -1,8 +1,8 @@
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;

View 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';

View file

@ -1,9 +0,0 @@
import Basic from './Basic';
import Cast from './Cast';
import DidYouKnow from './DidYouKnow';
import Info from './Info';
import Media from './Media';
import MoreLikeThis from './MoreLikeThis';
import Reviews from './Reviews';
export { Basic, Cast, DidYouKnow, Info, Media, MoreLikeThis, Reviews };

View file

@ -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();
};

View file

@ -1,4 +1,7 @@
import { ResultMetaTitleTypes, ResultMetaTypes } from '../shared/search';
import {
ResultMetaTitleTypes,
ResultMetaTypes,
} from 'src/interfaces/shared/search';
export default interface RawFind {
props: {

File diff suppressed because it is too large Load diff

View file

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

View file

@ -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'>;

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

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

View 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'];

View file

@ -1,5 +1,5 @@
import cleanFind from '../../utils/cleaners/find';
import { resultTitleTypes, resultTypes } from '../../utils/constants/find';
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 };

View file

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

View file

@ -1,57 +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='/find'>
<a className={className('/find')}>Search</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&nbsp;
<a
className={styles.nav__link}
href='https://www.gnu.org/licenses/agpl-3.0-standalone.html'
>
GNU AGPLv3
</a>
.
</p>
</footer>
);
};
export default Footer;

View file

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

View file

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

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

View file

@ -1,10 +1,9 @@
import type { AppProps } from 'next/app';
import usePageLoading from '../hooks/usePageLoading';
import ProgressBar from '../components/loaders/ProgressBar';
import ThemeProvider from '../context/theme-context';
import '../styles/main.scss';
import { useRouter } from 'next/router';
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) => {
const { isPageLoading, key } = usePageLoading();
@ -12,10 +11,12 @@ const ModifiedApp = ({ Component, pageProps }: AppProps) => {
return (
<ThemeProvider>
{isPageLoading && <ProgressBar />}
<Component
{...pageProps}
key={key} /* passing key to force react to remound components */
/>
<ErrorBoundary>
<Component
{...pageProps}
key={key} /* passing key to force react to remount components */
/>
</ErrorBoundary>
</ThemeProvider>
);
};

View file

@ -5,9 +5,17 @@ import Document, { Html, Head, Main, NextScript } from 'next/document';
const setInitialTheme = `
(() => {
document.documentElement.dataset.js = true;
const isLocalStorageAvailable = () => {
try {
window.localStorage.getItem('test');
return true;
} catch (e) {
return false;
}
}
let theme = 'light';
let themeColor = '#ffe5ef';
const userPrefersTheme = window.localStorage.getItem('theme') || null;
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';

View file

@ -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 (

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

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

View file

@ -1,16 +1,35 @@
import { NextApiRequest, NextApiResponse } from 'next';
import redis from '../../utils/redis';
import axiosInstance from '../../utils/axiosInstance';
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
) {
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))
@ -19,35 +38,39 @@ export default async function handler(
message: 'Invalid query',
});
// 2. sending streamed response if redis isn't enabled
if (redis === null) {
// 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,
});
res.setHeader('Content-Type', mediaRes.headers['content-type']);
// 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(mediaUrl);
const cachedMedia = await redis.getBuffer(mediaKey(mediaUrl));
if (cachedMedia) {
res.setHeader('x-cached', 'true');
res.status(302).send(cachedMedia);
res.send(cachedMedia);
return;
}
// 4. else getting, caching and sending response
const mediaRes = await axiosInstance(mediaUrl, {
const { data } = await axiosInstance(mediaUrl, {
responseType: 'arraybuffer',
});
const data = mediaRes.data;
// saving in redis for 30 minutes
await redis!.setex(mediaUrl, 30 * 60, Buffer.from(data));
await redis.setex(mediaKey(mediaUrl), ttl, Buffer.from(data));
// sending media
res.setHeader('x-cached', 'false');

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

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

View file

@ -1,44 +1,63 @@
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 (
<>
<Meta
title="Contact"
description="Contact page of libremdb, a free & open source IMDb front-end."
title='Contact'
description='Contact page of libremdb, a free & open source IMDb front-end.'
/>
<Layout className="">
<Layout className=''>
<section className={styles.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>

View file

@ -1,22 +1,18 @@
import { GetServerSideProps } from 'next';
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';
import Layout from '../../layouts/Layout';
import ErrorInfo from '../../components/error/ErrorInfo';
import Meta from '../../components/meta/Meta';
import Results from '../../components/find';
import basicSearch from '../../utils/fetchers/basicSearch';
import Form from '../../components/forms/find';
import Find, { FindQueryParams } from '../../interfaces/shared/search';
import { AppError } from '../../interfaces/shared/error';
import { cleanQueryStr } from '../../utils/helpers';
import styles from '../../styles/modules/pages/find/find.module.scss';
type Props =
| { data: { title: string; results: Find }; error: null }
| { data: { title: null; results: null }; error: null }
| { data: { title: string; results: null }; error: AppError };
type Props = InferGetServerSidePropsType<typeof getServerSideProps>;
const getMetadata = (title: string | null) => ({
title: title || 'Search',
@ -25,14 +21,16 @@ const getMetadata = (title: string | null) => ({
: 'Search for anything on libremdb, a free & open source IMDb front-end',
});
const BasicSearch = ({ data: { title, results }, error }: Props) => {
if (error)
return <ErrorInfo message={error.message} statusCode={error.statusCode} />;
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={`${styles.find} ${!title && styles.find__home}`}>
<Layout className={layoutClassName} originalPath={originalPath}>
{title && ( // only showing when user has searched for something
<Results results={results} title={title} className={styles.results} />
)}
@ -43,22 +41,30 @@ const BasicSearch = ({ data: { title, results }, error }: Props) => {
};
// TODO: use generics for passing in queryParams(to components) for better type-checking.
export const getServerSideProps: GetServerSideProps = async ctx => {
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 } };
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 basicSearch(queryStr);
const res = await getOrSetApiCache(findKey(queryStr), basicSearch, queryStr);
return {
props: { data: { title: query, results: res }, error: null },
props: { data: { title: query, results: res }, error: null, originalPath },
};
} catch (error: any) {
const { message, statusCode } = error;
@ -69,6 +75,7 @@ export const getServerSideProps: GetServerSideProps = async ctx => {
props: {
error: { message, statusCode },
data: { title: query, results: null },
originalPath,
},
};
}

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

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

View file

@ -1,14 +1,14 @@
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 (
<>
<Meta
title="Privacy"
description="Privacy policy of libremdb, a free & open source IMDb front-end."
title='Privacy'
description='Privacy policy of libremdb, a free & open source IMDb front-end.'
/>
<Layout className={styles.privacy}>
<section className={styles.policy}>
@ -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,12 +40,40 @@ const Privacy = () => {
prefrences, either turn off JavaScript or disable access to
Local Storage for libremdb.
</p>
</div>
</section>
<section className={styles.item}>
<h2
className={`heading heading__secondary ${styles.item__heading}`}
>
Instance information
</h2>
{process.env.NEXT_PUBLIC_INSTANCE_NAME &&
process.env.NEXT_PUBLIC_INSTANCE_MAIN_URL && (
<p className={styles.item__text}>
Operated by:&nbsp;
<a
className='link'
href={process.env.NEXT_PUBLIC_INSTANCE_MAIN_URL}
>
{process.env.NEXT_PUBLIC_INSTANCE_NAME}
</a>
</p>
)}
<p className={styles.item__text}>
Version:&nbsp;
<a
className='link'
href={`https://github.com/zyachel/libremdb/tree/v${packageInfo.version}`}
>
{packageInfo.version}
</a>
</p>
</section>
</div>
<footer className={styles.metadata}>
<p>
Last updated on <time>31 october, 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

View file

@ -1,34 +1,22 @@
// external
import { GetServerSideProps, GetStaticProps, GetStaticPaths } from 'next'
import Head from 'next/head';
import { useRouter } from 'next/router';
// local
import Meta from '../../../components/meta/Meta';
import Layout from '../../../layouts/Layout';
import ErrorInfo from '../../../components/error/ErrorInfo';
import {
Basic,
Cast,
DidYouKnow,
Info,
Media,
MoreLikeThis,
Reviews,
} from '../../../components/title';
// misc
import Title from '../../../interfaces/shared/title';
import { AppError } from '../../../interfaces/shared/error';
import title from '../../../utils/fetchers/title';
import { getProxiedIMDbImgUrl } from '../../../utils/helpers';
// styles
import styles from '../../../styles/modules/pages/title/title.module.scss';
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) => {
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,
@ -42,22 +30,11 @@ const TitleInfo = ({ data, error }: Props) => {
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
? getProxiedIMDbImgUrl(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} />
<Cast className={styles.cast} cast={data.cast} />
@ -73,23 +50,29 @@ const TitleInfo = ({ data, error }: Props) => {
};
// 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.
/*

View file

@ -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%);

View file

@ -22,17 +22,13 @@ $_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.

View file

@ -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);

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

View file

@ -0,0 +1,44 @@
.item {
height: 100%;
display: grid;
grid-template-rows: minmax(auto, 65%) auto;
text-decoration: none;
color: currentColor;
}
.imgContainer {
justify-self: stretch;
position: relative;
// for icon when image is unavailable
display: grid;
place-items: center;
}
.img {
height: 100%;
object-fit: cover;
}
.imgNA {
fill: var(--clr-fill-muted);
height: 40%;
}
.textContainer {
display: grid;
gap: var(--spacer-1);
padding: var(--spacer-1);
text-align: center;
justify-items: center;
align-content: start;
}
.name {
font-size: 1.2em;
}
.role {
font-size: .95em;
}

View file

@ -0,0 +1,59 @@
@use '../../../abstracts' as helper;
.item {
--width: 10rem;
--height: var(--width);
display: grid;
grid-template-columns: var(--width) auto;
@include helper.bp('bp-450') {
--height: 15rem;
grid-template-columns: auto;
}
}
.sansImage {
grid-template-columns: auto;
padding: var(--spacer-1);
.imgContainer {
display: none;
}
}
.imgContainer {
display: grid;
place-items: center;
min-height: var(--height);
position: relative;
}
.img {
object-fit: cover;
object-position: center 25%; // most of the time, person's face is visible at 1/4 of height in a potrait image.
}
.imgNA {
width: 80%;
fill: var(--clr-fill-muted);
}
.info {
display: grid;
padding: var(--spacer-3);
gap: var(--spacer-0);
@include helper.bp('bp-450') {
padding: var(--spacer-1);
}
& :empty {
display: none;
}
}
.heading {
font-size: var(--fs-4);
text-decoration: none;
}

View file

@ -0,0 +1,62 @@
.item {
height: 100%;
display: grid;
grid-template-rows: minmax(auto, 65%) auto;
text-decoration: none;
color: currentColor;
}
.imgContainer {
justify-self: stretch;
position: relative;
// for icon when image is unavailable
display: grid;
place-items: center;
}
.img {
height: 100%;
object-fit: cover;
}
.imgNA {
fill: var(--clr-fill-muted);
height: 40%;
}
.textContainer {
display: grid;
gap: var(--spacer-1);
padding: var(--spacer-1);
text-align: center;
justify-items: center;
align-content: start;
}
.children {
max-height: 7em; // firefox doesn't support lh yet.
max-height: 7lh;
overflow: auto;
}
.name {
font-size: 1.2em;
}
.rating {
display: flex;
align-items: center;
gap: var(--spacer-0);
line-height: 1;
flex-wrap: wrap;
justify-content: center;
}
.ratingIcon {
--dim: 1em;
height: var(--dim);
width: var(--dim);
fill: var(--clr-fill);
}

View file

@ -0,0 +1,11 @@
.card {
overflow: hidden;
border-radius: 5px;
background-color: var(--clr-bg-accent);
box-shadow: var(--clr-shadow);
}
.hoverable:hover,
.hoverable:focus-within {
background-color: var(--clr-bg-muted);
}

View file

@ -28,6 +28,16 @@
}
.heading {
// justify-self: center;
text-align: center;
}
.button {
align-self: end;
font: inherit;
cursor: pointer;
border: none;
background: transparent;
color: var(--clr-link);
border-bottom: 2px solid var(--clr-link);
}

View file

@ -1,13 +0,0 @@
.company {
background: var(--clr-bg-accent);
box-shadow: var(--clr-shadow);
border-radius: 5px;
display: grid;
padding: var(--spacer-3);
gap: var(--spacer-0);
}
.heading {
font-size: var(--fs-4);
text-decoration: none;
}

View file

@ -1,13 +0,0 @@
.keyword {
background: var(--clr-bg-accent);
box-shadow: var(--clr-shadow);
border-radius: 5px;
display: grid;
padding: var(--spacer-3);
gap: var(--spacer-0);
}
.heading {
font-size: var(--fs-4);
text-decoration: none;
}

View file

@ -1,57 +1,4 @@
@use '../../../abstracts' as helper;
.person {
--width: 10rem;
--height: var(--width);
background: var(--clr-bg-accent);
box-shadow: var(--clr-shadow);
border-radius: 5px;
overflow: hidden; // for background image
display: grid;
grid-template-columns: var(--width) auto;
@include helper.bp('bp-450') {
--height: 15rem;
grid-template-columns: auto;
}
}
.imgContainer {
display: grid;
place-items: center;
min-height: var(--height);
}
.img {
object-fit: cover;
object-position: center 25%; // most of the time, person's face is visible at 1/4 of height in a potrait image.
}
.imgNA {
width: 80%;
fill: var(--clr-fill-muted);
}
.info {
display: grid;
padding: var(--spacer-3);
gap: var(--spacer-0);
@include helper.bp('bp-450') {
padding: var(--spacer-1);
}
}
.heading {
font-size: var(--fs-4);
text-decoration: none;
}
.basicInfo, .seriesInfo {
.basicInfo {
display: flex;
list-style: none;
flex-wrap: wrap;
@ -64,11 +11,3 @@
font-size: var(--fs-5);
}
}
.stars {
span {
font-weight: var(--fw-bold);
}
}

Some files were not shown because too many files have changed in this diff Show more