Compare commits

..

No commits in common. "main" and "v2.0.0" have entirely different histories.
main ... v2.0.0

146 changed files with 1623 additions and 6552 deletions

View file

@ -1,43 +1,9 @@
################################################################################
### PLEASE FILL/ENABLE REQUIRED VARS AT LEAST BEFORE RUNNING THE APPLICATION ###
################################################################################
################################################################################
### 1. REQUIRED VARS(site may not work as expected without these).
################################################################################
## used for meta tags. e.g: 'https://libremdb.iket.me'. don't add end slash.
# required fields
# used for meta tags. e.g: 'https://libremdb.iket.me' don't add end slash.
NEXT_PUBLIC_URL=
## used when fetching data from IMDb. not adding these could result in not getting any response.
## example useragent header: 'Mozilla/5.0 (X11; Linux x86_64; rv:108.0) Gecko/20100101 Firefox/108.0'
AXIOS_USERAGENT=
## example accept header: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8'
AXIOS_ACCEPT=
################################################################################
### 2. OPTIONAL VARS(enabling these is encouraged)
################################################################################
## for forcing a certain language for data we get from imdb. Useful when you don't want your IP to determine the preferred language.
# AXIOS_LANGUAGE='en-US,en;q=0.5'
## comment it out if you wish to enable nextjs stats collection. more at https://nextjs.org/telemetry
NEXT_TELEMETRY_DISABLED=1
################################################################################
### 3. REDIS CONFIG(optional if you don't need redis)
################################################################################
## enables caching of api routes as well as media
# USE_REDIS=true
## in case you don't want to cache media but only api routes
# USE_REDIS_FOR_API_ONLY=true
## ttl for media and api
# REDIS_CACHE_TTL_API=3600
# REDIS_CACHE_TTL_MEDIA=3600
## for docker, just set the domain to the container name, default is 'libremdb_redis'
# REDIS_URL=localhost:6379
################################################################################
### 4. INSTANCE META FIELDS(not required but good to have)
################################################################################
## example: 'https://iket.me'.
NEXT_PUBLIC_INSTANCE_MAIN_URL=
## eg: 'zyachel'
NEXT_PUBLIC_INSTANCE_NAME=
# optional fields. uncomment them and add the values if you wish so.
# default useragent for requesting data from imdb is 'axios/0.27.2'
# AXIOS_USERAGENT=
# default accept header is 'application/json, text/plain, */*'
# AXIOS_ACCEPT=

29
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,29 @@
name: release
on:
push:
branches:
- main
- next
jobs:
changelog-and-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: generate changelog and bump version
id: changelog
uses: TriPSs/conventional-changelog-action@v3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: create release
uses: actions/create-release@v1
if: ${{ steps.changelog.outputs.skipped == 'false' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.changelog.outputs.tag }}
release_name: ${{ steps.changelog.outputs.tag }}
body: ${{ steps.changelog.outputs.clean_changelog }}

11
.gitignore vendored
View file

@ -28,15 +28,8 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
next-env.d.ts
#just dev stuff
dev/*
# other lockfiles
yarn.lock
package-lock.json
# docker
docker-compose.yml
dump.rdb
yarn.lock

View file

@ -1,9 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"arrowParens": "avoid",
"semi": true,
"singleQuote": true,
"jsxSingleQuote": true,
"printWidth": 100
}
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": true
}

View file

@ -1,36 +0,0 @@
{
"types": [
{
"type": "feat",
"section": "Features"
},
{
"type": "fix",
"section": "Bug Fixes"
},
{
"type": "chore",
"hidden": true
},
{
"type": "docs",
"hidden": true
},
{
"type": "style",
"hidden": true
},
{
"type": "refactor",
"hidden": true
},
{
"type": "perf",
"hidden": true
},
{
"type": "test",
"hidden": true
}
]
}

View file

@ -1,105 +1,54 @@
# Changelog
# [2.0.0](https://github.com/zyachel/libremdb/compare/v0.1.2...v2.0.0) (2022-10-31)
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
## [3.2.0](https://github.com/zyachel/libremdb/compare/v3.1.1...v3.2.0) (2023-10-28)
### Bug Fixes
* change to poster for og:image ([f207d68](https://github.com/zyachel/libremdb/commit/f207d688e2dc0d6c12a0b6e8f6ddc7b0eadf5e0b))
* remove double space in inspiration credit ([3f987b5](https://github.com/zyachel/libremdb/commit/3f987b59dcadbb5f931dda4d510b4c13a4ed5cd0))
### Features
* **list:** add list route ([97f1432](https://github.com/zyachel/libremdb/commit/97f1432ac5d23206229d806b7cb3e04af6dec36f))
* add "og:image" property for social media embeds ([d152cf4](https://github.com/zyachel/libremdb/commit/d152cf4b6210b3dd5eb33274d05695bd5593cd06))
* major rewrite ([9891204](https://github.com/zyachel/libremdb/commit/9891204f5a11eb24ad7c924f50f0e069589b82ff))
## [3.1.1](https://github.com/zyachel/libremdb/compare/v3.1.0...v3.1.1) (2023-10-14)
### BREAKING CHANGES
* the whole application is rewritten from scratch.
## [0.1.2](https://github.com/zyachel/libremdb/compare/v0.1.1...v0.1.2) (2022-06-06)
### Bug Fixes
* **card:** fix long attributes in cards under 'Known For' section ([736d680](https://github.com/zyachel/libremdb/commit/736d6802430a3f4f364915f3df93fc548a51ebf1))
* **error:** fix incorrect 'view on IMDb' link on error page ([0aea2f4](https://github.com/zyachel/libremdb/commit/0aea2f47dad6eb78e319ea1abd8c444f2cba4424))
* **media proxy:** fix 304 response code with body error ([c610ef4](https://github.com/zyachel/libremdb/commit/c610ef4d1be39c122715a0eb200155537e7d6abf))
* **name:** fix name route crash ([38ed0c6](https://github.com/zyachel/libremdb/commit/38ed0c62177532b93f61af4172ffa6e5b9995bdc))
* **name:** fix route crash for some ids ([e91c313](https://github.com/zyachel/libremdb/commit/e91c313f127632f1bd44d190af71bc841bbe87b7))
* **title:** fix a crash in title route ([21a1c83](https://github.com/zyachel/libremdb/commit/21a1c83d95b703fa08cdb96c206626f22d5366c9))
* change the order in which env vars are loaded ([55c0eba](https://github.com/zyachel/libremdb/commit/55c0eba6e47c85654242173796e76205328f5f31))
* **robots.txt:** disallow all robots ([f39998d](https://github.com/zyachel/libremdb/commit/f39998d57bd2531fd1bd8b21e32ca563baf7565c))
## [3.1.0](https://github.com/zyachel/libremdb/compare/v3.0.0...v3.1.0) (2023-05-21)
### Performance Improvements
* implement caching of static files ([170fbab](https://github.com/zyachel/libremdb/commit/170fbabe5ef4b8cec63ca8831a4ae2a79798a6b0))
## [0.1.1](https://github.com/zyachel/libremdb/compare/v0.1.0...v0.1.1) (2022-06-03)
### Bug Fixes
* typo in URL ([#2](https://github.com/zyachel/libremdb/issues/2)) ([9f35a66](https://github.com/zyachel/libremdb/commit/9f35a668b508d79353da5db70014d99094788d5a))
# [0.1.0](https://github.com/zyachel/libremdb/compare/30dac07ba33dbe4331a5c9fa6cd2c332100868df...v0.1.0) (2022-05-21)
### Features
* **cache:** implement caching of routes ([c53c88d](https://github.com/zyachel/libremdb/commit/c53c88db9bf98258547e2ca512f864800821cb1f))
### Bug Fixes
* **form:** fix hydration error ([8599ae2](https://github.com/zyachel/libremdb/commit/8599ae2c5ac11f2818f56c9f7de7666a38b4386c))
* **name:** fix a couple of crashes in name and title route ([8d9b663](https://github.com/zyachel/libremdb/commit/8d9b6630a576b7e8331eb5431cd90d02733b4917))
## [3.0.0](https://github.com/zyachel/libremdb/compare/v2.4.0...v3.0.0) (2023-04-15)
### ⚠ BREAKING CHANGES
* **title:** older versions won't work, at least for title route
### Features
* add info related to the current instance ([2c5d2f8](https://github.com/zyachel/libremdb/commit/2c5d2f86e46a52223f07d573b152bad5174ee2d9))
* **route:** add name route ([75732e0](https://github.com/zyachel/libremdb/commit/75732e00869f9777e87e767a48648996345f02f7))
### Bug Fixes
* **title:** fix title page crash ([8ce02d0](https://github.com/zyachel/libremdb/commit/8ce02d02364c8e1f03a8b16594bc20ee6766a8c6))
# [2.4.0](https://github.com/zyachel/libremdb/compare/v2.3.1...v2.4.0) (2023-01-22)
### Bug Fixes
* fix app crash ([71d1d5b](https://github.com/zyachel/libremdb/commit/71d1d5b34e2866729ae0c96c59ea51e8d1a3dcca))
### Features
* add error boundary ([5cc2ef0](https://github.com/zyachel/libremdb/commit/5cc2ef02cec0b31c5d449e189a054fbef5801f60))
* add review section ([30dac07](https://github.com/zyachel/libremdb/commit/30dac07ba33dbe4331a5c9fa6cd2c332100868df))
## [2.3.1](https://github.com/zyachel/libremdb/compare/v2.3.0...v2.3.1) (2023-01-15)
### Bug Fixes
* fix unseekable videos on webkit-based browsers ([a32785c](https://github.com/zyachel/libremdb/commit/a32785ce00b638e9079f0924fd9b00f98c077348))
# [2.3.0](https://github.com/zyachel/libremdb/compare/v2.2.2...v2.3.0) (2022-12-31)
### Bug Fixes
* couple css improvements for webkit-based browsers ([81eaf2f](https://github.com/zyachel/libremdb/commit/81eaf2fd5e5980c0c4d59a8805cf541fa8fe51f9))
### Features
* **search:** add basic search functionality ([0cff34a](https://github.com/zyachel/libremdb/commit/0cff34a766b09ba17be2a89f6290889dbf225436))
## [2.2.2](https://github.com/zyachel/libremdb/compare/v2.2.1...v2.2.2) (2022-12-10)
### Bug Fixes
* app crash on qutebrowser ([78b14ec](https://github.com/zyachel/libremdb/commit/78b14ec07955d29403b8b5ae0d449f38eea2bbc5))
## [2.2.1](https://github.com/zyachel/libremdb/compare/v2.2.0...v2.2.1) (2022-12-01)
### Bug Fixes
* **title:** fix site crash ([dd75df0](https://github.com/zyachel/libremdb/commit/dd75df01eb7c03d8945a8bd20ed231a66bd88b8f))

View file

@ -1,35 +0,0 @@
# Thanks @yordis on Github! https://github.com/vercel/next.js/discussions/16995#discussioncomment-132339
# Install dependencies only when needed
FROM node:lts-alpine AS deps
WORKDIR /opt/app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm
RUN pnpm install --frozen-lockfile
# Rebuild the source code only when needed
# This is where because may be the case that you would try
# to build the app based on some `X_TAG` in my case (Git commit hash)
# but the code hasn't changed.
FROM node:lts-alpine AS builder
ENV NODE_ENV=production
WORKDIR /opt/app
RUN npm install -g pnpm
COPY . .
COPY --from=deps /opt/app/node_modules ./node_modules
RUN pnpm build
# Production image, copy all the files and run next
FROM gcr.io/distroless/nodejs18-debian11 AS runner
ARG X_TAG
WORKDIR /opt/app
ENV NODE_ENV=production
COPY --from=builder /opt/app/next.config.mjs ./
COPY --from=builder /opt/app/public ./public
COPY --from=builder /opt/app/.next ./.next
COPY --from=builder /opt/app/node_modules ./node_modules
ENV HOST=0.0.0.0
ENV PORT=3000
CMD ["./node_modules/next/dist/bin/next", "start"]

View file

@ -6,7 +6,7 @@ Inspired by projects like [teddit](https://codeberg.org/teddit/teddit), [nitter]
| | |
| -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| <img src="./public/img/misc/preview.jpg" title="screenshot (desktop screen, light mode)" width="1500" /> | <img src="./public/img/misc/preview2.jpg" title="screenshot (mobile screen, dark mode)" width="400" /> |
| <img src="./public/img/misc/preview.png" title="screenshot (desktop screen, light mode)" width="1500" /> | <img src="./public/img/misc/preview2.png" title="screenshot (mobile screen, dark mode)" width="385" /> |
---
@ -32,28 +32,23 @@ Inspired by projects like [teddit](https://codeberg.org/teddit/teddit), [nitter]
## Instances
### Clearnet
<!-- prettier-ignore -->
| Instance URL | Region | Notes |
| ------------ | ------ | ----- |
| 1. Clearnet | | |
| [libremdb.iket.me](https://libremdb.iket.me) | Canada | Operated by me |
| [libremdb.pussthecat.org](https://libremdb.pussthecat.org) | Germany | Operated by [PussTheCat.org](https://pussthecat.org/) |
| [ld.vern.cc](https://ld.vern.cc) | US | Operated by [~vern](https://vern.cc) |
| [binge.whatever.social](https://binge.whatever.social) | US & Germany | Operated by [Whatever Social](https://whatever.social) |
| [libremdb.lunar.icu](https://libremdb.lunar.icu) | Germany (Cloudflare) | Operated by [lunar.icu](https://lunar.icu/) |
| [libremdb.jeikobu.net](https://libremdb.jeikobu.net) | Germany (Cloudflare) | Operated by [shindouj](https://github.com/shindouj) |
| [lmdb.hostux.net](https://lmdb.hostux.net) | France | Operated by [Hostux.net](https://hostux.net) |
| [binge.whateveritworks.org](https://binge.whateveritworks.org) | Germany (Cloudflare) | Operated by [WhateverItWorks](https://github.com/WhateverItWorks) |
| [libremdb.nerdyfam.tech](https://libremdb.nerdyfam.tech) | US | Operated by [Nerdyfam.tech](https://nerdyfam.tech/) |
| [libremdb.tux.pizza](https://libremdb.tux.pizza) | US | Operated by [tux.pizza](https://tux.pizza) |
| [libremdb.frontendfriendly.xyz](https://libremdb.frontendfriendly.xyz) | &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/)
| [libremdbeu.herokuapp.com](https://libremdbeu.herokuapp.com) | Europe | Operated by [toyboatcash](https://github.com/toyboatcash) |
| [lmdb.tokhmi.xyz](https://lmdb.tokhmi.xyz) | U.S. | Operated by [Tokhmi](https://tokhmi.xyz) |
| [libremdb.esmailelbob.xyz](https://libremdb.esmailelbob.xyz) | Canada | Operated by [Esmail EL BoB](https://esmailelbob.xyz) |
| [ld.vern.cc](https://ld.vern.cc) | Canada | Operated by [~vern](https://vern.cc) |
| 2. Onion | | |
| [ld.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion](http://ld.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion) | US | Operated by [~vern](https://vern.cc) |
| [libremdb.esmail5pdn24shtvieloeedh7ehz3nrwcdivnfhfcedl7gf4kwddhkqd.onion](http://libremdb.esmail5pdn24shtvieloeedh7ehz3nrwcdivnfhfcedl7gf4kwddhkqd.onion) | Canada | Operated by [Esmail EL BoB](https://esmailelbob.xyz) |
| [ld.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion](http://ld.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion) | Canada | Operated by [~vern](https://vern.cc) |
| 3. I2P | | |
| [vernz3ubrntql4wrgyrssd6u3qzi36zrhz2agbo6vibzbs5olk2q.b32.i2p](http://vernz3ubrntql4wrgyrssd6u3qzi36zrhz2agbo6vibzbs5olk2q.b32.i2p) | US | Operated by [~vern](https://vern.cc) |
| [vernz3ubrntql4wrgyrssd6u3qzi36zrhz2agbo6vibzbs5olk2q.b32.i2p](http://vernz3ubrntql4wrgyrssd6u3qzi36zrhz2agbo6vibzbs5olk2q.b32.i2p) | Canada | Operated by [~vern](https://vern.cc) |
---
@ -69,12 +64,15 @@ Inspired by projects like [teddit](https://codeberg.org/teddit/teddit), [nitter]
- It doesn't have all routes.
I'll implement more with time :)
- Is content served from third-parties, like Amazon?
Nope, libremdb proxies all image and video requests through the instance to avoid exposing your IP address, browser information and other personally identifiable metadata ([Contributor](https://github.com/httpjamesm)).
- I see connection being made to some Amazon domains.
For now, images and videos are directly served from Amazon. If I have enough time in the future, I'll implement a way to serve the images from libremdb instead.
- Will Amazon track me then?
They may log your IP address, useragent, and other such
identifiers. I'd recommend using a VPN, or accessing the website through TOR for mitigating this risk.
- Why not just use IMDb?
Refer to the [features section](#some-features) above.
- Why didn't you use other databases like [TMDB](https://www.themoviedb.org/) or [OMDb](https://www.omdbapi.com/)?
IMDb simply has superior dataset compared to all other alternatives. With that being said, I'd encourage you to check out those alternatives too.
@ -89,7 +87,7 @@ Inspired by projects like [teddit](https://codeberg.org/teddit/teddit), [nitter]
A key named 'theme' is stored in Local Storage provided by your browser, if you ever override the default theme. To remove it, go to site data settings, and clear the data for this website. To permamently disable libremdb from storing your theme prefrences, either turn off JavaScript or disable access to Local Storage for libremdb.
- Information collected by other services:
None. libremdb proxies images anonymously through the instance for maximum privacy ([Contributor](https://github.com/httpjamesm)).
libremdb connects to 'media-amazon.com' and 'media-imdb.com' for fetching images and videos. So, Amazon might log your IP address, and other information(such as http headers) sent by your browser.
---
@ -110,13 +108,13 @@ Inspired by projects like [teddit](https://codeberg.org/teddit/teddit), [nitter]
- [ ] lists
- [ ] moviemeter
- [x] person info(includes directors and actors)
- [ ] person info(includes directors and actors)
- [ ] company info
- [ ] user info
- [X] use redis, or any other caching strategy
- [ ] use redis, or any other caching strategy
- [x] implement a better installation method
- [x] serve images and videos from libremdb itself
- [ ] serve images and videos from libremdb itself
---
@ -130,36 +128,27 @@ As libremdb is made with Next.js, you can deploy it anywhere where Next.js is su
for Node.js, visit [their website](https://nodejs.org/en/).
for Git, run `sudo apt install git` if you're on a Debian-based distro. Else visit [their website](https://git-scm.com/).
2. Install redis(optional).
You can install redis from [here](https://redis.io).
3. Clone and set up the repo.
2. Clone and set up the repo.
```bash
git clone https://github.com/zyachel/libremdb.git # replace github.com with codeberg.org if you wish so.
cd libremdb
# change the configuration file to your liking.
# optional configuration
cp .env.local.example .env.local
# replace 'pnpm' with yarn or npm if you use those.
# replace 'pnpm' with yarn or npm if you use those
pnpm install
pnpm build
pnpm start
# optional: if you're using redis
redis-server
```
libremdb will start running at http://localhost:3000.
To change port, modify the last command like this: `pnpm start -- -p <port-number>`.
### Docker (Local)
### Docker
You can build the docker image using the provided Dockerfile(thanks to [@httpjamesm](https://github.com/httpjamesm)) and set it up using the [example docker-compose file](./docker-compose.example.yml).
There's a [docker image](https://github.com/PussTheCat-org/docker-libremdb-quay) made by [@TheFrenchGhosty](https://github.com/TheFrenchGhosty) for [PussTheCat.org's instance](https://libremdb.pussthecat.org). You can use that in case you wish to use docker.
Change the docker-compose file to your liking and run `docker-compose up -d` to start the container, that's all!
### Docker (Built)
There's a [docker image](https://github.com/PussTheCat-org/docker-libremdb-quay) made by [@TheFrenchGhosty](https://github.com/TheFrenchGhosty) for [PussTheCat.org's instance](https://libremdb.pussthecat.org). You can use that as well.
---
## Miscellaneous
@ -171,15 +160,13 @@ There's a [docker image](https://github.com/PussTheCat-org/docker-libremdb-quay)
```
Description: redirect IMDb to libremdb
Example URL: https://www.imdb.com/title/tt0258463/?ref_=tt_sims_tt_t_4
Include pattern: https?:\/\/(www\.)?imdb\.com\/(.*)
Include pattern: https?:\/\/(www\.)?imdb\.com\/([^\?]*)
Redirect to: https://libremdb.iket.me/$2
Pattern type: Regular Expression
```
- [LibRedirect](https://github.com/libredirect/libredirect/)
- [Privacy Redirector](https://github.com/dybdeskarphet/privacy-redirector)
### Similar projects
- [Teddit](https://codeberg.org/teddit/teddit)

View file

@ -1,47 +0,0 @@
# docker-compose.yml
version: '3'
services:
libremdb:
container_name: libremdb
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
env_file: .env.local.example
depends_on:
- libremdb-redis
restart: always
user: 65534:65534 # equivalent to the nobody user
read_only: true
tmpfs:
- /opt/app/.next/cache/:size=10M,mode=0770,uid=65534,gid=65534,noexec,nosuid,nodev
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
networks:
- libremdb
libremdb-redis:
container_name: libremdb_redis
image: redis
# FOR DEBUGGING ONLY
# ports:
# - "6379:6379"
restart: always
user: nobody
read_only: true
security_opt:
- no-new-privileges:true
tmpfs:
- /data:size=10M,mode=0770,uid=65534,gid=65534,noexec,nosuid,nodev
cap_drop:
- ALL
networks:
- libremdb
networks:
libremdb:

5
next-env.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View file

@ -6,7 +6,7 @@ const nextConfig = {
return [
{
source: '/',
destination: '/find',
destination: '/about',
permanent: true,
},
];
@ -20,7 +20,6 @@ const nextConfig = {
},
isrMemoryCacheSize: 20 * 1024 * 1024,
},
poweredByHeader: false,
};
export default nextConfig;

View file

@ -1,15 +1,11 @@
{
"name": "libremdb",
"version": "3.2.0",
"version": "2.0.0",
"description": "a free & open source IMDb front-end",
"private": true,
"type": "module",
"author": "libremdb-contributors",
"license": "AGPL-3.0-or-later",
"repository": {
"type": "git",
"url": "https://github.com/zyachel/libremdb/"
},
"scripts": {
"dev": "next dev",
"build": "next build",
@ -19,11 +15,10 @@
"dependencies": {
"axios": "^0.27.2",
"cheerio": "1.0.0-rc.12",
"ioredis": "^5.3.2",
"next": "12.2.5",
"react": "18.2.0",
"react-dom": "18.2.0",
"sharp": "^0.31.3"
"sharp": "^0.31.0"
},
"devDependencies": {
"@types/node": "18.7.3",
@ -31,11 +26,11 @@
"@types/react-dom": "18.0.6",
"eslint": "8.22.0",
"eslint-config-next": "12.2.5",
"sass": "^1.62.1",
"sass": "^1.54.4",
"typescript": "4.7.4"
},
"engines": {
"node": ">=16.5.0",
"pnpm": ">=8.0.0"
"pnpm": ">=7.0.0"
}
}
}

1433
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

BIN
public/img/misc/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View file

@ -15,19 +15,42 @@
</symbol>
<!--miscellaneous logos-->
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-cancel">
<path d="M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256s256-114.6 256-256S397.4 0 256 0zM64 256c0-41.4 13.3-79.68 35.68-111.1l267.4 267.4C335.7 434.7 297.4 448 256 448C150.1 448 64 361.9 64 256zM412.3 367.1L144.9 99.68C176.3 77.3 214.6 64 256 64c105.9 0 192 86.13 192 192C448 297.4 434.7 335.7 412.3 367.1z"></path>
</symbol>
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" id="icon-code-document">
<path d="M162.1 257.8c-7.812-7.812-20.47-7.812-28.28 0l-48 48c-7.812 7.812-7.812 20.5 0 28.31l48 48C137.8 386.1 142.9 388 148 388s10.23-1.938 14.14-5.844c7.812-7.812 7.812-20.5 0-28.31L128.3 320l33.86-33.84C169.1 278.3 169.1 265.7 162.1 257.8zM365.3 93.38l-74.63-74.64C278.6 6.742 262.3 0 245.4 0H64C28.65 0 0 28.65 0 64l.0065 384c0 35.34 28.65 64 64 64H320c35.2 0 64-28.8 64-64V138.6C384 121.7 377.3 105.4 365.3 93.38zM336 448c0 8.836-7.164 16-16 16H64.02c-8.838 0-16-7.164-16-16L48 64.13c0-8.836 7.164-16 16-16h160L224 128c0 17.67 14.33 32 32 32h79.1V448zM221.9 257.8c-7.812 7.812-7.812 20.5 0 28.31L255.7 320l-33.86 33.84c-7.812 7.812-7.812 20.5 0 28.31C225.8 386.1 230.9 388 236 388s10.23-1.938 14.14-5.844l48-48c7.812-7.812 7.812-20.5 0-28.31l-48-48C242.3 250 229.7 250 221.9 257.8z"></path>
</symbol>
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" id="icon-computer-home">
<path d="M218.3 8.486C230.6-2.829 249.4-2.829 261.7 8.486L469.7 200.5C476.4 206.7 480 215.2 480 224H336C316.9 224 299.7 232.4 288 245.7V208C288 199.2 280.8 192 272 192H208C199.2 192 192 199.2 192 208V272C192 280.8 199.2 288 208 288H271.1V416H112C85.49 416 64 394.5 64 368V256H32C18.83 256 6.996 247.9 2.198 235.7C-2.6 223.4 .6145 209.4 10.3 200.5L218.3 8.486zM336 256H560C577.7 256 592 270.3 592 288V448H624C632.8 448 640 455.2 640 464C640 490.5 618.5 512 592 512H303.1C277.5 512 255.1 490.5 255.1 464C255.1 455.2 263.2 448 271.1 448H303.1V288C303.1 270.3 318.3 256 336 256zM352 304V448H544V304H352z"></path>
</symbol>
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" id="icon-link-slash">
<path d="M485.1 354.9l113.5-113.5c55.21-55.21 55.21-144.7 0-199.9C570.1 13.8 534.8 0 498.6 0s-72.36 13.8-99.96 41.41l-43.36 43.36c15.11 8.012 29.47 17.58 41.91 30.02c3.146 3.146 5.898 6.518 8.742 9.838l37.96-37.96C458.5 72.05 477.1 64 498.6 64s40.1 8.047 54.71 22.66c14.61 14.61 22.66 34.04 22.66 54.71s-8.049 40.1-22.66 54.71l-119 119l-30.09-23.59c21.49-51.28 12.12-112.4-29.63-154.1C346.1 109.8 310.8 96 274.6 96c-29.6 0-58.93 9.752-83.83 28.23L38.81 5.109C34.41 1.672 29.19 0 24.03 0c-7.125 0-14.19 3.156-18.91 9.187c-8.188 10.44-6.375 25.53 4.062 33.7l591.1 463.1c10.5 8.203 25.56 6.328 33.69-4.078c8.188-10.44 6.375-25.53-4.062-33.7L485.1 354.9zM350.8 249.6L244.3 166.2C253.8 162.2 264 160 274.6 160c20.67 0 40.1 8.049 54.71 22.66c14.62 14.61 22.66 34.04 22.66 54.71C352 241.5 351.4 245.6 350.8 249.6zM234 387.4l-37.96 37.96C181.5 439.1 162 448 141.4 448c-20.67 0-40.1-8.047-54.71-22.66c-14.61-14.61-22.66-34.04-22.66-54.71s8.049-40.1 22.66-54.71l84.83-84.83L120.7 191.3L41.41 270.7c-55.21 55.21-55.21 144.7 0 199.9C69.01 498.2 105.2 512 141.4 512c36.18 0 72.36-13.8 99.96-41.41l43.36-43.36c-15.11-8.012-29.47-17.58-41.91-30.02C239.6 394.1 236.9 390.7 234 387.4zM265.4 374.6C293 402.2 329.2 416 365.4 416c11.98 0 23.84-2.082 35.51-5.111L224.6 272.7C223.9 309.5 237.3 346.5 265.4 374.6z"></path>
</symbol>
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" id="icon-eye-slash">
<path d="M320 400c-75.85 0-137.25-58.71-142.9-133.11L72.2 185.82c-13.79 17.3-26.48 35.59-36.72 55.59a32.35 32.35 0 0 0 0 29.19C89.71 376.41 197.07 448 320 448c26.91 0 52.87-4 77.89-10.46L346 397.39a144.13 144.13 0 0 1-26 2.61zm313.82 58.1l-110.55-85.44a331.25 331.25 0 0 0 81.25-102.07 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64a308.15 308.15 0 0 0-147.32 37.7L45.46 3.37A16 16 0 0 0 23 6.18L3.37 31.45A16 16 0 0 0 6.18 53.9l588.36 454.73a16 16 0 0 0 22.46-2.81l19.64-25.27a16 16 0 0 0-2.82-22.45zm-183.72-142l-39.3-30.38A94.75 94.75 0 0 0 416 256a94.76 94.76 0 0 0-121.31-92.21A47.65 47.65 0 0 1 304 192a46.64 46.64 0 0 1-1.54 10l-73.61-56.89A142.31 142.31 0 0 1 320 112a143.92 143.92 0 0 1 144 144c0 21.63-5.29 41.79-13.9 60.11z"></path>
</symbol>
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-feather">
<path d="M483.4 244.2L351.9 287.1h97.74c-9.874 10.62 3.75-3.125-46.24 46.87l-147.6 49.12h98.24c-74.99 73.12-194.6 70.62-246.8 54.1l-66.14 65.99c-9.374 9.374-24.6 9.374-33.98 0s-9.374-24.6 0-33.98l259.5-259.2c6.249-6.25 6.249-16.37 0-22.62c-6.249-6.249-16.37-6.249-22.62 0l-178.4 178.2C58.78 306.1 68.61 216.7 129.1 156.3l85.74-85.68c90.62-90.62 189.8-88.27 252.3-25.78C517.8 95.34 528.9 169.7 483.4 244.2z"></path>
</symbol>
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-fast-forward">
<path d="M52.51 440.6l171.5-142.9V214.3L52.51 71.41C31.88 54.28 0 68.66 0 96.03v319.9C0 443.3 31.88 457.7 52.51 440.6zM308.5 440.6l192-159.1c15.25-12.87 15.25-36.37 0-49.24l-192-159.1c-20.63-17.12-52.51-2.749-52.51 24.62v319.9C256 443.3 287.9 457.7 308.5 440.6z"></path>
</symbol>
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-graph-rising">
<path d="M472 432h-48a24 24 0 01-24-24V104a24 24 0 0124-24h48a24 24 0 0124 24v304a24 24 0 01-24 24zM344 432h-48a24 24 0 01-24-24V184a24 24 0 0124-24h48a24 24 0 0124 24v224a24 24 0 01-24 24zM216 432h-48a24 24 0 01-24-24V248a24 24 0 0124-24h48a24 24 0 0124 24v160a24 24 0 01-24 24zM88 432H40a24 24 0 01-24-24v-96a24 24 0 0124-24h48a24 24 0 0124 24v96a24 24 0 01-24 24z"></path>
</symbol>
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" id="icon-rating">
<path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"></path>
</symbol>
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-rewind">
<path d="M459.5 71.41l-171.5 142.9v83.45l171.5 142.9C480.1 457.7 512 443.3 512 415.1V96.03C512 68.66 480.1 54.28 459.5 71.41zM203.5 71.41L11.44 231.4c-15.25 12.87-15.25 36.37 0 49.24l192 159.1c20.63 17.12 52.51 2.749 52.51-24.62v-319.9C255.1 68.66 224.1 54.28 203.5 71.41z"></path>
</symbol>
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-like-dislike">
<path d="M22.5,10H15.75C15.13,10 14.6,10.38 14.37,10.91L12.11,16.2C12.04,16.37 12,16.56 12,16.75V18A1,1 0 0,0 13,19H18.18L17.5,22.18V22.42C17.5,22.73 17.63,23 17.83,23.22L18.62,24L23.56,19.06C23.83,18.79 24,18.41 24,18V11.5A1.5,1.5 0 0,0 22.5,10M12,6A1,1 0 0,0 11,5H5.82L6.5,1.82V1.59C6.5,1.28 6.37,1 6.17,0.79L5.38,0L0.44,4.94C0.17,5.21 0,5.59 0,6V12.5A1.5,1.5 0 0,0 1.5,14H8.25C8.87,14 9.4,13.62 9.63,13.09L11.89,7.8C11.96,7.63 12,7.44 12,7.25V6Z"></path>
</symbol>
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" id="icon-person-slash">
<path d="M95.1 477.3c0 19.14 15.52 34.67 34.66 34.67h378.7c5.625 0 10.73-1.65 15.42-4.029L264.9 304.3C171.3 306.7 95.1 383.1 95.1 477.3zM630.8 469.1l-277.1-217.9c54.69-14.56 95.18-63.95 95.18-123.2C447.1 57.31 390.7 0 319.1 0C250.2 0 193.7 55.93 192.3 125.4l-153.4-120.3C34.41 1.672 29.19 0 24.03 0C16.91 0 9.845 3.156 5.127 9.187c-8.187 10.44-6.375 25.53 4.062 33.7L601.2 506.9c10.5 8.203 25.56 6.328 33.69-4.078C643.1 492.4 641.2 477.3 630.8 469.1z"></path>
</symbol>
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-image-slash">
<path d="M21 17.2L6.8 3H19C20.1 3 21 3.9 21 5V17.2M20.7 22L19.7 21H5C3.9 21 3 20.1 3 19V4.3L2 3.3L3.3 2L22 20.7L20.7 22M16.8 18L12.9 14.1L11 16.5L8.5 13.5L5 18H16.8Z"></path>
</symbol>
@ -37,10 +60,14 @@
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-responsive">
<path d="M4,6V16H9V12A2,2 0 0,1 11,10H16A2,2 0 0,1 18,12V16H20V6H4M0,20V18H4A2,2 0 0,1 2,16V6A2,2 0 0,1 4,4H20A2,2 0 0,1 22,6V16A2,2 0 0,1 20,18H24V20H18V20C18,21.11 17.1,22 16,22H11A2,2 0 0,1 9,20H9L0,20M11.5,20A0.5,0.5 0 0,0 11,20.5A0.5,0.5 0 0,0 11.5,21A0.5,0.5 0 0,0 12,20.5A0.5,0.5 0 0,0 11.5,20M15.5,20A0.5,0.5 0 0,0 15,20.5A0.5,0.5 0 0,0 15.5,21A0.5,0.5 0 0,0 16,20.5A0.5,0.5 0 0,0 15.5,20M13,20V21H14V20H13M11,12V19H16V12H11Z"></path>
</symbol>
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-search">
<path d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"></path>
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-legal">
<path d="M12,3C10.73,3 9.6,3.8 9.18,5H3V7H4.95L2,14C1.53,16 3,17 5.5,17C8,17 9.56,16 9,14L6.05,7H9.17C9.5,7.85 10.15,8.5 11,8.83V20H2V22H22V20H13V8.82C13.85,8.5 14.5,7.85 14.82,7H17.95L15,14C14.53,16 16,17 18.5,17C21,17 22.56,16 22,14L19.05,7H21V5H14.83C14.4,3.8 13.27,3 12,3M12,5A1,1 0 0,1 13,6A1,1 0 0,1 12,7A1,1 0 0,1 11,6A1,1 0 0,1 12,5M5.5,10.25L7,14H4L5.5,10.25M18.5,10.25L20,14H17L18.5,10.25Z"></path>
</symbol>
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-external-link">
<path d="M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"></path>
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-code-block">
<path d="M5,3H7V5H5V10A2,2 0 0,1 3,12A2,2 0 0,1 5,14V19H7V21H5C3.93,20.73 3,20.1 3,19V15A2,2 0 0,0 1,13H0V11H1A2,2 0 0,0 3,9V5A2,2 0 0,1 5,3M19,3A2,2 0 0,1 21,5V9A2,2 0 0,0 23,11H24V13H23A2,2 0 0,0 21,15V19A2,2 0 0,1 19,21H17V19H19V14A2,2 0 0,1 21,12A2,2 0 0,1 19,10V5H17V3H19M12,15A1,1 0 0,1 13,16A1,1 0 0,1 12,17A1,1 0 0,1 11,16A1,1 0 0,1 12,15M8,15A1,1 0 0,1 9,16A1,1 0 0,1 8,17A1,1 0 0,1 7,16A1,1 0 0,1 8,15M16,15A1,1 0 0,1 17,16A1,1 0 0,1 16,17A1,1 0 0,1 15,16A1,1 0 0,1 16,15Z"></path>
</symbol>
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-ads-slash">
<path d="M12.2 9L10.2 7H13C14.1 7 15 7.9 15 9V11.8L13 9.8V9H12.2M23 9V7H19C17.9 7 17 7.9 17 9V11C17 12.1 17.9 13 19 13H21V15H18.2L20.2 17H21C22.1 17 23 16.1 23 15V13C23 11.9 22.1 11 21 11H19V9H23M22.1 21.5L20.8 22.8L14.4 16.4C14.1 16.7 13.6 17 13 17H9V10.9L7 8.9V17H5V13H3V17H1V9C1 7.9 1.9 7 3 7H5.1L1.1 3L2.4 1.7L22.1 21.5M5 9H3V11H5V9M13 14.9L11 12.9V15H13V14.9Z"></path>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,48 @@
import Link from 'next/link';
import Layout from '../../layouts/Layout';
import Meta from '../Meta/Meta';
import styles from '../../styles/modules/components/error/error-info.module.scss';
// for details regarding the svg, go to sadgnu.svg file
// description copied verbatim from https://www.gnu.org/graphics/sventsitsky-sadgnu.html
// 404 idea from ninamori.org 404 page.
const ErrorInfo = ({ message = 'Not found, sorry.', statusCode = 404 }) => {
return (
<>
<Meta
title={`${message} (${statusCode})`}
description='you encountered an error page!'
/>
<Layout className={styles.error}>
<svg
className={styles.gnu}
focusable='false'
role='img'
aria-labelledby='gnu-title gnu-desc'
>
<title id='gnu-title'>GNU and Tux</title>
<desc id='gnu-desc'>
A pencil drawing of a big gnu and a small penguin, both very sad.
GNU is despondently sitting on a bench, and Tux stands beside him,
looking down and patting him on the back.
</desc>
<use href='/svg/sadgnu.svg#sad-gnu'></use>
</svg>
<h1 className={`heading heading__primary ${styles.heading}`}>
<span>{message}</span>
<span> ({statusCode})</span>
</h1>
<p className={styles.back}>
Go back to{' '}
<Link href='/about'>
<a className='link'>the homepage</a>
</Link>
.
</p>
</Layout>
</>
);
};
export default ErrorInfo;

View file

@ -1,5 +1,4 @@
import Head from 'next/head';
import { ReactNode } from 'react';
type Props = {
title: string;
@ -7,15 +6,11 @@ type Props = {
imgUrl?: string;
};
const BASE_URL = process.env.NEXT_PUBLIC_URL ?? 'https://iket.me';
const Meta = ({
title,
description = 'libremdb, a free & open source IMDb front-end.',
imgUrl = 'icon.svg',
}: Props) => {
const url = new URL(imgUrl, BASE_URL);
return (
<Head>
<meta charSet='UTF-8' />
@ -35,7 +30,10 @@ const Meta = ({
<meta property='og:site_name' content='libremdb' />
<meta property='og:locale' content='en_US' />
<meta property='og:type' content='video.movie' />
<meta property='og:image' content={url.toString()} />
<meta
property='og:image'
content={`${process.env.NEXT_PUBLIC_URL}/${imgUrl}`}
/>
</Head>
);
};

View file

@ -1,6 +1,7 @@
import { useContext } from 'react';
import { themeContext } from 'src/context/theme-context';
import styles from 'src/styles/modules/components/buttons/themeToggler.module.scss';
import { themeContext } from '../../context/theme-context';
import styles from '../../styles/modules/components/buttons/themeToggler.module.scss';
type Props = {
className: string;
@ -16,9 +17,9 @@ const ThemeToggler = (props: Props) => {
return (
<button
className={`${styles.button} ${props.className}`}
aria-label='Change theme'
onClick={clickHandler}
>
<span className='visually-hidden'>Change theme</span>
<svg
className={`icon ${styles.icon}`}
focusable='false'

View file

@ -1,29 +0,0 @@
import type { ReactNode, ElementType, ComponentPropsWithoutRef } from 'react';
import styles from 'src/styles/modules/components/card/card.module.scss';
// ensuring that other attributes to <Card/> are correct based on the value of 'as' prop.
// a cheap implementation of as prop found in libraries like CharkaUI or MaterialUI.
type Props<T extends ElementType> = {
children: ReactNode;
as?: T | 'section';
hoverable?: true;
} & ComponentPropsWithoutRef<T>;
const Card = <T extends ElementType = 'li'>({
children,
as,
hoverable,
className,
...rest
}: Props<T>) => {
const Component = as ?? 'li';
const classNames = `${hoverable ? styles.hoverable : ''} ${styles.card} ${className}`;
return (
<Component className={classNames} {...rest}>
{children}
</Component>
);
};
export default Card;

View file

@ -1,45 +0,0 @@
import { ComponentPropsWithoutRef, CSSProperties, ReactNode } from 'react';
import Image from 'next/future/image';
import Card from './Card';
import { getProxiedIMDbImgUrl, modifyIMDbImg } from 'src/utils/helpers';
import styles from 'src/styles/modules/components/card/card-basic.module.scss';
type Props = {
children: ReactNode;
className?: string;
image?: string;
title: string;
} & ComponentPropsWithoutRef<'section'>;
const CardBasic = ({ image, children, className, title, ...rest }: Props) => {
const style: CSSProperties = {
backgroundImage: image && `url(${getProxiedIMDbImgUrl(modifyIMDbImg(image, 300))})`,
};
return (
<Card as='section' className={`${styles.container} ${className}`} {...rest}>
<div className={styles.imageContainer} style={style}>
{image ? (
<Image
className={styles.image}
src={modifyIMDbImg(image)}
alt=''
priority
fill
sizes='300px'
/>
) : (
<svg className={styles.imageNA}>
<use href='/svg/sprite.svg#icon-image-slash' />
</svg>
)}
</div>
<div className={styles.info}>
<h1 className={`${styles.title} heading heading__primary`}>{title}</h1>
{children}
</div>
</Card>
);
};
export default CardBasic;

View file

@ -1,51 +0,0 @@
import Card from './Card';
import styles from 'src/styles/modules/components/card/card-cast.module.scss';
import { ComponentPropsWithoutRef, ReactNode } from 'react';
import Link from 'next/link';
import Image from 'next/future/image';
import { modifyIMDbImg } from 'src/utils/helpers';
type Props = {
link: string;
name: string;
characters: string[] | null;
attributes: string[] | null;
image?: string | null;
children?: ReactNode;
} & ComponentPropsWithoutRef<'li'>;
const CardCast = ({ link, name, image, children, characters, attributes, ...rest }: Props) => {
return (
<Card hoverable {...rest}>
<Link href={link}>
<a className={styles.item}>
<div className={styles.imgContainer}>
{image ? (
<Image
src={modifyIMDbImg(image, 400)}
alt=''
fill
className={styles.img}
sizes='200px'
/>
) : (
<svg className={styles.imgNA}>
<use href='/svg/sprite.svg#icon-image-slash' />
</svg>
)}
</div>
<div className={styles.textContainer}>
<p className={`heading ${styles.name}`}>{name}</p>
<p className={styles.role}>
{characters?.join(', ')}
{attributes && <span> ({attributes.join(', ')})</span>}
</p>
{children}
</div>
</a>
</Link>
</Card>
);
};
export default CardCast;

View file

@ -1,40 +0,0 @@
import { ComponentPropsWithoutRef, ReactNode } from 'react';
import Link from 'next/link';
import Image from 'next/future/image';
import Card from './Card';
import { modifyIMDbImg } from 'src/utils/helpers';
import styles from 'src/styles/modules/components/card/card-result.module.scss';
type Props = {
link: string;
name: string;
image?: string;
showImage?: true;
children?: ReactNode;
} & ComponentPropsWithoutRef<'li'>;
const CardResult = ({ link, name, image, showImage, children, ...rest }: Props) => {
let ImageComponent = null;
if (showImage)
ImageComponent = image ? (
<Image src={modifyIMDbImg(image, 400)} alt='' fill className={styles.img} sizes='200px' />
) : (
<svg className={styles.imgNA}>
<use href='/svg/sprite.svg#icon-image-slash' />
</svg>
);
return (
<Card hoverable {...rest} className={`${styles.item} ${!showImage && styles.sansImage}`}>
<div className={styles.imgContainer}>{ImageComponent}</div>
<div className={styles.info}>
<Link href={link}>
<a className={`heading ${styles.heading}`}>{name}</a>
</Link>
{children}
</div>
</Card>
);
};
export default CardResult;

View file

@ -1,63 +0,0 @@
import Card from './Card';
import styles from 'src/styles/modules/components/card/card-title.module.scss';
import { ComponentPropsWithoutRef, ReactNode } from 'react';
import Link from 'next/link';
import Image from 'next/future/image';
import { formatNumber, modifyIMDbImg } from 'src/utils/helpers';
type Props = {
link: string;
name: string;
titleType: string;
year?: { start: number; end: number | null };
ratings?: { avg: number | null; numVotes: number };
image?: string;
children?: ReactNode;
} & ComponentPropsWithoutRef<'li'>;
const CardTitle = ({ link, name, year, image, ratings, titleType, children, ...rest }: Props) => {
const years = year?.end ? `${year.start}-${year.end}` : year?.start;
return (
<Card hoverable {...rest}>
<Link href={link}>
<a className={styles.item}>
<div className={styles.imgContainer}>
{image ? (
<Image
src={modifyIMDbImg(image, 400)}
alt=''
fill
className={styles.img}
sizes='200px'
/>
) : (
<svg className={styles.imgNA}>
<use href='/svg/sprite.svg#icon-image-slash' />
</svg>
)}
</div>
<div className={styles.textContainer}>
<p className={`heading ${styles.name}`}>{name}</p>
<p>
<span>{titleType}</span>
<span>{years && ` (${years})`}</span>
</p>
{ratings?.avg && (
<p className={styles.rating}>
<span className={styles.ratingNum}>{ratings.avg}</span>
<svg className={styles.ratingIcon}>
<use href='/svg/sprite.svg#icon-rating'></use>
</svg>
<span> ({formatNumber(ratings.numVotes)} votes)</span>
</p>
)}
<div className={styles.children}>{children}</div>
</div>
</a>
</Link>
</Card>
);
};
export default CardTitle;

View file

@ -1,5 +0,0 @@
export { default as Card } from './Card';
export { default as CardTitle } from './CardTitle';
export { default as CardBasic } from './CardBasic';
export { default as CardCast } from './CardCast';
export { default as CardResult } from './CardResult';

View file

@ -1,45 +0,0 @@
import React, { Component, ErrorInfo, ReactNode } from 'react';
import ErrorInfoComponent from './ErrorInfo';
type Props = {
children: ReactNode;
};
type State = {
hasError: boolean;
};
class ErrorBoundary extends Component<Props, State> {
state: State = {
hasError: false,
};
static getDerivedStateFromError(error: Error): State {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo);
}
resetError() {
this.setState({ hasError: false });
}
render() {
if (this.state.hasError)
return (
<ErrorInfoComponent
message='Something weird happened on your browser.'
misc={{
subtext: 'Check console for more information.',
buttonClickHandler: this.resetError.bind(this),
buttonText: 'Reload Page',
}}
/>
);
return this.props.children;
}
}
export default ErrorBoundary;

View file

@ -1,71 +0,0 @@
import Link from 'next/link';
import Layout from 'src/components/layout';
import Meta from 'src/components/meta/Meta';
import styles from 'src/styles/modules/components/error/error-info.module.scss';
// for details regarding the svg, go to sadgnu.svg file
// description copied verbatim from https://www.gnu.org/graphics/sventsitsky-sadgnu.html
// 404 idea from ninamori.org 404 page.
type Props = {
message: string;
statusCode?: number;
originalPath?: string;
/** props specific to error boundary. */
misc?: {
subtext: string;
buttonText: string;
buttonClickHandler: () => void;
};
};
const ErrorInfo = ({ message, statusCode, misc, originalPath }: Props) => {
const title = statusCode ? `${message} (${statusCode})` : message;
return (
<>
<Meta title={title} description='you encountered an error page!' />
<Layout className={styles.error} originalPath={originalPath}>
<svg
className={styles.gnu}
focusable='false'
role='img'
aria-labelledby='gnu-title gnu-desc'
>
<title id='gnu-title'>GNU and Tux</title>
<desc id='gnu-desc'>
A pencil drawing of a big gnu and a small penguin, both very sad. GNU is despondently
sitting on a bench, and Tux stands beside him, looking down and patting him on the back.
</desc>
<use href='/svg/sadgnu.svg#sad-gnu'></use>
</svg>
<h1 className={`heading heading__primary ${styles.heading}`}>{title}</h1>
{misc ? (
<>
<p>{misc.subtext}</p>
<button className={styles.button} onClick={misc.buttonClickHandler}>
{misc.buttonText}
</button>
</>
) : (
<p>
Go back to{' '}
<Link href='/'>
<a className='link'>the homepage</a>
</Link>
, or view this route{' '}
<a
className='link'
href={`https://www.imdb.com${originalPath ?? ''}`}
target='_blank'
rel='noreferrer'
>
on IMDb
</a>
.
</p>
)}
</Layout>
</>
);
};
export default ErrorInfo;

View file

@ -1,13 +0,0 @@
import { CardResult } from 'src/components/card';
import { Companies } from 'src/interfaces/shared/search';
type Props = { company: Companies[number] };
const Company = ({ company }: Props) => (
<CardResult name={company.name} link={`/search/title?companies=${company.id}`}>
{company.country && <p>{company.country}</p>}
{!!company.type && <p>{company.type}</p>}
</CardResult>
);
export default Company;

View file

@ -1,12 +0,0 @@
import { CardResult } from 'src/components/card';
import { Keywords } from 'src/interfaces/shared/search';
type Props = { keyword: Keywords[number] };
const Keyword = ({ keyword }: Props) => (
<CardResult link={`/search/keyword?keywords=${keyword.text}`} name={keyword.text}>
{keyword.numTitles && <p>{keyword.numTitles} titles</p>}
</CardResult>
);
export default Keyword;

View file

@ -1,20 +0,0 @@
import { CardResult } from 'src/components/card';
import { People } from 'src/interfaces/shared/search';
import styles from 'src/styles/modules/components/find/person.module.scss';
type Props = { person: People[number] };
const Person = ({ person }: Props) => {
return (
<CardResult showImage name={person.name} link={`/name/${person.id}`} image={person.image?.url}>
<p>{person.aka}</p>
<p>{person.jobCateogry}</p>
<ul className={styles.basicInfo} aria-label='quick facts'>
{person.knownForTitle && <li>{person.knownForTitle}</li>}
{person.knownInYear && <li>{person.knownInYear}</li>}
</ul>
</CardResult>
);
};
export default Person;

View file

@ -1,37 +0,0 @@
import Link from 'next/link';
import { CardResult } from 'src/components/card';
import { Titles } from 'src/interfaces/shared/search';
import styles from 'src/styles/modules/components/find/title.module.scss';
type Props = { title: Titles[number] };
const Title = ({ title }: Props) => {
return (
<CardResult showImage name={title.name} link={`/title/${title.id}`} image={title.image?.url}>
<ul aria-label='quick facts' className={styles.basicInfo}>
<li>{title.type}</li>
<li>{title.sAndE}</li>
<li>{title.releaseYear}</li>
</ul>
{!!title.credits.length && (
<p className={styles.stars}>
<span>Stars: </span>
{title.credits.join(', ')}
</p>
)}
{title.seriesId && (
<ul aria-label='quick series facts' className={styles.seriesInfo}>
{title.seriesType && <li>{title.seriesType}</li>}
<li>
<Link href={`/title/${title.seriesId}`}>
<a className='link'>{title.seriesName}</a>
</Link>
</li>
{title.seriesReleaseYear && <li>{title.seriesReleaseYear}</li>}
</ul>
)}
</CardResult>
);
};
export default Title;

View file

@ -1,92 +0,0 @@
import Company from './Company';
import Person from './Person';
import Title from './Title';
import Keyword from './Keyword';
import Find from 'src/interfaces/shared/search';
import { getResTitleTypeHeading } from 'src/utils/helpers';
import styles from 'src/styles/modules/components/find/results.module.scss';
type Props = {
results: Find | null;
className?: string;
title: string;
};
const resultsExist = (
results: Props['results']
): results is NonNullable<Props['results']> =>
Boolean(
results &&
(results.people.length ||
results.keywords.length ||
results.companies.length ||
results.titles.length)
);
// MAIN COMPONENT
const Results = ({ results, className, title }: Props) => {
if (!resultsExist(results))
return (
<h1 className={`heading heading__primary ${className}`}>
No results found
</h1>
);
const { titles, people, keywords, companies, meta } = results;
const titlesSectionHeading = getResTitleTypeHeading(
meta.type,
meta.titleType
);
return (
<article className={`${className} ${styles.results}`}>
<h1 className='heading heading__primary'>Results for '{title}'</h1>
<div className={styles.results__list}>
{!!titles.length && (
<section className={styles.titles}>
<h2 className='heading heading__secondary'>
{titlesSectionHeading}
</h2>
<ul className={styles.titles__list}>
{titles.map(title => (
<Title title={title} key={title.id} />
))}
</ul>
</section>
)}
{!!people.length && (
<section className={styles.people}>
<h2 className='heading heading__secondary'>People</h2>
<ul className={styles.people__list}>
{people.map(person => (
<Person person={person} key={person.id} />
))}
</ul>
</section>
)}
{!!companies.length && (
<section className={styles.people}>
<h2 className='heading heading__secondary'>Companies</h2>
<ul className={styles.people__list}>
{companies.map(company => (
<Company company={company} key={company.id} />
))}
</ul>
</section>
)}
{!!keywords.length && (
<section className={styles.people}>
<h2 className='heading heading__secondary'>Keywords</h2>
<ul className={styles.people__list}>
{keywords.map(keyword => (
<Keyword keyword={keyword} key={keyword.id} />
))}
</ul>
</section>
)}
</div>
</article>
);
};
export default Results;

View file

@ -1,121 +0,0 @@
import { ChangeEventHandler, FormEventHandler, useRef, useState } from 'react';
import { useRouter } from 'next/router';
import { cleanQueryStr } from 'src/utils/helpers';
import { QueryTypes } from 'src/interfaces/shared/search';
import { resultTypes, resultTitleTypes } from 'src/utils/constants/find';
import styles from 'src/styles/modules/components/form/find.module.scss';
type Props = {
className?: string;
};
const Form = ({ className }: Props) => {
const router = useRouter();
const formRef = useRef<HTMLFormElement>(null);
const [isDisabled, setIsDisabled] = useState(false);
// title types can't be selected unless type selected is 'title'
const typesChangeHandler: ChangeEventHandler<HTMLFieldSetElement> = e => {
const el = e.target as unknown as HTMLInputElement; // we have only radios that'll fire change event.
const value = el.value as QueryTypes;
if (value === 'tt') setIsDisabled(false);
else setIsDisabled(true);
};
// preventing page refresh and instead handling submission through js
const submitHandler: FormEventHandler<HTMLFormElement> = e => {
e.preventDefault();
const formEl = formRef.current!;
const formData = new FormData(formEl);
const query = (formData.get('q') as string).trim();
const entries = [...formData.entries()] as [string, string][];
const queryStr = cleanQueryStr(entries);
if (query) router.push(`/find?${queryStr}`);
else setIsDisabled(false);
formEl.reset();
};
return (
<form
action='/find'
onSubmit={submitHandler}
ref={formRef}
className={`${className} ${styles.form}`}
>
<p className='heading heading__primary'>Search</p>
<p className={styles.searchbar}>
<svg
className={`icon ${styles.searchbar__icon}`}
focusable='false'
aria-hidden='true'
role='img'
>
<use href='/svg/sprite.svg#icon-search'></use>
</svg>
<input
id='searchbar'
type='search'
name='q'
placeholder='movies, people...'
className={styles.searchbar__input}
required
minLength={2}
/>
<label className='visually-hidden' htmlFor='searchbar'>
Search for anything
</label>
</p>
<fieldset className={styles.types} onChange={typesChangeHandler}>
<legend className={`heading ${styles.types__heading}`}>Filter by Type</legend>
<RadioBtns data={resultTypes} className={styles.type} />
</fieldset>
<fieldset className={styles.titleTypes} disabled={isDisabled}>
<legend className={`heading ${styles.titleTypes__heading}`}>Filter by Title Type</legend>
<RadioBtns data={resultTitleTypes} className={styles.titleType} />
</fieldset>
<p className={styles.exact}>
<label htmlFor='exact'>Exact Matches</label>
<input type='checkbox' name='exact' id='exact' value='true' />
</p>
<div className={styles.buttons}>
<button type='reset' className={styles.button}>
Clear
</button>
<button type='submit' className={styles.button}>
Submit
</button>
</div>
</form>
);
};
const RadioBtns = ({
data,
className,
}: {
data: typeof resultTypes | typeof resultTitleTypes;
className: string;
}) => (
<>
{data.types.map(({ name, val }) => (
<p className={className} key={val}>
<input
type='radio'
name={data.key}
id={`${data.key}:${val}`}
value={val}
className='visually-hidden'
/>
<label htmlFor={`${data.key}:${val}`}>{name}</label>
</p>
))}
</>
);
export default Form;

View file

@ -1,52 +0,0 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import styles from 'src/styles/modules/layout/footer.module.scss';
const links = [
{ path: '/about', text: 'About' },
{ path: '/find', text: 'Find' },
{ path: '/privacy', text: 'Privacy' },
{ path: '/contact', text: 'Contact' },
] as const;
const Footer = () => {
const { pathname } = useRouter();
return (
<footer id='footer' className={styles.footer}>
<nav aria-label='primary navigation' className={styles.nav}>
<ul className={styles.list}>
{links.map(link => (
<li className={styles.nav__item} key={link.path}>
<Link href={link.path}>
<a
className={styles.nav__link}
aria-current={pathname === link.path ? 'page' : undefined}
>
{link.text}
</a>
</Link>
</li>
))}
<li className={styles.nav__item}>
<a href='#' className={styles.nav__link}>
Back to top
</a>
</li>
</ul>
</nav>
<p className={styles.licence}>
Licensed under{' '}
<a
className={styles.nav__link}
href='https://www.gnu.org/licenses/agpl-3.0-standalone.html'
>
GNU AGPLv3
</a>
.
</p>
</footer>
);
};
export default Footer;

View file

@ -1,23 +0,0 @@
import type { DataKind, Data as TData } from 'src/interfaces/shared/list';
import type { ToArray } from 'src/interfaces/shared';
import Images from './Images';
import Names from './Names';
import Titles from './Titles';
type Props = {
data: ToArray<TData<DataKind>>;
};
const Data = ({ data }: Props) => {
if (isDataImages(data)) return <Images images={data} />;
if (isDataNames(data)) return <Names names={data} />;
return <Titles titles={data} />;
};
export default Data;
const isDataImages = (data: unknown): data is TData<'images'>[] =>
Array.isArray(data) && typeof data[0] === 'string';
const isDataNames = (data: unknown): data is TData<'names'>[] =>
Array.isArray(data) && data[0] && typeof data[0] === 'object' && 'about' in data[0];

View file

@ -1,22 +0,0 @@
import Image from 'next/future/image';
import { modifyIMDbImg } from 'src/utils/helpers';
import type { Data } from 'src/interfaces/shared/list';
import styles from 'src/styles/modules/components/list/images.module.scss';
type Props = {
images: Data<'images'>[];
};
const Images = ({ images }: Props) => {
return (
<section className={styles.container}>
{images.map(image => (
<figure className={styles.imgContainer} key={image}>
<Image src={modifyIMDbImg(image, 400)} alt='' fill className={styles.img} sizes='200px'/>
</figure>
))}
</section>
);
};
export default Images;

View file

@ -1,35 +0,0 @@
import Link from 'next/link';
import { formatDate } from 'src/utils/helpers';
import List from 'src/interfaces/shared/list';
import styles from 'src/styles/modules/components/list/meta.module.scss';
type Props = {
title: string;
meta: List['meta'];
description: List['description'];
};
const Meta = ({ title, meta, description }: Props) => {
const by = meta.by.link ? (
<Link href={meta.by.link}>
<a className='link'>{meta.by.name}</a>
</Link>
) : (
meta.by.name
);
return (
<header className={styles.container}>
<h1 className='heading heading__secondary'>{title}</h1>
<ul className={styles.list}>
<li>by {by}</li>
<li>{meta.created}</li>
{meta.updated && <li>{meta.updated}</li>}
<li>
{meta.num} {meta.type}
</li>
</ul>
{description && <p>{description}</p>}
</header>
);
};
export default Meta;

View file

@ -1,57 +0,0 @@
import Image from 'next/future/image';
import { getProxiedIMDbImgUrl, modifyIMDbImg } from 'src/utils/helpers';
import { Card } from 'src/components/card';
import type { Data } from 'src/interfaces/shared/list';
import styles from 'src/styles/modules/components/list/names.module.scss';
import OptionalLink from './OptionalLink';
type Props = {
names: Data<'names'>[];
};
const Names = ({ names }: Props) => {
return (
<ul className={styles.names}>
{names.map(name => (
<Name {...name} key={name.name} />
))}
</ul>
);
};
export default Names;
const Name = ({ about, image, job, knownFor, knownForLink, name, url }: Props['names'][number]) => {
// const style: CSSProperties = {
// backgroundImage: image ? `url(${getProxiedIMDbImgUrl(modifyIMDbImg(image, 300))})` : undefined,
// };
return (
<Card hoverable className={styles.name}>
<div className={styles.imgContainer}>
{image ? (
<Image src={modifyIMDbImg(image, 400)} alt='' fill className={styles.img} sizes='200px' />
) : (
<svg className={styles.imgNA}>
<use href='/svg/sprite.svg#icon-image-slash' />
</svg>
)}
</div>
<div className={styles.info}>
<h2 className={`heading ${styles.heading}`}>
<OptionalLink href={url} className={`heading ${styles.heading}`}>
{name}
</OptionalLink>
</h2>
<ul className={styles.basicInfo} aria-label='quick facts'>
{job && <li>{job}</li>}
{knownFor && (
<li>
<OptionalLink href={knownForLink}>{knownFor}</OptionalLink>
</li>
)}
</ul>
<p>{about}</p>
</div>
</Card>
);
};

View file

@ -1,20 +0,0 @@
import type { ReactNode, ComponentPropsWithoutRef } from 'react';
import Link from 'next/link';
const OptionalLink = ({
href,
children,
...rest
}: { href?: string | null; children: ReactNode } & Omit<ComponentPropsWithoutRef<'a'>, 'href'>) => (
<>
{href ? (
<Link href={href}>
<a {...rest}>{children}</a>
</Link>
) : (
children
)}
</>
);
export default OptionalLink;

View file

@ -1,33 +0,0 @@
import OptionalLink from './OptionalLink';
import type List from 'src/interfaces/shared/list';
import styles from 'src/styles/modules/components/list/pagination.module.scss';
type Props = {
pagination: List['pagination'];
};
const Pagination = ({ pagination }: Props) => {
const prevLink = pagination.prev && pagination.prev !== '#' ? pagination.prev : null;
const nextLink = pagination.next && pagination.next !== '#' ? pagination.next : null;
if (!prevLink && !nextLink) return null;
return (
<nav aria-label='pagination'>
<ul className={styles.nav}>
<li aria-hidden={!prevLink}>
<OptionalLink href={prevLink} className='link'>
Prev
</OptionalLink>
</li>
<li>{pagination.range} shown</li>
<li aria-hidden={!nextLink}>
<OptionalLink href={nextLink} className='link'>
Next
</OptionalLink>
</li>
</ul>
</nav>
);
};
export default Pagination;

View file

@ -1,79 +0,0 @@
import Image from 'next/future/image';
import { getProxiedIMDbImgUrl, modifyIMDbImg } from 'src/utils/helpers';
import { Card } from 'src/components/card';
import type { Data } from 'src/interfaces/shared/list';
import styles from 'src/styles/modules/components/list/titles.module.scss';
import { CSSProperties } from 'react';
import OptionalLink from './OptionalLink';
type Props = {
titles: Data<'titles'>[];
};
const Titles = ({ titles }: Props) => {
return (
<ul className={styles.titles}>
{titles.map(title => (
<Title {...title} key={title.name} />
))}
</ul>
);
};
export default Titles;
const Title = (props: Props['titles'][number]) => {
const style: CSSProperties = {
backgroundImage: props.image
? `url(${getProxiedIMDbImgUrl(modifyIMDbImg(props.image, 300))})`
: undefined,
};
return (
<Card hoverable className={styles.title}>
<div className={styles.imgContainer}>
{props.image ? (
<Image src={modifyIMDbImg(props.image, 400)} alt='' fill className={styles.img} />
) : (
<svg className={styles.imgNA}>
<use href='/svg/sprite.svg#icon-image-slash' />
</svg>
)}
</div>
<div className={styles.info}>
<h2 className={`heading heading__tertiary ${styles.heading}`}>
<OptionalLink href={props.url} className={`heading ${styles.heading}`}>
{props.name} {props.year}
</OptionalLink>
</h2>
<ul className={styles.basicInfo} aria-label='quick facts'>
{props.certificate && <li>{props.certificate}</li>}
{props.runtime && <li>{props.runtime}</li>}
{props.genre && <li>{props.genre}</li>}
</ul>
<ul className={styles.ratings}>
{Boolean(props.rating) && <li className={styles.rating}>
<span className={styles.rating__num}>{props.rating}</span>
<svg className={styles.rating__icon}>
<use href='/svg/sprite.svg#icon-rating'></use>
</svg>
<span className={styles.rating__text}> Avg. rating</span>
</li>}
{Boolean(props.metascore) && <li className={styles.rating}>
<span className={styles.rating__num}>{props.metascore}</span>
<span className={styles.rating__text}>Metascore</span>
</li>}
</ul>
<p className={styles.plot}>
<span>Plot:</span> {props.plot}
</p>
<ul className={styles.otherInfo}>
{props.otherInfo.map(([infoHeading, info]) => (
<li key={infoHeading}>
<span>{infoHeading}:</span> {info}
</li>
))}
</ul>
</div>
</Card>
);
};

View file

@ -1,3 +0,0 @@
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 'src/styles/modules/components/loaders/progress-bar.module.scss';
import styles from '../../styles/modules/components/loaders/progress-bar.module.scss';
const ProgressBar = () => {
return <span className={styles.progress} role='progressbar'></span>;

View file

@ -1,59 +0,0 @@
import { CardBasic } from 'src/components/card';
import { Basic as BasicType } from 'src/interfaces/shared/name';
import { formatNumber } from 'src/utils/helpers';
import styles from 'src/styles/modules/components/name/basic.module.scss';
type Props = {
className: string;
data: BasicType;
};
const Basic = ({ data, className }: Props) => {
return (
<CardBasic className={className} image={data.poster?.url} title={data.name}>
<div className={styles.ratings}>
{data.ranking && (
<p className={styles.rating}>
<span className={styles.rating__num}>{formatNumber(data.ranking.position)}</span>
<svg className={styles.rating__icon}>
<use href='/svg/sprite.svg#icon-graph-rising'></use>
</svg>
<span className={styles.rating__text}>
{' '}
Popularity (
<span className={styles.rating__sub}>{getRankingStats(data.ranking)}</span>)
</span>
</p>
)}
</div>
{!!data.primaryProfessions.length && (
<p className={styles.genres}>
<span className={styles.heading}>Profession: </span>
{data.primaryProfessions.join(', ')}
</p>
)}
{
<p className={styles.overview}>
<span className={styles.heading}>About: </span>
{data.bio.short}...
</p>
}
{data.knownFor.title && (
<p className={styles.genres}>
<span className={styles.heading}>Known for: </span>
{data.knownFor.title} ({data.knownFor.role})
</p>
)}
</CardBasic>
);
};
const getRankingStats = (ranking: NonNullable<Props['data']['ranking']>) => {
if (ranking.direction === 'FLAT') return '\u2192';
const change = formatNumber(ranking.change);
return (ranking.direction === 'UP' ? '\u2191' : '\u2193') + change;
};
export default Basic;

View file

@ -1,12 +0,0 @@
import styles from 'src/styles/modules/components/name/did-you-know.module.scss';
type Props = { bio: string };
const Bio = ({ bio }: Props) => (
<section className={styles.bio}>
<h2 className='heading heading__secondary'>About</h2>
<div dangerouslySetInnerHTML={{ __html: bio }} />
</section>
);
export default Bio;

View file

@ -1,43 +0,0 @@
import { Credits } from 'src/interfaces/shared/name';
import { CardTitle } from 'src/components/card';
import styles from 'src/styles/modules/components/name/credits.module.scss';
type Props = {
className: string;
data: Credits;
};
const Credits = ({ className, data }: Props) => {
if (!data.total) return null;
return (
<section className={`${className} ${styles.credits}`}>
<h2 className='heading heading__secondary'>Credits</h2>
{data.released.map(
(item, i) =>
!!item.total && (
<details open={i === 0} key={item.category.id}>
<summary>
{item.category.text} ({item.total})
</summary>
<ul className={styles.container} key={item.category.id}>
{item.titles.map(title => (
<CardTitle
key={title.id}
link={`/title/${title.id}`}
name={title.title}
titleType={title.type.text}
image={title.poster?.url}
year={title.releaseYear}
ratings={title.ratings}
/>
))}
</ul>
</details>
)
)}
</section>
);
};
export default Credits;

View file

@ -1,51 +0,0 @@
import Link from 'next/link';
import { DidYouKnow } from 'src/interfaces/shared/name';
import styles from 'src/styles/modules/components/name/did-you-know.module.scss';
type Props = {
data: DidYouKnow;
};
const DidYouKnow = ({ data }: Props) => (
<section className={styles.container}>
<h2 className='heading heading__secondary'>Did you know</h2>
{!!data.trivia?.total && (
<section>
<h3 className='heading heading__tertiary'>Trivia</h3>
<div dangerouslySetInnerHTML={{ __html: data.trivia.html }}></div>
</section>
)}
{!!data.quotes?.total && (
<section>
<h3 className='heading heading__tertiary'>Quotes</h3>
<div dangerouslySetInnerHTML={{ __html: data.quotes.html }}></div>
</section>
)}
{!!data.trademark?.total && (
<section>
<h3 className='heading heading__tertiary'>Trademark</h3>
<div dangerouslySetInnerHTML={{ __html: data.trademark.html }}></div>
</section>
)}
{!!data.nicknames.length && (
<section>
<h3 className='heading heading__tertiary'>Nicknames</h3>
<p>{data.nicknames.join(', ')}</p>
</section>
)}
{!!data.salary?.total && (
<section>
<h3 className='heading heading__tertiary'>Salary</h3>
<p>
<span>{data.salary.value} in </span>
<Link href={`/title/${data.salary.title.id}`}>
<a className={'link'}>{data.salary.title.text}</a>
</Link>
<span> ({data.salary.title.year})</span>
</p>
</section>
)}
</section>
);
export default DidYouKnow;

View file

@ -1,184 +0,0 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import Name, { PersonalDetails } from 'src/interfaces/shared/name';
import styles from 'src/styles/modules/components/name/info.module.scss';
type Props = {
info: PersonalDetails;
accolades: Name['accolades'];
};
const PersonalDetails = ({ info, accolades }: Props) => {
const {
query: { nameId },
} = useRouter();
return (
<div className={styles.info}>
<section className={styles.accolades}>
<h2 className='heading heading__secondary'>Accolades</h2>
<div className={styles.accolades__container}>
{accolades.awards && (
<p>
<span>
Won {accolades.awards.wins} {accolades.awards.name}
</span>
<span> (out of {accolades.awards.nominations} nominations)</span>
</p>
)}
<p>
{accolades.wins} wins and {accolades.nominations} nominations in total
</p>
<p>
<Link href={`/name/${nameId}/awards`}>
<a className='link'>View all awards</a>
</Link>
</p>
</div>
</section>
<section className={styles.details}>
<h2 className='heading heading__secondary'>Personal details</h2>
<div className={styles.details__container}>
{!!info.officialSites.length && (
<p>
<span>Official sites: </span>
{info.officialSites.map((site, i) => (
<span key={site.url}>
{!!i && ', '}
<a href={site.url} className='link' target='_blank' rel='noreferrer'>
{site.name}
</a>
</span>
))}
</p>
)}
{!!info.alsoKnownAs.length && (
<p>
<span>Also known as: </span>
<span>{info.alsoKnownAs.join(', ')}</span>
</p>
)}
{info.height && (
<p>
<span>Height: </span>
<span>{info.height}</span>
</p>
)}
{info.birth && (
<p>
<span>Born: </span>
<span>{info.birth.date}</span>
<span>
{' '}
in{' '}
<Link href={`/search/name?birth_place=${info.birth.location}`}>
<a className='link'>{info.birth.location}</a>
</Link>
</span>
</p>
)}
{info.death.date && (
<p>
<span>Died: </span>
<span>{info.death.date}</span>
{info.death.location && (
<span>
{' '}
in{' '}
<Link href={`/search/name?death_place=${info.death.location}`}>
<a className='link'>{info.death.location}</a>
</Link>
</span>
)}
</p>
)}
{info.death.cause && (
<p>
<span>Death cause: </span>
<span>{info.death.cause}</span>
</p>
)}
{!!info.spouses?.length && (
<p>
<span>Spouses: </span>
{info.spouses.map((spouse, i) => (
<span key={spouse.name}>
{!!i && ', '}
{renderPersonNameWithLink(spouse)} {spouse.range}
{spouse.attributes && ' (' + spouse.attributes.join(', ') + ')'}
</span>
))}
</p>
)}
{!!info.children?.length && (
<p>
<span>Children: </span>
{info.children.map((child, i) => (
<span key={child.name}>
{!!i && ', '}
{renderPersonNameWithLink(child)}
</span>
))}
</p>
)}
{!!info.parents?.length && (
<p>
<span>Parents: </span>
{info.parents.map((parent, i) => (
<span key={parent.name}>
{!!i && ', '}
{renderPersonNameWithLink(parent)}
</span>
))}
</p>
)}
{!!info.relatives?.length && (
<p>
<span>Relatives: </span>
{info.relatives.map((relative, i) => (
<span key={relative.name}>
{!!i && ', '}
{renderPersonNameWithLink(relative)} ({relative.relation})
</span>
))}
</p>
)}
{!!info.otherWorks?.length && (
<p>
<span>Other Works: </span>
{info.otherWorks.map((work, i) => (
<span key={work.text}>
{!!i && ', '}
<span dangerouslySetInnerHTML={{ __html: work.text }} />
</span>
))}
</p>
)}
{!!info.publicity.total && (
<p>
<span>Publicity Listings: </span>
<span>{info.publicity.articles} Articles</span>,{' '}
<span>{info.publicity.interviews} Interviews</span>,{' '}
<span>{info.publicity.magazines} Magazines</span>,{' '}
<span>{info.publicity.pictorials} Pictorials</span>,{' '}
<span>{info.publicity.printBiographies} Print biographies</span>, and{' '}
<span>{info.publicity.filmBiographies} Biographies</span>
</p>
)}
</div>
</section>
</div>
);
};
export default PersonalDetails;
const renderPersonNameWithLink = (person: { name: string; id: string | null }) =>
person.id ? (
<Link href={`/name/${person.id}`}>
<a className='link'>{person.name}</a>
</Link>
) : (
<span>{person.name}</span>
);

View file

@ -1,34 +0,0 @@
import type { KnownFor as KnownForType } from 'src/interfaces/shared/name';
import { CardTitle } from 'src/components/card';
import styles from 'src/styles/modules/components/name/known-for.module.scss';
type Props = { data: KnownForType };
const KnownFor = ({ data }: Props) => {
if (!data.length) return null;
return (
<section className={styles.knownFor}>
<h2 className='heading heading__secondary'>Known For</h2>
<ul className={styles.container}>
{data.map(title => (
<CardTitle
key={title.id}
link={`/title/${title.id}`}
name={title.title}
titleType={title.type.text}
image={title.poster?.url}
year={title.releaseYear}
>
<p className={styles.item__role}>{getRoles(title)}</p>
</CardTitle>
))}
</ul>
</section>
);
};
const getRoles = (title: Props['data'][number]) =>
(title.summary.characters ?? title.summary.jobs)?.join(', ');
export default KnownFor;

View file

@ -1,6 +0,0 @@
export { default as Basic } from './Basic';
export { default as DidYouKnow } from './DidYouKnow';
export { default as Info } from './Info';
export { default as Credits } from './Credits';
export { default as KnownFor } from './KnownFor';
export { default as Bio } from './Bio';

View file

@ -1,9 +1,10 @@
import { Fragment } from 'react';
import Image from 'next/future/image';
import Link from 'next/link';
import { CardBasic } from 'src/components/card';
import { Basic } from 'src/interfaces/shared/title';
import { formatNumber, formatTime } from 'src/utils/helpers';
import styles from 'src/styles/modules/components/title/basic.module.scss';
import { formatNumber, formatTime, modifyIMDbImg } from '../../utils/helpers';
import { Basic } from '../../interfaces/shared/title';
import styles from '../../styles/modules/components/title/basic.module.scss';
type Props = {
className: string;
@ -18,92 +19,134 @@ const Basic = ({ data, className }: Props) => {
: data.releaseYear?.start;
return (
<CardBasic
<section
// role is valid but not known to jsx-a11y
// aria-description={`basic info for '${data.title}'`}
// style={{ backgroundImage: data.poster && `url(${data.poster?.url})` }}
className={`${styles.container} ${className}`}
image={data.poster?.url}
title={data.title}
>
<ul className={styles.meta} aria-label='quick facts'>
{data.status && data.status.id !== 'released' && (
<li className={styles.meta__text}>{data.status.text}</li>
)}
<li className={styles.meta__text}>{data.type.name}</li>
{data.releaseYear && <li className={styles.meta__text}>{releaseTime}</li>}
{data.ceritficate && <li className={styles.meta__text}>{data.ceritficate}</li>}
{data.runtime && <li className={styles.meta__text}>{formatTime(data.runtime)}</li>}
</ul>
<div className={styles.ratings}>
{data.ratings.avg && (
<>
<p className={styles.rating}>
<span className={styles.rating__num}>{data.ratings.avg}</span>
<svg className={styles.rating__icon}>
<use href='/svg/sprite.svg#icon-rating'></use>
</svg>
<span className={styles.rating__text}> Avg. rating</span>
</p>
<p className={styles.rating}>
<span className={styles.rating__num}>{formatNumber(data.ratings.numVotes)}</span>
<svg className={styles.rating__icon}>
<use href='/svg/sprite.svg#icon-like-dislike'></use>
</svg>
<span className={styles.rating__text}> No. of votes</span>
</p>
</>
)}
{data.ranking && (
<p className={styles.rating}>
<span className={styles.rating__num}>{formatNumber(data.ranking.position)}</span>
<svg className={styles.rating__icon}>
<use href='/svg/sprite.svg#icon-graph-rising'></use>
</svg>
<span className={styles.rating__text}>
{' '}
Popularity (
<span className={styles.rating__sub}>
{data.ranking.direction === 'UP'
? `\u2191${formatNumber(data.ranking.change)}`
: data.ranking.direction === 'DOWN'
? `\u2193${formatNumber(data.ranking.change)}`
: ''}
</span>
)
</span>
</p>
<div
className={styles.imageContainer}
style={{
backgroundImage:
data.poster && `url(${modifyIMDbImg(data.poster.url, 300)})`,
}}
>
{data.poster ? (
<Image
className={styles.image}
src={modifyIMDbImg(data.poster.url)}
alt={data.poster.caption}
priority
fill
sizes='300px'
/>
) : (
<svg className={styles.image__NA}>
<use href='/svg/sprite.svg#icon-image-slash' />
</svg>
)}
</div>
<div className={styles.info}>
<h1 className={`${styles.title} heading heading__primary`}>
{data.title}
</h1>
<ul className={styles.meta} aria-label='quick facts'>
{data.status.id !== 'released' && (
<li className={styles.meta__text}>{data.status.text}</li>
)}
<li className={styles.meta__text}>{data.type.name}</li>
{data.releaseYear && (
<li className={styles.meta__text}>{releaseTime}</li>
)}
{data.ceritficate && (
<li className={styles.meta__text}>{data.ceritficate}</li>
)}
{data.runtime && (
<li className={styles.meta__text}>{formatTime(data.runtime)}</li>
)}
</ul>
<div className={styles.ratings}>
{data.ratings.avg && (
<>
<p className={styles.rating}>
<span className={styles.rating__num}>{data.ratings.avg}</span>
<svg className={styles.rating__icon}>
<use href='/svg/sprite.svg#icon-rating'></use>
</svg>
<span className={styles.rating__text}> Avg. rating</span>
</p>
<p className={styles.rating}>
<span className={styles.rating__num}>
{formatNumber(data.ratings.numVotes)}
</span>
<svg className={styles.rating__icon}>
<use href='/svg/sprite.svg#icon-like-dislike'></use>
</svg>
<span className={styles.rating__text}> No. of votes</span>
</p>
</>
)}
{data.ranking && (
<p className={styles.rating}>
<span className={styles.rating__num}>
{formatNumber(data.ranking.position)}
</span>
<svg className={styles.rating__icon}>
<use href='/svg/sprite.svg#icon-graph-rising'></use>
</svg>
<span className={styles.rating__text}>
{' '}
Popularity (
<span className={styles.rating__sub}>
{data.ranking.direction === 'UP'
? `\u2191${formatNumber(data.ranking.change)}`
: data.ranking.direction === 'DOWN'
? `\u2193${formatNumber(data.ranking.change)}`
: ''}
</span>
)
</span>
</p>
)}
</div>
{!!data.genres.length && (
<p className={styles.genres}>
<span className={styles.genres__heading}>Genres: </span>
{data.genres.map((genre, i) => (
<Fragment key={genre.id}>
{i > 0 && ', '}
<Link href={`/search/title?genres=${genre.id}`}>
<a className={styles.link}>{genre.text}</a>
</Link>
</Fragment>
))}
</p>
)}
<p className={styles.overview}>
<span className={styles.overview__heading}>Plot: </span>
<span className={styles.overview__text}>{data.plot || '-'}</span>
</p>
{data.primaryCrew.map(crewType => (
<p className={styles.crewType} key={crewType.type.id}>
<span className={styles.crewType__heading}>{`${crewType.type.category}: `}</span>
{crewType.crew.map((crew, i) => (
<Fragment key={crew.id}>
{i > 0 && ', '}
<Link href={`/name/${crew.id}`}>
<a className={styles.link}>{crew.name}</a>
</Link>
</Fragment>
))}
</p>
))}
</CardBasic>
{!!data.genres.length && (
<p className={styles.genres}>
<span className={styles.genres__heading}>Genres: </span>
{data.genres.map((genre, i) => (
<Fragment key={genre.id}>
{i > 0 && ', '}
<Link href={`/search/title?genres=${genre.id}`}>
<a className={styles.link}>{genre.text}</a>
</Link>
</Fragment>
))}
</p>
)}
{
<p className={styles.overview}>
<span className={styles.overview__heading}>Plot: </span>
<span className={styles.overview__text}>{data.plot || '-'}</span>
</p>
}
{data.primaryCrew.map(crewType => (
<p className={styles.crewType} key={crewType.type.id}>
<span className={styles.crewType__heading}>
{`${crewType.type.category}: `}
</span>
{crewType.crew.map((crew, i) => (
<Fragment key={crew.id}>
{i > 0 && ', '}
<Link href={`/name/${crew.id}`}>
<a className={styles.link}>{crew.name}</a>
</Link>
</Fragment>
))}
</p>
))}
</div>
</section>
);
};

View file

@ -1,6 +1,9 @@
import { CardCast } from 'src/components/card';
import { Cast } from 'src/interfaces/shared/title';
import styles from 'src/styles/modules/components/title/cast.module.scss';
import Image from 'next/future/image';
import Link from 'next/link';
import { Cast } from '../../interfaces/shared/title';
import { modifyIMDbImg } from '../../utils/helpers';
import styles from '../../styles/modules/components/title/cast.module.scss';
type Props = {
className: string;
@ -8,25 +11,46 @@ type Props = {
};
const Cast = ({ className, cast }: Props) => {
if (!cast.length) return null;
if (!cast.length) return <></>;
return (
<section className={`${className} ${styles.container}`}>
<h2 className='heading heading__secondary'>Cast</h2>
<ul className={styles.cast}>
{cast.map(member => (
<CardCast
key={member.id}
link={`/name/${member.id}`}
name={member.name}
image={member.image}
characters={member.characters}
attributes={member.attributes}
/>
<li key={member.id} className={styles.member}>
<div className={styles.member__imgContainer}>
{member.image ? (
<Image
src={modifyIMDbImg(member.image, 400)}
alt=''
fill
className={styles.member__img}
sizes='200px'
/>
) : (
<svg className={styles.member__imgNA}>
<use href='/svg/sprite.svg#icon-image-slash' />
</svg>
)}
</div>
<div className={styles.member__textContainer}>
<p>
<Link href={`/name/${member.id}`}>
<a className={styles.member__name}>{member.name}</a>
</Link>
</p>
<p className={styles.member__role}>
{member.characters?.join(', ')}
{member.attributes && (
<span> ({member.attributes.join(', ')})</span>
)}
</p>
</div>
</li>
))}
</ul>
</section>
);
};
export default Cast;

View file

@ -1,6 +1,7 @@
import Link from 'next/link';
import { DidYouKnow } from 'src/interfaces/shared/title';
import styles from 'src/styles/modules/components/title/did-you-know.module.scss';
import { Fragment } from 'react';
import { DidYouKnow } from '../../interfaces/shared/title';
import styles from '../../styles/modules/components/title/did-you-know.module.scss';
type Props = {
data: DidYouKnow;

View file

@ -1,16 +1,17 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import { Info } from 'src/interfaces/shared/title';
import { formatMoney, formatTime } from 'src/utils/helpers';
import styles from 'src/styles/modules/components/title/info.module.scss';
import { NextRouter } from 'next/router';
import { Info } from '../../interfaces/shared/title';
import { formatMoney, formatTime } from '../../utils/helpers';
import styles from '../../styles/modules/components/title/info.module.scss';
type Props = {
info: Info;
className: string;
router: NextRouter;
};
const Info = ({ info, className }: Props) => {
const router = useRouter();
const Info = ({ info, className, router }: Props) => {
const { titleId } = router.query;
const { boxOffice, details, meta, keywords, technicalSpecs, accolades } =
info;
@ -19,7 +20,7 @@ const Info = ({ info, className }: Props) => {
<div className={`${className} ${styles.info}`}>
{meta.infoEpisode && (
<section className={styles.episodeInfo}>
<h2 className="heading heading__secondary">Episode info</h2>
<h2 className='heading heading__secondary'>Episode info</h2>
<div className={styles.episodeInfo__container}>
{meta.infoEpisode.numSeason && (
<p className={styles.series}>
@ -49,14 +50,14 @@ const Info = ({ info, className }: Props) => {
{meta.infoEpisode.prevId && (
<p>
<Link href={`/title/${meta.infoEpisode.prevId}`}>
<a className="link">Go to previous episode</a>
<a className='link'>Go to previous episode</a>
</Link>
</p>
)}
{meta.infoEpisode.nextId && (
<p>
<Link href={`/title/${meta.infoEpisode.nextId}`}>
<a className="link">Go to next episode</a>
<a className='link'>Go to next episode</a>
</Link>
</p>
)}
@ -65,7 +66,7 @@ const Info = ({ info, className }: Props) => {
)}
{meta.infoSeries && (
<section className={styles.seriesInfo}>
<h2 className="heading heading__secondary">Series info</h2>
<h2 className='heading heading__secondary'>Series info</h2>
<div className={styles.seriesInfo__container}>
<p>
<span>Total Seasons: </span>
@ -81,19 +82,19 @@ const Info = ({ info, className }: Props) => {
</p>
<p>
<Link href={`/title/${titleId}/episodes`}>
<a className="link">See all Episodes</a>
<a className='link'>See all Episodes</a>
</Link>
</p>
</div>
</section>
)}
<section className={styles.accolades}>
<h2 className="heading heading__secondary">Accolades</h2>
<h2 className='heading heading__secondary'>Accolades</h2>
<div className={styles.accolades__container}>
{accolades.topRating && (
<p>
<Link href={`/chart/top`}>
<a className="link">Top rated (#{accolades.topRating})</a>
<a className='link'>Top rated (#{accolades.topRating})</a>
</Link>
</p>
)}
@ -111,21 +112,24 @@ const Info = ({ info, className }: Props) => {
</p>
<p>
<Link href={`/title/${titleId}/awards`}>
<a className="link">View all awards</a>
<a className='link'>View all awards</a>
</Link>
</p>
</div>
</section>
{!!keywords.total && (
<section className={styles.keywords}>
<h2 className="heading heading__secondary">Keywords</h2>
<h2 className='heading heading__secondary'>Keywords</h2>
<ul className={styles.keywords__container}>
{keywords.list.map(word => (
<li className={styles.keywords__item} key={word}>
<Link
href={`/search/keyword/?keywords=${word.replace(/\s/g, '-')}`}
href={`/search/keyword/?keywords=${word.replaceAll(
' ',
'-'
)}`}
>
<a className="link">{word}</a>
<a className='link'>{word}</a>
</Link>
</li>
))}
@ -134,7 +138,7 @@ const Info = ({ info, className }: Props) => {
)}
{!!Object.keys(details).length && (
<section className={styles.details}>
<h2 className="heading heading__secondary">Details</h2>
<h2 className='heading heading__secondary'>Details</h2>
<div className={styles.details__container}>
{details.releaseDate && (
<p>
@ -155,7 +159,7 @@ const Info = ({ info, className }: Props) => {
<Link
href={`/search/title/?country_of_origin=${country.id}`}
>
<a className="link">{country.text}</a>
<a className='link'>{country.text}</a>
</Link>
</span>
))}
@ -167,7 +171,7 @@ const Info = ({ info, className }: Props) => {
{details.officialSites.sites.map((site, i) => (
<span key={site.url}>
{!!i && ', '}
<a href={site.url} className="link">
<a href={site.url} className='link'>
{site.name}
</a>
</span>
@ -181,7 +185,7 @@ const Info = ({ info, className }: Props) => {
<span key={lang.id}>
{!!i && ', '}
<Link href={`/search/title/?primary_language=${lang.id}`}>
<a className="link">{lang.text}</a>
<a className='link'>{lang.text}</a>
</Link>
</span>
))}
@ -200,7 +204,7 @@ const Info = ({ info, className }: Props) => {
<span key={loc}>
{!!i && ', '}
<Link href={`/search/title/?locations=${loc}`}>
<a className="link">{loc}</a>
<a className='link'>{loc}</a>
</Link>
</span>
))}
@ -213,7 +217,7 @@ const Info = ({ info, className }: Props) => {
<span key={co.id}>
{!!i && ', '}
<Link href={`/company/${co.id}`}>
<a className="link">{co.name}</a>
<a className='link'>{co.name}</a>
</Link>
</span>
))}
@ -224,7 +228,7 @@ const Info = ({ info, className }: Props) => {
)}
{!!Object.keys(boxOffice).length && (
<section className={styles.boxoffice}>
<h2 className="heading heading__secondary">Box office</h2>
<h2 className='heading heading__secondary'>Box office</h2>
<div className={styles.boxoffice__container}>
{boxOffice.budget && (
<p>
@ -276,7 +280,7 @@ const Info = ({ info, className }: Props) => {
)}
{!!Object.keys(technicalSpecs).length && (
<section className={styles.technical}>
<h2 className="heading heading__secondary">Technical specs</h2>
<h2 className='heading heading__secondary'>Technical specs</h2>
<div className={styles.technical__container}>
{technicalSpecs.runtime && (
<p>
@ -292,7 +296,7 @@ const Info = ({ info, className }: Props) => {
<span key={color.id}>
{!!i && ', '}
<Link href={`/search/title/?colors=${color.id}`}>
<a className="link">{color.name}</a>
<a className='link'>{color.name}</a>
</Link>
</span>
))}
@ -307,7 +311,7 @@ const Info = ({ info, className }: Props) => {
<span key={sound.id}>
{!!i && ', '}
<Link href={`/search/title/?sound_mixes=${sound.id}`}>
<a className="link">{sound.name}</a>
<a className='link'>{sound.name}</a>
</Link>
</span>
))}

View file

@ -1,17 +1,18 @@
import Image from 'next/future/image';
import Link from 'next/link';
import { Media } from 'src/interfaces/shared';
import { getProxiedIMDbImgUrl, modifyIMDbImg } from 'src/utils/helpers';
import styles from 'src/styles/modules/components/media/media.module.scss';
import { NextRouter } from 'next/router';
import { Media } from '../../interfaces/shared/title';
import { modifyIMDbImg } from '../../utils/helpers';
import styles from '../../styles/modules/components/title/media.module.scss';
type Props = {
className: string;
media: Media;
router: NextRouter;
};
// TODO: refactor this component.
const Media = ({ className, media }: Props) => {
const Media = ({ className, media, router }: Props) => {
return (
<div className={`${className} ${styles.media}`}>
{(media.trailer || !!media.videos.total) && (
@ -20,20 +21,21 @@ const Media = ({ className, media }: Props) => {
<div className={styles.videos__container}>
{media.trailer && (
<div className={styles.trailer}>
<div key={router.asPath} className={styles.trailer}>
<video
aria-label='trailer video'
// it's a relatively new tag. hence jsx-all1 complains
aria-description={media.trailer.caption}
controls
playsInline
poster={getProxiedIMDbImgUrl(modifyIMDbImg(media.trailer.thumbnail))}
poster={modifyIMDbImg(media.trailer.thumbnail)}
className={styles.trailer__video}
preload='none'
>
{media.trailer.urls.map(source => (
<source
key={source.url}
type={source.mimeType}
src={getProxiedIMDbImgUrl(source.url)}
src={source.url}
data-res={source.resolution}
/>
))}
@ -74,7 +76,9 @@ const Media = ({ className, media }: Props) => {
fill
sizes='400px'
/>
<figcaption className={styles.image__caption}>{image.caption.plainText}</figcaption>
<figcaption className={styles.image__caption}>
{image.caption.plainText}
</figcaption>
</figure>
))}
</div>

View file

@ -1,6 +1,8 @@
import { CardTitle } from 'src/components/card';
import { MoreLikeThis } from 'src/interfaces/shared/title';
import styles from 'src/styles/modules/components/title/more-like-this.module.scss';
import Image from 'next/future/image';
import Link from 'next/link';
import { MoreLikeThis } from '../../interfaces/shared/title';
import { formatNumber, modifyIMDbImg } from '../../utils/helpers';
import styles from '../../styles/modules/components/title/more-like-this.module.scss';
type Props = {
className: string;
@ -8,22 +10,52 @@ type Props = {
};
const MoreLikeThis = ({ className, data }: Props) => {
if (!data.length) return null;
if (!data.length) return <></>;
return (
<section className={`${className} ${styles.morelikethis}`}>
<h2 className='heading heading__secondary'>More like this</h2>
<ul className={styles.container}>
{data.map(title => (
<CardTitle
key={title.id}
link={`/title/${title.id}`}
name={title.title}
titleType={title.type.text}
image={title.poster?.url}
year={title.releaseYear}
ratings={title.ratings}
/>
<li key={title.id}>
<Link href={`/title/${title.id}`}>
<a className={styles.item}>
<div className={styles.item__imgContainer}>
{title.poster ? (
<Image
src={modifyIMDbImg(title.poster.url, 400)}
alt=''
fill
className={styles.item__img}
sizes='200px'
/>
) : (
<svg className={styles.item__imgNA}>
<use href='/svg/sprite.svg#icon-image-slash' />
</svg>
)}
</div>
<div className={styles.item__textContainer}>
<h3 className={`heading ${styles.item__heading}`}>
{title.title}
</h3>
{title.ratings.avg && (
<p className={styles.item__rating}>
<span className={styles.item__ratingNum}>
{title.ratings.avg}
</span>
<svg className={styles.item__ratingIcon}>
<use href='/svg/sprite.svg#icon-rating'></use>
</svg>
<span>
({formatNumber(title.ratings.numVotes)} votes)
</span>
</p>
)}
</div>
</a>
</Link>
</li>
))}
</ul>
</section>

View file

@ -1,20 +1,20 @@
import { useRouter } from 'next/router';
import { NextRouter } from 'next/router';
import Link from 'next/link';
import { Reviews } from 'src/interfaces/shared/title';
import { formatNumber } from 'src/utils/helpers';
import styles from 'src/styles/modules/components/title/reviews.module.scss';
import { Reviews } from '../../interfaces/shared/title';
import { formatNumber } from '../../utils/helpers';
import styles from '../../styles/modules/components/title/reviews.module.scss';
type Props = {
reviews: Reviews;
router: NextRouter;
};
const Reviews = ({ reviews }: Props) => {
const router = useRouter();
const Reviews = ({ reviews, router }: Props) => {
const { titleId } = router.query;
return (
<section className={styles.reviews}>
<h2 className="heading heading__secondary">Reviews</h2>
<h2 className='heading heading__secondary'>Reviews</h2>
{reviews.featuredReview && (
<article className={styles.reviews__reviewContainer}>
@ -38,7 +38,7 @@ const Reviews = ({ reviews }: Props) => {
{' '}
by{' '}
<Link href={`/user/${reviews.featuredReview.reviewer.id}`}>
<a className="link">{reviews.featuredReview.reviewer.name}</a>
<a className='link'>{reviews.featuredReview.reviewer.name}</a>
</Link>
</span>
<span> on {reviews.featuredReview.date}.</span>
@ -58,21 +58,21 @@ const Reviews = ({ reviews }: Props) => {
<div className={styles.reviews__stats}>
<p>
<Link href={`/title/${titleId}/reviews`}>
<a className="link">
<a className='link'>
{formatNumber(reviews.numUserReviews)} User reviews
</a>
</Link>
</p>
<p>
<Link href={`/title/${titleId}/externalreviews`}>
<a className="link">
<a className='link'>
{formatNumber(reviews.numCriticReviews)} Critic reviews
</a>
</Link>
</p>
<p>
<Link href={`/title/${titleId}/criticreviews`}>
<a className="link"> {reviews.metacriticScore} Metascore</a>
<a className='link'> {reviews.metacriticScore} Metascore</a>
</Link>
</p>
</div>

View file

@ -1,6 +0,0 @@
export { default as Basic } from './Basic';
export { default as Cast } from './Cast';
export { default as DidYouKnow } from './DidYouKnow';
export { default as Info } from './Info';
export { default as MoreLikeThis } from './MoreLikeThis';
export { default as Reviews } from './Reviews';

View file

@ -1,13 +1,10 @@
import React, { useState, createContext, ReactNode } from 'react';
import { isLocalStorageAvailable } from 'src/utils/helpers';
const getInitialTheme = () => {
// for server-side rendering, as window isn't availabe there
if (typeof window === 'undefined') return 'light';
const userPrefersTheme = (
isLocalStorageAvailable() ? window.localStorage.getItem('theme') : null
) as 'light' | 'dark' | null;
const userPrefersTheme = window.localStorage.getItem('theme') || null;
const browserPrefersDarkTheme = window.matchMedia(
'(prefers-color-scheme: dark)'
).matches;
@ -28,7 +25,7 @@ const updateMetaTheme = () => {
const initialContext = {
theme: '',
setTheme: (theme: ReturnType<typeof getInitialTheme>) => { },
setTheme: (theme: string) => {},
};
export const themeContext = createContext(initialContext);
@ -36,9 +33,9 @@ export const themeContext = createContext(initialContext);
const ThemeProvider = ({ children }: { children: ReactNode }) => {
const [curTheme, setCurTheme] = useState(getInitialTheme);
const setTheme = (theme: typeof curTheme) => {
const setTheme = (theme: string) => {
setCurTheme(theme);
if (isLocalStorageAvailable()) window.localStorage.setItem('theme', theme);
window.localStorage.setItem('theme', theme);
document.documentElement.dataset.theme = theme;
updateMetaTheme();
};

View file

@ -1,31 +0,0 @@
import { useCallback, useEffect, useState } from 'react';
import { useRouter } from 'next/router';
/**
* for showing progress bar. could've used nprogress package, but didn't feel like it
* @returns isPageLoading: as the name suggests.
* @returns key: a unique key(in reality, a part of url) telling whether the page has changed or not
*/
const useIsPageLoading = () => {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const handleStart = useCallback(() => setIsLoading(true), []);
const handleEnd = useCallback(() => setIsLoading(false), []);
useEffect(() => {
router.events.on('routeChangeStart', handleStart);
router.events.on('routeChangeComplete', handleEnd);
router.events.on('routeChangeError', handleEnd);
return () => {
router.events.off('routeChangeStart', handleStart);
router.events.off('routeChangeComplete', handleEnd);
router.events.off('routeChangeError', handleEnd);
};
}, [router, handleStart, handleEnd]);
return { isPageLoading: isLoading, key: router.asPath };
};
export default useIsPageLoading;

View file

@ -1,86 +0,0 @@
import {
ResultMetaTitleTypes,
ResultMetaTypes,
} from 'src/interfaces/shared/search';
export default interface RawFind {
props: {
pageProps: {
findPageMeta: {
searchTerm: string;
includeAdult: false;
isExactMatch: boolean;
searchType?: ResultMetaTypes;
titleSearchType?: ResultMetaTitleTypes[];
};
nameResults: {
results: Array<{
id: string;
displayNameText: string;
knownForJobCategory: string | 0;
knownForTitleText: string | 0;
knownForTitleYear: string | 0;
avatarImageModel?: {
url: string;
// maxHeight: number;
// maxWidth: number;
caption: string;
};
akaName?: string;
}>;
// nextCursor?: string;
// hasExactMatches?: boolean;
};
titleResults: {
results: Array<{
id: string;
titleNameText: string;
titleReleaseText?: string;
titleTypeText: string;
titlePosterImageModel?: {
url: string;
// maxHeight: number;
// maxWidth: number;
caption: string;
};
topCredits: Array<string>;
imageType: string;
seriesId?: string;
seriesNameText?: string;
seriesReleaseText?: string;
seriesTypeText?: string;
seriesSeasonText?: string;
seriesEpisodeText?: string;
}>;
// nextCursor?: string;
// hasExactMatches?: boolean;
};
companyResults: {
results: Array<{
id: string;
companyName: string;
countryText: string;
typeText: string | 0;
}>;
// nextCursor?: string;
// hasExactMatches?: boolean;
};
keywordResults: {
results: Array<{
id: string;
keywordText: string;
numTitles: number;
}>;
// nextCursor?: string;
// hasExactMatches?: boolean;
};
resultsSectionOrder: Array<string>;
};
};
}
// const x: RawFind<'tt'> = {
// props: {pageProps: {findPageMeta: {
// titleSearchType: ['MOVIE']
// }}}
// }

File diff suppressed because it is too large Load diff

View file

@ -18,7 +18,7 @@ export default interface RawTitle {
restrictionReason: Array<string>;
unrestrictedTotal: number;
};
} | null;
};
canHaveEpisodes: boolean;
series?: {
episodeNumber: {
@ -125,7 +125,7 @@ export default interface RawTitle {
runtime: {
value: number;
};
description?: {
description: {
value: string;
language: string;
};
@ -516,11 +516,9 @@ export default interface RawTitle {
canRate: {
isRatable: boolean;
};
titleGenres: {
titleCardGenres: {
genres: Array<{
genre: {
text: string;
};
text: string;
}>;
};
canHaveEpisodes: boolean;

View file

@ -1,3 +1,5 @@
import { AppError as AppErrorClass } from 'src/utils/helpers';
export type AppError = Omit<InstanceType<typeof AppErrorClass>, 'name'>;
export type AppError = {
message: string;
statusCode: number;
stack?: any;
};

View file

@ -1,6 +0,0 @@
import type Name from './name';
export type Media = Name['media']; // exactly the same in title and name
// forcefully makes array of individual elements of T, where t is any conditional type.
export type ToArray<T> = T extends any ? T[] : never;

View file

@ -1,39 +0,0 @@
import list from 'src/utils/fetchers/list';
// for full title
type List = Awaited<ReturnType<typeof list>>;
export type { List as default };
type DataTitle = {
image: string | null;
name: string;
url: string | null;
year: string;
certificate: string;
runtime: string;
genre: string;
plot: string;
rating: string;
metascore: string;
otherInfo: string[][];
};
type DataName = {
image: string | null;
name: string;
url: string | null;
job: string | null;
knownFor: string | null;
knownForLink: string | null;
about: string;
};
type DataImage = string;
export type DataKind = 'images' | 'titles' | 'names';
export type Data<T extends DataKind> = T extends 'images'
? DataImage
: T extends 'names'
? DataName
: DataTitle;

View file

@ -1,16 +0,0 @@
import cleanName from 'src/utils/cleaners/name';
type Name = ReturnType<typeof cleanName>;
export type { Name as default };
export type Basic = Name['basic'];
export type Media = Name['media'];
export type Credits = Name['credits'];
export type DidYouKnow = Name['didYouKnow'];
export type PersonalDetails = Name['personalDetails'];
export type KnownFor = Name['knownFor'];

View file

@ -1,28 +0,0 @@
import cleanFind from 'src/utils/cleaners/find';
import { resultTitleTypes, resultTypes } from 'src/utils/constants/find';
type BasicSearch = ReturnType<typeof cleanFind>;
export type { BasicSearch as default };
export type Titles = BasicSearch['titles'];
export type People = BasicSearch['people'];
export type Companies = BasicSearch['companies'];
export type Keywords = BasicSearch['keywords'];
// q=babylon&s=tt&ttype=ft&exact=true
export type FindQueryParams = {
q: string;
exact?: 'true';
s?: QueryTypes;
ttype?: QueryTitleTypes;
};
export type ResultMetaTypes = typeof resultTypes.types[number]['id'] | null;
export type ResultMetaTitleTypes =
| typeof resultTitleTypes.types[number]['id']
| null;
export type QueryTypes = typeof resultTypes.types[number]['val'];
export type QueryTitleTypes = typeof resultTitleTypes.types[number]['val'];

View file

@ -1,5 +1,7 @@
import cleanTitle from 'src/utils/cleaners/title';
import title from 'src/utils/fetchers/title';
import cleanTitle from '../../utils/cleaners/title';
import title from '../../utils/fetchers/title';
export type AxiosTitleRes = Awaited<ReturnType<typeof title>>;
// for full title
type Title = ReturnType<typeof cleanTitle>;

52
src/layouts/Footer.tsx Normal file
View file

@ -0,0 +1,52 @@
import { FC } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import styles from '../styles/modules/layout/footer.module.scss';
const Footer: FC = () => {
const { pathname } = useRouter();
const className = (link: string) =>
pathname === link ? styles.nav__linkActive : styles.nav__link;
return (
<footer id='footer' className={styles.footer}>
<nav aria-label='primary navigation' className={styles.nav}>
<ul className={styles.list}>
<li className={styles.nav__item}>
<Link href='/about'>
<a className={className('/about')}>About</a>
</Link>
</li>
<li className={styles.nav__item}>
<Link href='/privacy'>
<a className={className('/privacy')}>Privacy</a>
</Link>
</li>
<li className={styles.nav__item}>
<Link href='/contact'>
<a className={className('/contact')}>Contact</a>
</Link>
</li>
<li className={styles.nav__item}>
<a href='#' className={styles.nav__link}>
Back to top
</a>
</li>
</ul>
</nav>
<p className={styles.licence}>
Licensed under&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,22 +1,37 @@
import { ReactNode } from 'react';
// import dynamic from 'next/dynamic';
import Link from 'next/link';
import ThemeToggler from 'src/components/buttons/ThemeToggler';
import styles from 'src/styles/modules/layout/header.module.scss';
import styles from '../styles/modules/layout/header.module.scss';
import ThemeToggler from '../components/buttons/ThemeToggler';
type Props = { full?: boolean; originalPath?: string };
// const ThemeToggler = dynamic(
// () => import('../components/buttons/ThemeToggler'),
// { ssr: false }
// );
const Header = ({ full, originalPath }: Props) => {
type Props = { full?: boolean; children?: ReactNode };
const Header = (props: Props) => {
return (
<header id='header' className={`${styles.header} ${full ? styles.header__about : ''}`}>
<header
id='header'
className={`${styles.header} ${props.full ? styles.header__about : ''}`}
>
<div className={styles.topbar}>
<Link href='/find'>
<Link href='/about'>
<a aria-label='go to homepage' className={styles.logo}>
<svg className={styles.logo__icon} role='img' aria-hidden>
<svg
className={styles.logo__icon}
focusable='false'
role='img'
aria-hidden='true'
>
<use href='/svg/sprite.svg#icon-logo'></use>
</svg>
<span className={styles.logo__text}>libremdb</span>
</a>
</Link>
{full && (
{props.full && (
<nav className={styles.nav}>
<ul className={styles.nav__list}>
<li className={styles.nav__item}>
@ -37,40 +52,27 @@ const Header = ({ full, originalPath }: Props) => {
</ul>
</nav>
)}
<div className={styles.misc}>
<a href={`https://www.imdb.com${originalPath ?? ''}`} target='_blank' rel='noreferrer'>
<span className='visually-hidden'>View on IMDb (opens in new tab)</span>
<svg className='icon' role='img' aria-hidden>
<use href='/svg/sprite.svg#icon-external-link'></use>
</svg>
</a>
<Link href='/find'>
<a>
<span className='visually-hidden'>Search</span>
<svg className='icon' role='img' aria-hidden>
<use href='/svg/sprite.svg#icon-search'></use>
</svg>
</a>
</Link>
<ThemeToggler className={styles.themeToggler} />
</div>
<ThemeToggler className={styles.themeToggler} />
</div>
{full && (
{props.full && (
<div className={styles.hero}>
<h1 className={`heading heading__primary ${styles.hero__text}`}>
A free & open source IMDb front-end
</h1>
<p className={styles.hero__more}>
inspired by projects like{' '}
inspired by projects like&nbsp;
<a href='https://codeberg.org/teddit/teddit' className='link'>
teddit
</a>
,{' '}
,&nbsp;
<a href='https://github.com/zedeus/nitter' className='link'>
nitter
</a>
, and{' '}
<a href='https://github.com/digitalblossom/alternative-frontends' className='link'>
, and&nbsp;
<a
href='https://github.com/digitalblossom/alternative-frontends'
className='link'
>
many others
</a>
.

View file

@ -1,18 +1,17 @@
import { ReactNode } from 'react';
import React from 'react';
import Footer from './Footer';
import Header from './Header';
type Props = {
full?: true;
children: ReactNode;
full?: boolean;
children: React.ReactNode;
className: string;
originalPath?: string;
};
const Layout = ({ full, children, className, originalPath }: Props) => {
const Layout = ({ full, children, className }: Props) => {
return (
<>
<Header full={full} originalPath={originalPath} />
<Header full={full} />
<main id='main' className={`main ${className}`}>
{children}
</main>

View file

@ -1,7 +1,7 @@
import ErrorInfo from 'src/components/error/ErrorInfo';
import ErrorInfo from '../components/Error/ErrorInfo';
const Error404 = () => {
return <ErrorInfo message='Not found, sorry.' statusCode={404} />;
return <ErrorInfo />;
};
export default Error404;

View file

@ -1,6 +1,6 @@
import ErrorInfo from 'src/components/error/ErrorInfo';
import ErrorInfo from '../components/Error/ErrorInfo';
const Error500 = () => {
return <ErrorInfo message='Server messed up, sorry.' statusCode={500} />;
return <ErrorInfo message='server messed up, sorry.' statusCode={500} />;
};
export default Error500;

View file

@ -1,25 +0,0 @@
import type { GetServerSideProps, InferGetServerSidePropsType } from 'next';
import ErrorInfo from 'src/components/error/ErrorInfo';
const error = {
statusCode: 404,
message: 'Not found, sorry.',
} as const;
type Props = InferGetServerSidePropsType<typeof getServerSideProps>;
const Error404 = ({ originalPath }: Props) => {
return <ErrorInfo {...error} originalPath={originalPath} />;
};
export default Error404;
type Data = { originalPath: string };
type Params = { error: string[] };
export const getServerSideProps: GetServerSideProps<Data, Params> = async ctx => {
ctx.res.statusCode = error.statusCode;
ctx.res.statusMessage = error.message;
return { props: { originalPath: ctx.resolvedUrl } };
};

View file

@ -1,22 +1,38 @@
import { AppProps } from 'next/app';
import ProgressBar from 'src/components/loaders/ProgressBar';
import ErrorBoundary from 'src/components/error/ErrorBoundary';
import ThemeProvider from 'src/context/theme-context';
import usePageLoading from 'src/hooks/usePageLoading';
import 'src/styles/main.scss';
import { useCallback, useEffect, useState } from 'react';
import type { AppProps } from 'next/app';
import { useRouter } from 'next/router';
import ProgressBar from '../components/loaders/ProgressBar';
import ThemeProvider from '../context/theme-context';
import '../styles/main.scss';
const ModifiedApp = ({ Component, pageProps }: AppProps) => {
const { isPageLoading, key } = usePageLoading();
// for showing progress bar
// could've used nprogress package, but didn't feel like it
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const handleStart = useCallback(() => setIsLoading(true), []);
const handleEnd = useCallback(() => setIsLoading(false), []);
useEffect(() => {
router.events.on('routeChangeStart', handleStart);
router.events.on('routeChangeComplete', handleEnd);
router.events.on('routeChangeError', handleEnd);
return () => {
router.events.off('routeChangeStart', handleStart);
router.events.off('routeChangeComplete', handleEnd);
router.events.off('routeChangeError', handleEnd);
};
}, [router, handleStart, handleEnd]);
//
return (
<ThemeProvider>
{isPageLoading && <ProgressBar />}
<ErrorBoundary>
<Component
{...pageProps}
key={key} /* passing key to force react to remount components */
/>
</ErrorBoundary>
{isLoading && <ProgressBar />}
<Component {...pageProps} />
</ThemeProvider>
);
};

View file

@ -3,25 +3,15 @@ import Document, { Html, Head, Main, NextScript } from 'next/document';
// for preventing Flash of inAccurate coloR Theme(fart)
// chris coyier came up with that acronym(https://css-tricks.com/flash-of-inaccurate-color-theme-fart/)
const setInitialTheme = `
(() => {
document.documentElement.dataset.js = true;
const isLocalStorageAvailable = () => {
try {
window.localStorage.getItem('test');
return true;
} catch (e) {
return false;
}
}
let theme = 'light';
let themeColor = '#ffe5ef';
const userPrefersTheme = isLocalStorageAvailable() ? window.localStorage.getItem('theme') : null;
const browserPrefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (userPrefersTheme) theme = userPrefersTheme;
else if (browserPrefersDarkTheme) theme = 'dark';
if(theme === 'dark') themeColor = '#141c2e';
document.documentElement.dataset.theme = theme;
document.querySelector('meta[name="theme-color"]').setAttribute('content', themeColor);
document.documentElement.dataset.js = true;
document.documentElement.dataset.theme = (() => {
const userPrefersTheme = window.localStorage.getItem('theme') || null;
const browserPrefersDarkTheme = window.matchMedia(
'(prefers-color-scheme: dark)'
).matches;
if (userPrefersTheme) return userPrefersTheme;
else if (browserPrefersDarkTheme) return 'dark';
else return 'light';
})();
`;

View file

@ -1,7 +1,9 @@
/* eslint-disable react/no-unescaped-entities */
import Link from 'next/link';
import Meta from 'src/components/meta/Meta';
import Layout from 'src/components/layout';
import styles from 'src/styles/modules/pages/about/about.module.scss';
import Meta from '../../components/Meta/Meta';
import Layout from '../../layouts/Layout';
import styles from '../../styles/modules/pages/about/about.module.scss';
const About = () => {
return (
@ -89,20 +91,16 @@ const About = () => {
<p className={styles.faq__description}>
Replace `imdb.com` in any IMDb URL with any of the instances.
For example: `
<a
href='https://imdb.com/title/tt1049413'
className='link'
target='_blank'
rel='noreferrer'
>
<a href='https://imdb.com/title/tt1049413' className='link'>
imdb.com/title/tt1049413
</a>
` to `
<Link href='/title/tt1049413'>
<a className='link'>
{process.env.NEXT_PUBLIC_URL || ''}/title/tt1049413
</a>
</Link>
<a
href='https://libremdb.iket.me/title/tt1049413'
className='link'
>
libremdb.iket.me/title/tt1049413
</a>
` . To avoid changing the URLs manually, you can use extensions
like{' '}
<a
@ -135,21 +133,22 @@ const About = () => {
</details>
<details className={styles.faq}>
<summary className={styles.faq__summary}>
Is content served from third-parties, like Amazon?
I see connection being made to some Amazon domains.
</summary>
<p className={styles.faq__description}>
Nope, libremdb proxies all image and video requests through the
instance to avoid exposing your IP address, browser information
and other personally identifiable metadata (
<a
href='https://github.com/httpjamesm'
target='_blank'
rel='noopener noreferrer'
className='link'
>
Contributor
</a>
).
For now, images and videos are directly served from Amazon. If I
have enough time in the future, I'll implement a way to serve
the images from libremdb instead.
</p>
</details>
<details className={styles.faq}>
<summary className={styles.faq__summary}>
Will Amazon track me then?
</summary>
<p className={styles.faq__description}>
They may log your IP address, useragent, and other such
identifiers. I'd recommend using a VPN, or accessing the website
through TOR for mitigating this risk.
</p>
</details>
<details className={styles.faq}>

View file

@ -1,7 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
type ResponseData = { status: false; message: string };
export default async function handler(_: NextApiRequest, res: NextApiResponse<ResponseData>) {
res.status(400).json({ status: false, message: 'Not found' });
}

View file

@ -1,32 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import Find, { type FindQueryParams } from 'src/interfaces/shared/search';
import basicSearch from 'src/utils/fetchers/basicSearch';
import getOrSetApiCache from 'src/utils/getOrSetApiCache';
import { findKey } from 'src/utils/constants/keys';
import { AppError, cleanQueryStr } from 'src/utils/helpers';
type ResponseData =
| { status: true; data: { title: null | string; results: null | Find } }
| { status: false; message: string };
export default async function handler(req: NextApiRequest, res: NextApiResponse<ResponseData>) {
try {
if (req.method !== 'GET') throw new AppError('Invalid method', 400);
const queryObj = req.query as FindQueryParams | Record<string, never>;
const query = queryObj.q?.trim();
if (!query) {
return res.status(200).json({ status: true, data: { title: null, results: null } });
}
const entries = Object.entries(queryObj);
const queryStr = cleanQueryStr(entries);
const results = await getOrSetApiCache(findKey(queryStr), basicSearch, queryStr);
res.status(200).json({ status: true, data: { title: query, results } });
} catch (error: any) {
const { message = 'Not found', statusCode = 404 } = error;
res.status(statusCode).json({ status: false, message });
}
}

View file

@ -1,23 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type List from 'src/interfaces/shared/list';
import list from 'src/utils/fetchers/list';
import getOrSetApiCache from 'src/utils/getOrSetApiCache';
import { listKey } from 'src/utils/constants/keys';
import { AppError } from 'src/utils/helpers';
type ResponseData = { status: true; data: List } | { status: false; message: string };
export default async function handler(req: NextApiRequest, res: NextApiResponse<ResponseData>) {
try {
if (req.method !== 'GET') throw new AppError('Invalid method', 400);
const listId = req.query.listId as string;
const pageNum = req.query.page as string | undefined;
const data = await getOrSetApiCache(listKey(listId, pageNum), list, listId, pageNum);
res.status(200).json({ status: true, data });
} catch (error: any) {
const { message = 'Not found', statusCode = 404 } = error;
res.status(statusCode).json({ status: false, message });
}
}

View file

@ -1,94 +0,0 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { AxiosRequestHeaders } from 'axios';
import redis from 'src/utils/redis';
import axiosInstance from 'src/utils/axiosInstance';
import { mediaKey } from 'src/utils/constants/keys';
const dontCacheMedia =
process.env.USE_REDIS_FOR_API_ONLY === 'true' || process.env.USE_REDIS !== 'true';
const ttl = process.env.REDIS_CACHE_TTL_MEDIA ?? 30 * 60;
const getCleanReqHeaders = (headers: NextApiRequest['headers']) => {
const cleanHeaders: AxiosRequestHeaders = {};
if (headers.accept) cleanHeaders.accept = headers.accept;
if (headers.range) cleanHeaders.range = headers.range;
if (headers['accept-encoding'])
cleanHeaders['accept-encoding'] = headers['accept-encoding'].toString();
return cleanHeaders;
};
const resHeadersArr = ['content-range', 'content-length', 'content-type', 'accept-ranges'];
// checks if a url is pointing towards a video/image from imdb
const regex =
/^https:\/\/((m\.)?media-amazon\.com|imdb-video\.media-imdb\.com).*\.(jpg|jpeg|png|mp4|gif|webp).*$/;
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const mediaUrl = req.query.url as string | undefined;
const requestHeaders = getCleanReqHeaders(req.headers);
// 1. returning if query is illegal
if (!mediaUrl || !regex.test(mediaUrl))
return res.status(400).json({
success: false,
message: 'Invalid query',
});
// 2. sending streamed response if redis, or redis for media isn't enabled
if (dontCacheMedia) {
const mediaRes = await axiosInstance.get(mediaUrl, {
responseType: 'stream',
headers: requestHeaders,
});
// chromium browsers want a 206 response with specific headers. so, we gotta pass them on.
res.statusCode = mediaRes.status;
resHeadersArr.forEach(key => {
const val = mediaRes.headers[key];
if (val) res.setHeader(key, val);
});
mediaRes.data.pipe(res);
return;
}
// 3. else if resourced is cached, sending it
const cachedMedia = await redis.getBuffer(mediaKey(mediaUrl));
if (cachedMedia) {
res.setHeader('x-cached', 'true');
res.send(cachedMedia);
return;
}
// 4. else getting, caching and sending response
const { data } = await axiosInstance(mediaUrl, {
responseType: 'arraybuffer',
});
// saving in redis for 30 minutes
await redis.setex(mediaKey(mediaUrl), ttl, Buffer.from(data));
// sending media
res.setHeader('x-cached', 'false');
res.send(data);
// sending token response on any error
} catch {
res.status(404);
res.json({
success: false,
message: 'something went wrong',
});
return;
}
}
export const config = {
api: {
responseLimit: false,
},
};

View file

@ -1,22 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type Name from 'src/interfaces/shared/name';
import name from 'src/utils/fetchers/name';
import getOrSetApiCache from 'src/utils/getOrSetApiCache';
import { nameKey } from 'src/utils/constants/keys';
import { AppError } from 'src/utils/helpers';
type ResponseData = { status: true; data: Name } | { status: false; message: string };
export default async function handler(req: NextApiRequest, res: NextApiResponse<ResponseData>) {
try {
if (req.method !== 'GET') throw new AppError('Invalid method', 400);
const nameId = req.query.nameId as string;
const data = await getOrSetApiCache(nameKey(nameId), name, nameId);
res.status(200).json({ status: true, data });
} catch (error: any) {
const { message = 'Not found', statusCode = 404 } = error;
res.status(statusCode).json({ status: false, message });
}
}

View file

@ -1,21 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type Title from 'src/interfaces/shared/title';
import title from 'src/utils/fetchers/title';
import getOrSetApiCache from 'src/utils/getOrSetApiCache';
import { titleKey } from 'src/utils/constants/keys';
import { AppError } from 'src/utils/helpers';
type ResponseData = { status: true; data: Title } | { status: false; message: string };
export default async function handler(req: NextApiRequest, res: NextApiResponse<ResponseData>) {
try {
if (req.method !== 'GET') throw new AppError('Invalid method', 400);
const titleId = req.query.titleId as string;
const data = await getOrSetApiCache(titleKey(titleId), title, titleId);
res.status(200).json({ status: true, data });
} catch (error: any) {
const { message = 'Not found', statusCode = 404 } = error;
res.status(statusCode).json({ status: false, message });
}
}

View file

@ -1,6 +1,7 @@
import Meta from 'src/components/meta/Meta';
import Layout from 'src/components/layout';
import styles from 'src/styles/modules/pages/contact/contact.module.scss';
import Meta from '../../components/Meta/Meta';
import Layout from '../../layouts/Layout';
import styles from '../../styles/modules/pages/contact/contact.module.scss';
const Contact = () => {
return (
@ -14,50 +15,30 @@ const Contact = () => {
<h1 className={`heading heading__primary ${styles.contact__heading}`}>
Contact
</h1>
<div className={styles.list}>
<div className={styles.item}>
<p className={styles.item__text}>
For any issues, questions, bugs, or requests regarding the
service, you can go to{' '}
<a href='https://github.com/zyachel/libremdb' className='link'>
GitHub
</a>
.
</p>
<p className={styles.item__text}>
Alternatively, you can visit{' '}
<a
href='https://codeberg.org/zyachel/libremdb'
className='link'
>
the repository on Codeberg
</a>
.
</p>
</div>
{process.env.NEXT_PUBLIC_INSTANCE_MAIN_URL && (
<div className={styles.item}>
<p className={styles.item__text}>
If you have some questions related to this instance,{' '}
<a
href={process.env.NEXT_PUBLIC_INSTANCE_MAIN_URL}
className='link'
>
contact instance maintainer(s)
</a>
.
</p>
</div>
)}
<div className={styles.item}>
<p className={styles.item__text}>
In case you wish to contact me(the dev) personally,{' '}
<a href='https://iket.me/contact/' className='link'>
here you go
</a>
<span aria-label='smily text emoji'> :)</span>
</p>
</div>
<p className={styles.item}>
You can use{' '}
<a href='https://github.com/zyachel/libremdb' className='link'>
GitHub
</a>{' '}
or{' '}
<a href='https://codeberg.org/zyachel/libremdb' className='link'>
Codeberg
</a>{' '}
for general issues, questions, or requests.
</p>
<p className={styles.item}>
In case you wish to contact me personally, I'm reachable via{' '}
<a className='link' href='https://matrix.to/#/@ninal:matrix.org'>
[matrix]
</a>{' '}
and{' '}
<a className='link' href='mailto:aricla@protonmail.com'>
email
</a>
.
</p>
</div>
</section>
</Layout>

View file

@ -1,84 +0,0 @@
import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
import Layout from 'src/components/layout';
import ErrorInfo from 'src/components/error/ErrorInfo';
import Meta from 'src/components/meta/Meta';
import Results from 'src/components/find';
import Form from 'src/components/forms/find';
import Find, { FindQueryParams } from 'src/interfaces/shared/search';
import { AppError } from 'src/interfaces/shared/error';
import basicSearch from 'src/utils/fetchers/basicSearch';
import getOrSetApiCache from 'src/utils/getOrSetApiCache';
import { cleanQueryStr } from 'src/utils/helpers';
import { findKey } from 'src/utils/constants/keys';
import styles from 'src/styles/modules/pages/find/find.module.scss';
type Props = InferGetServerSidePropsType<typeof getServerSideProps>;
const getMetadata = (title: string | null) => ({
title: title || 'Search',
description: title
? `results for '${title}'`
: 'Search for anything on libremdb, a free & open source IMDb front-end',
});
const BasicSearch = ({ data: { title, results }, error, originalPath }: Props) => {
if (error) return <ErrorInfo {...error} originalPath={originalPath} />;
let layoutClassName = styles.find;
if (!title) layoutClassName += ' ' + styles.find__home;
return (
<>
<Meta {...getMetadata(title)} />
<Layout className={layoutClassName} originalPath={originalPath}>
{title && ( // only showing when user has searched for something
<Results results={results} title={title} className={styles.results} />
)}
<Form className={styles.form} />
</Layout>
</>
);
};
// TODO: use generics for passing in queryParams(to components) for better type-checking.
type Data = (
| { data: { title: string; results: Find }; error: null }
| { data: { title: null; results: null }; error: null }
| { data: { title: string; results: null }; error: AppError }
) & {
originalPath: string;
};
export const getServerSideProps: GetServerSideProps<Data, FindQueryParams> = async ctx => {
// sample query str: find/?q=babylon&s=tt&ttype=ft&exact=true
const queryObj = ctx.query as FindQueryParams;
const query = queryObj.q?.trim();
const originalPath = ctx.resolvedUrl;
if (!query) return { props: { data: { title: null, results: null }, error: null, originalPath } };
try {
const entries = Object.entries(queryObj);
const queryStr = cleanQueryStr(entries);
const res = await getOrSetApiCache(findKey(queryStr), basicSearch, queryStr);
return {
props: { data: { title: query, results: res }, error: null, originalPath },
};
} catch (error: any) {
const { message, statusCode } = error;
ctx.res.statusCode = statusCode;
ctx.res.statusMessage = message;
return {
props: {
error: { message, statusCode },
data: { title: query, results: null },
originalPath,
},
};
}
};
export default BasicSearch;

View file

@ -1,54 +0,0 @@
import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
import Meta from 'src/components/meta/Meta';
import Layout from 'src/components/layout';
import ErrorInfo from 'src/components/error/ErrorInfo';
import { Data, Meta as ListMeta, Pagination } from 'src/components/list';
import { AppError } from 'src/interfaces/shared/error';
import TList from 'src/interfaces/shared/list';
import getOrSetApiCache from 'src/utils/getOrSetApiCache';
import list from 'src/utils/fetchers/list';
import { listKey } from 'src/utils/constants/keys';
import styles from 'src/styles/modules/pages/list/list.module.scss';
type Props = InferGetServerSidePropsType<typeof getServerSideProps>;
const List = ({ data, error, originalPath }: Props) => {
if (error) return <ErrorInfo {...error} originalPath={originalPath} />;
const description = data.description || `List created by ${data.meta.by.name} (${data.meta.num} ${data.meta.type}).`
return (
<>
<Meta title={data.title} description={description} />
<Layout className={styles.list} originalPath={originalPath}>
<ListMeta title={data.title} description={data.description} meta={data.meta} />
{/* @ts-expect-error don't have time to fix it. just a type fluff. */}
<Data data={data.data} />
<Pagination pagination={data.pagination} />
</Layout>
</>
);
};
type TData = ({ data: TList; error: null } | { error: AppError; data: null }) & {
originalPath: string;
};
type Params = { listId: string };
export const getServerSideProps: GetServerSideProps<TData, Params> = async ctx => {
const listId = ctx.params!.listId;
const pageNum = (ctx.query.page as string | undefined) ?? '1';
const originalPath = ctx.resolvedUrl;
try {
const data = await getOrSetApiCache(listKey(listId, pageNum), list, listId, pageNum);
return { props: { data, error: null, originalPath } };
} catch (error: any) {
const { message = 'Internal server error', statusCode = 500 } = error;
ctx.res.statusCode = statusCode;
ctx.res.statusMessage = message;
return { props: { error: { message, statusCode }, data: null, originalPath } };
}
};
export default List;

View file

@ -1,67 +0,0 @@
import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
import Meta from 'src/components/meta/Meta';
import Layout from 'src/components/layout';
import ErrorInfo from 'src/components/error/ErrorInfo';
import Media from 'src/components/media/Media';
import { Basic, Credits, DidYouKnow, Info, Bio, KnownFor } from 'src/components/name';
import Name from 'src/interfaces/shared/name';
import { AppError } from 'src/interfaces/shared/error';
import name from 'src/utils/fetchers/name';
import getOrSetApiCache from 'src/utils/getOrSetApiCache';
import { getProxiedIMDbImgUrl } from 'src/utils/helpers';
import { nameKey } from 'src/utils/constants/keys';
import styles from 'src/styles/modules/pages/name/name.module.scss';
type Props = InferGetServerSidePropsType<typeof getServerSideProps>;
const NameInfo = ({ data, error, originalPath }: Props) => {
if (error) return <ErrorInfo {...error} originalPath={originalPath} />;
return (
<>
<Meta
title={data.basic.name}
description={data.basic.bio.short + '...'}
imgUrl={data.basic.poster?.url && getProxiedIMDbImgUrl(data.basic.poster.url)}
/>
<Layout className={styles.name} originalPath={originalPath}>
<Basic data={data.basic} className={styles.basic} />
<Media className={styles.media} media={data.media} />
<div className={styles.textarea}>
<KnownFor data={data.knownFor} />
<Bio bio={data.basic.bio.full} />
</div>
<div className={styles.infoarea}>
<Info info={data.personalDetails} accolades={data.accolades} />
<DidYouKnow data={data.didYouKnow} />
</div>
<Credits className={styles.credits} data={data.credits} />
</Layout>
</>
);
};
type Data = ({ data: Name; error: null } | { error: AppError; data: null }) & {
originalPath: string;
};
type Params = { nameId: string };
export const getServerSideProps: GetServerSideProps<Data, Params> = async ctx => {
const nameId = ctx.params!.nameId;
const originalPath = ctx.resolvedUrl;
try {
const data = await getOrSetApiCache(nameKey(nameId), name, nameId);
return { props: { data, error: null, originalPath } };
} catch (error: any) {
const { message, statusCode } = error;
ctx.res.statusCode = statusCode;
ctx.res.statusMessage = message;
return { props: { error: { message, statusCode }, data: null, originalPath } };
}
};
export default NameInfo;

View file

@ -1,7 +1,7 @@
import Meta from 'src/components/meta/Meta';
import Layout from 'src/components/layout';
import packageInfo from 'src/../package.json';
import styles from 'src/styles/modules/pages/privacy/privacy.module.scss';
import Meta from '../../components/Meta/Meta';
import Layout from '../../layouts/Layout';
import styles from '../../styles/modules/pages/privacy/privacy.module.scss';
const Privacy = () => {
return (
@ -16,15 +16,15 @@ const Privacy = () => {
Privacy Policy
</h1>
<div className={styles.list}>
<section className={styles.item}>
<div className={styles.item}>
<h2
className={`heading heading__secondary ${styles.item__heading}`}
>
Information collected
</h2>
<p className={styles.item__text}>No information is collected.</p>
</section>
<section className={styles.item}>
</div>
<div className={styles.item}>
<h2
className={`heading heading__secondary ${styles.item__heading}`}
>
@ -40,40 +40,25 @@ const Privacy = () => {
prefrences, either turn off JavaScript or disable access to
Local Storage for libremdb.
</p>
</section>
<section className={styles.item}>
</div>
<div className={styles.item}>
<h2
className={`heading heading__secondary ${styles.item__heading}`}
>
Instance information
Information collected by other services
</h2>
{process.env.NEXT_PUBLIC_INSTANCE_NAME &&
process.env.NEXT_PUBLIC_INSTANCE_MAIN_URL && (
<p className={styles.item__text}>
Operated by:&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>
libremdb connects to 'media-amazon.com' and 'media-imdb.com' for
fetching images and videos. So, Amazon might log your IP
address, and other information(such as http headers) sent by
your browser.
</p>
</section>
</div>
</div>
<footer className={styles.metadata}>
<p>
Privacy policy last updated on <time>31 october, 2022.</time>
Last updated on <time>10 september, 2022.</time>
</p>
<p>
You can see the full revision history of this privacy policy on

View file

@ -1,22 +1,34 @@
import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
import Meta from 'src/components/meta/Meta';
import Layout from 'src/components/layout';
import ErrorInfo from 'src/components/error/ErrorInfo';
import Media from 'src/components/media/Media';
import { Basic, Cast, DidYouKnow, Info, MoreLikeThis, Reviews } from 'src/components/title';
import Title from 'src/interfaces/shared/title';
import { AppError } from 'src/interfaces/shared/error';
import getOrSetApiCache from 'src/utils/getOrSetApiCache';
import title from 'src/utils/fetchers/title';
import { getProxiedIMDbImgUrl } from 'src/utils/helpers';
import { titleKey } from 'src/utils/constants/keys';
import styles from 'src/styles/modules/pages/title/title.module.scss';
// external
import { GetServerSideProps, GetStaticProps, GetStaticPaths } from 'next'
import { useRouter } from 'next/router'
// local
import Meta from '../../../components/Meta/Meta'
import Layout from '../../../layouts/Layout'
import title from '../../../utils/fetchers/title'
// components
import ErrorInfo from '../../../components/Error/ErrorInfo'
import Basic from '../../../components/title/Basic'
import Media from '../../../components/title/Media'
import Cast from '../../../components/title/Cast'
import DidYouKnow from '../../../components/title/DidYouKnow'
import Info from '../../../components/title/Info'
import Reviews from '../../../components/title/Reviews'
import MoreLikeThis from '../../../components/title/MoreLikeThis'
// misc
import Title from '../../../interfaces/shared/title'
import { AppError } from '../../../interfaces/shared/error'
// styles
import styles from '../../../styles/modules/pages/title/title.module.scss'
import Head from 'next/head'
type Props = InferGetServerSidePropsType<typeof getServerSideProps>;
type Props = { data: Title; error: null } | { error: AppError; data: null }
// TO-DO: make a wrapper page component to display errors, if present in props
const TitleInfo = ({ data, error, originalPath }: Props) => {
if (error) return <ErrorInfo {...error} originalPath={originalPath} />;
const TitleInfo = ({ data, error }: Props) => {
const router = useRouter()
if (error)
return <ErrorInfo message={error.message} statusCode={error.statusCode} />
const info = {
meta: data.meta,
@ -25,54 +37,55 @@ const TitleInfo = ({ data, error, originalPath }: Props) => {
boxOffice: data.boxOffice,
technicalSpecs: data.technicalSpecs,
accolades: data.accolades,
};
}
return (
<>
<Meta
title={`${data.basic.title} (${data.basic.releaseYear?.start || data.basic.type.name})`}
description={data.basic.plot ?? undefined}
imgUrl={data.basic.poster?.url && getProxiedIMDbImgUrl(data.basic.poster.url)}
title={`${data.basic.title} (${
data.basic.releaseYear?.start || data.basic.type.name
})`}
description={data.basic.plot || undefined}
/>
<Layout className={styles.title} originalPath={originalPath}>
<Head>
<meta
title="og:image"
content={data.basic.poster?.url || '/icon-512.png'}
/>
</Head>
<Layout className={styles.title}>
<Basic data={data.basic} className={styles.basic} />
<Media className={styles.media} media={data.media} />
<Media className={styles.media} media={data.media} router={router} />
<Cast className={styles.cast} cast={data.cast} />
<div className={styles.textarea}>
<DidYouKnow data={data.didYouKnow} />
<Reviews reviews={data.reviews} />
<Reviews reviews={data.reviews} router={router} />
</div>
<Info className={styles.infoarea} info={info} />
<Info className={styles.infoarea} info={info} router={router} />
<MoreLikeThis className={styles.related} data={data.moreLikeThis} />
</Layout>
</>
);
};
)
}
// TO-DO: make a getServerSideProps wrapper for handling errors
type Data = ({ data: Title; error: null } | { error: AppError; data: null }) & {
originalPath: string;
};
type Params = { titleId: string };
export const getServerSideProps: GetServerSideProps<Data, Params> = async ctx => {
const titleId = ctx.params!.titleId;
const originalPath = ctx.resolvedUrl;
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const titleId = ctx.params!.titleId as string
try {
const data = await getOrSetApiCache(titleKey(titleId), title, titleId);
const data = await title(titleId)
return { props: { data, error: null, originalPath } };
return { props: { data, error: null } }
} catch (error: any) {
const { message, statusCode } = error;
ctx.res.statusCode = statusCode;
ctx.res.statusMessage = message;
const { message, statusCode } = error
ctx.res.statusCode = statusCode
ctx.res.statusMessage = message
return { props: { error: { message, statusCode }, data: null, originalPath } };
return { props: { error: { message, statusCode }, data: null } }
}
};
}
export default TitleInfo;
export default TitleInfo
// could've used getStaticProps instead of getServerSideProps, but meh.
/*

View file

@ -78,21 +78,3 @@
background-size: 100% $height;
}
}
////////////////////////////////////////////////////////////////
// CHECK IF BROWSER IS SAFARI(it's the new IE)
////////////////////////////////////////////////////////////////
@mixin fix-for-safari {
@supports (-webkit-appearance: none) and (stroke-color: transparent) {
@content;
}
}
////////////////////////////////////////////////////////////////
//
////////////////////////////////////////////////////////////////
@mixin focus-rules {
outline: 3px solid var(--clr-highlight);
outline-offset: 0.2em;
}

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,19 @@ $_light: (
// 4.2 for borders, primarily
fill-muted: hsl(0, 0%, 80%),
// shadows on cards
shadow: 0 0 0.5em hsla(0, 0%, 0%, 0.2),
shadow: 0 0 1rem hsla(0, 0%, 0%, 0.2),
// keyboard, focus hightlight
highlight: hsl(176, 43%, 46%),
// for gradient behind hero text on about page.
gradient:
(
radial-gradient(at 23% 32%, hsla(344, 79%, 40%, 0.15) 0px, transparent 70%),
radial-gradient(
at 23% 32%,
hsla(344, 79%, 40%, 0.15) 0px,
transparent 70%
),
radial-gradient(at 72% 55%, hsla(344, 79%, 40%, 0.2) 0px, transparent 50%)
),
// changes color of native html elemnts, either 'light' or 'dark' must be set.
scheme: light
)
);
$_dark: (
@ -52,7 +54,6 @@ $_dark: (
radial-gradient(at 23% 32%, hsla(344, 79%, 40%, 0.04) 0px, transparent 70%),
radial-gradient(at 72% 55%, hsla(344, 79%, 40%, 0.05) 0px, transparent 50%),
),
scheme: dark,
);
$themes: (

View file

@ -9,17 +9,11 @@ body {
#__next {
display: grid;
grid-template-rows: min-content 1fr min-content;
&:has(span[role='progressbar']) {
cursor: progress;
}
}
body {
color: var(--clr-text);
background-color: var(--clr-bg);
color-scheme: var(--clr-scheme);
accent-color: var(--clr-fill);
}
// restricting to 1600px width
@ -28,20 +22,3 @@ body {
width: min(100%, var(--max-width));
margin-inline: auto;
}
////////////////////////////////////////////////////////
// KEYBOARD NAVIGATION
////////////////////////////////////////////////////////
:focus {
@include helper.focus-rules;
}
@supports selector(:focus-visible) {
:focus {
outline: none;
}
:focus-visible {
@include helper.focus-rules;
}
}

View file

@ -1,5 +1,5 @@
@forward './reset';
@forward './helpers';
// @forward './helpers';
@forward './root';
@forward './base';
@forward './fonts';

View file

@ -24,9 +24,4 @@
@include helper.bp('bp-700') {
@include helper.typescale('mobile');
}
// not using any external fonts on webkit because of many issues
@include helper.fix-for-safari {
--ff-accent: var(--ff-base);
}
}

View file

@ -7,7 +7,6 @@ body {
color: var(--clr-text-accent);
font-family: var(--ff-accent);
font-weight: var(--fw-medium);
line-height: 1.2;
&__primary {
font-size: var(--fs-1);

View file

@ -1,97 +0,0 @@
@use '../../../abstracts' as helper;
.container {
margin-inline: auto;
display: grid;
grid-template-columns: minmax(25rem, 30rem) 1fr;
@include helper.bp('bp-900') {
grid-template-columns: none;
grid-template-rows: 30rem min-content;
}
@include helper.bp('bp-700') {
grid-template-rows: 25rem min-content;
}
}
.imageContainer {
display: flex; // for bringing out image__NA out of blur
position: relative;
height: auto;
width: auto;
overflow: hidden;
background-size: cover;
background-position: top;
place-items: center;
@include helper.bp('bp-900') {
padding: var(--spacer-2);
isolation: isolate;
// for adding layer of color on top of background image
&::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
to top,
var(--clr-bg-accent) 10%,
transparent
);
backdrop-filter: blur(8px);
}
}
}
.image {
object-fit: cover;
object-position: center;
@include helper.bp('bp-900') {
z-index: 1;
object-fit: contain;
outline: 3px solid var(--clr-fill);
outline-offset: 5px;
max-height: 100%;
margin: auto;
// overrriding nex/future/image defaults
height: initial !important;
width: initial !important;
position: relative !important;
}
}
.imageNA {
z-index: 1;
fill: var(--clr-fill-muted);
}
.info {
padding: var(--spacer-2) var(--spacer-4);
display: flex;
flex-direction: column;
gap: var(--spacer-2);
@include helper.bp('bp-900') {
// text-align: center;
// align-items: center;
}
@include helper.bp('bp-450') {
gap: var(--spacer-1);
}
}
.title {
line-height: 1;
@include helper.bp('bp-900') {
text-align: center;
}
}

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