Compare commits

...

116 commits
v0.1.0 ... main

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

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

    feat(api): add a catch-all route

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

    fix(api): refactor all endpoints a bit

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix caching error

---------

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

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

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

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

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

fix: https://github.com/zyachel/libremdb/issues/28
2022-12-31 22:40:29 +05:30
Conventional Changelog Action
5d45990798 chore(release): v2.3.0 [skip ci] 2022-12-31 17:01:44 +00:00
zyachel
0cff34a766 feat(search): add basic search functionality
this commit adds basic search feature.

fix: https://codeberg.org/zyachel/libremdb/issues/9, https://github.com/zyachel/libremdb/issues/10
2022-12-31 22:21:36 +05:30
zyachel
81eaf2fd5e fix: couple css improvements for webkit-based browsers
introduces a new mixin to handle webkit-based shenanigans.
also adds a mixin to make keyboard navigation easier.
and other small tweaks to css(like accent color and color scheme).
2022-12-31 22:10:31 +05:30
zyachel
57b050f196 refactor: refactor code a bit
make a hook to abstarct logic from page
changes to .prettierrc
changes to a couple of imports/exports
bunch of screen reader tweaks
2022-12-31 22:02:48 +05:30
zyachel
64f3896258 style: remove whitespace from .gitignore
was causing typescript decralation file to be indexed by git
2022-12-24 23:11:27 +05:30
zyachel
b4bcdb7152 docs: update instances list
add a new instance

https://github.com/zyachel/libremdb/issues/27
2022-12-24 23:03:51 +05:30
Conventional Changelog Action
80b0ca6bf0 chore(release): v2.2.2 [skip ci] 2022-12-10 14:45:51 +00:00
zyachel
78b14ec079 fix: app crash on qutebrowser
add a prop in numberformat options that was resulting in out of range error.
also swapped
'replaceAll' with 'replace'

https://github.com/zyachel/libremdb/issues/24
2022-12-10 20:15:28 +05:30
zyachel
c2df20e6ad docs(.env): update .env
some variables aren't optional anymore

fix https://github.com/zyachel/libremdb/issues/22
2022-12-10 20:15:28 +05:30
Conventional Changelog Action
2afb5b1da6 chore(release): v2.2.1 [skip ci] 2022-12-01 17:04:11 +00:00
zyachel
dd75df01eb fix(title): fix site crash
production status of titles is sometimes null. this commit accounts for that and prevents site from crashing.

https://github.com/zyachel/libremdb/issues/22
2022-12-01 22:30:07 +05:30
TinyWiFi
28d8331ae9
docs: update Region for vern.cc Public Instances
done on behalf of ~vern admins (https://vern.cc/admins)
2022-11-27 12:27:45 +00:00
Conventional Changelog Action
7501b69078 chore(release): v2.2.0 [skip ci] 2022-11-13 12:51:29 +00:00
zyachel
5dc6f60cae docs: update README 2022-11-13 18:19:12 +05:30
zyachel
1658769a30 feat: force a certain language when getting data
when fetching data, sometimes it's returned in a language depending on server IP address.
 this
commit fixes that behaviour

https://github.com/zyachel/libremdb/issues/20
2022-11-13 17:39:29 +05:30
zyachel
5fd0d92187 fix(redis): fix logs being polluted when redis is disabled
also used streaming when redis is disabled for faster response.
2022-11-13 17:29:47 +05:30
Conventional Changelog Action
6f664d2164 chore(release): v2.1.0 [skip ci] 2022-11-13 06:41:18 +00:00
zyachel
31218adac1
Merge pull request #19 from httpjamesm/main
Anonymous media proxy, local docker, private IP ratelimit and caching with redis
2022-11-13 12:11:01 +05:30
httpjamesm
0b1081c485 chore: update env local example 2022-11-12 10:58:25 -05:00
httpjamesm
7a717aa212 feat: make redis cache optional 2022-11-12 10:57:56 -05:00
httpjamesm
a410bc4264 refactor: use zyachel's code 2022-11-12 10:56:51 -05:00
httpjamesm
fda79adc52 docs: remove unnecessary big header 2022-10-31 18:26:15 -04:00
httpjamesm
0213de9403 docs: add newline 2022-10-31 18:25:04 -04:00
httpjamesm
6ae71d7907 fix: remove "information collected by other services" in privacy 2022-10-31 18:23:05 -04:00
httpjamesm
eac51d2465 docs: correct two FAQ questions 2022-10-31 18:21:49 -04:00
httpjamesm
672ee27dcc Merge remote-tracking branch 'upstream/main' 2022-10-31 18:18:01 -04:00
httpjamesm
cc7968074b docs: update readme with redis and new local docker instructions, privacy updates and more 2022-10-31 18:14:52 -04:00
httpjamesm
cdd73c6123 chore: update env local example with redis_url 2022-10-31 18:14:28 -04:00
httpjamesm
44d3a33fb3 feat: update information in FAQ 2022-10-31 18:10:34 -04:00
httpjamesm
720f2b6acb feat: IP ratelimit for media proxy 2022-10-31 18:05:19 -04:00
httpjamesm
9bce8a2dd5 fix: bypass response limit for media proxy endpoint 2022-10-31 17:46:17 -04:00
httpjamesm
1983f6b1fb feat: proxy videos and add more descriptive error messages 2022-10-31 17:45:14 -04:00
httpjamesm
dba2ba5aa4 feat: fetch images from media proxy on frontend 2022-10-31 17:37:36 -04:00
httpjamesm
b7ee6863e5 feat: docker support for easy deployment 2022-10-31 17:28:27 -04:00
httpjamesm
2c8d138cbd feat: cache media proxy data in redis for 30 mins 2022-10-31 17:28:18 -04:00
httpjamesm
59a314b2bd feat: media proxy for anonymous loads 2022-10-31 17:04:26 -04:00
Conventional Changelog Action
00cadf54e0 chore(release): v2.0.0 [skip ci] 2022-10-31 14:53:29 +00:00
httpjamesm
f207d688e2 fix: change to poster for og:image 2022-10-31 20:23:12 +05:30
httpjamesm
4638f913e8 chore: add prettierrc file for future contributors 2022-10-31 20:23:12 +05:30
httpjamesm
d152cf4b62 feat: add "og:image" property for social media embeds 2022-10-31 20:23:12 +05:30
httpjamesm
3f987b59dc fix: remove double space in inspiration credit 2022-10-31 20:23:12 +05:30
httpjamesm
261a37576b fix: change to poster for og:image 2022-10-30 19:21:19 -04:00
httpjamesm
0c76f485f9 chore: add prettierrc file for future contributors 2022-10-30 19:18:20 -04:00
httpjamesm
a2fc2322a3 feat: add "og:image" property for social media embeds 2022-10-30 19:18:12 -04:00
httpjamesm
478b45977d fix: remove double space in inspiration credit 2022-10-30 19:14:17 -04:00
zyachel
256ce38d35 docs: update readme
make readme more readable
2022-09-24 20:36:24 +05:30
Conventional Changelog Action
d2a752015f chore(release): v1.0.0 [skip ci] 2022-09-24 09:31:09 -04:00
zyachel
9891204f5a feat: major rewrite
the application is now rewritten in next.js. this commit also adds the ability to see trailers, did you know, more like this, etc. on title page.

BREAKING CHANGE: the whole application is rewritten from scratch.
2022-09-24 09:31:09 -04:00
Esmail EL BoB
620ddf348a
docs: update instances section
add a new instance
2022-08-05 16:43:35 +00:00
Esmail EL BoB
d9f7cd1244
docs(instances): update instances list
added https://libremdb.esmailelbob.xyz/
2022-07-25 08:26:40 +05:30
zyachel
f68a0da431 docs(instances): update instances list
add a new insatnce

fix https://github.com/zyachel/libremdb/issues/8
2022-07-09 17:27:49 +05:30
zyachel
c751d8814d docs: add a new section 2022-06-20 16:14:09 +05:30
zyachel
6f43b68949 docs: update instances list
add a new instance

fix #5
2022-06-20 15:29:44 +05:30
Dyras
e272d8101d
docs: fix typo
fix spelling of Debian.

#4
2022-06-18 15:27:29 +05:30
Conventional Changelog Action
5358d58e31 chore(release): v0.1.2 [skip ci] 2022-06-06 19:18:41 +00:00
zyachel
b2b50df02b build(dependecies): update dependencies 2022-06-05 22:44:33 +05:30
zyachel
170fbabe5e perf: implement caching of static files 2022-06-05 21:22:38 +05:30
zyachel
55c0eba6e4 fix: change the order in which env vars are loaded 2022-06-05 21:13:47 +05:30
zyachel
f39998d57b fix(robots.txt): disallow all robots
crawl way too much
2022-06-05 19:19:29 +05:30
zyachel
7757d5b183 docs: add docker instructions
thanks to @TheFrenchGhosty, there's a docker image now

#2
2022-06-04 04:27:15 +05:30
zyachel
ea3bb0e384 docs: update privacy section
having heroku's potential logging advisory across instances' webpages(where instances might not be even using heroku) isn't ideal.
So, rephrased the privacy/faq sections in README as well as on the webpage.
2022-06-04 04:25:09 +05:30
zyachel
ebd4e83580 docs: add instances section
add instances section to include PussTheCat.org's libremdb instance

#3
2022-06-04 03:24:21 +05:30
Conventional Changelog Action
0cebb42dc5 chore(release): v0.1.1 [skip ci] 2022-06-03 18:58:43 +00:00
TheFrenchGhosty
9f35a668b5
fix: typo in URL (#2)
* Fix the URL of the Invidious website

* Also fix it in the similar projects page
2022-06-04 00:28:28 +05:30
213 changed files with 11684 additions and 4856 deletions

43
.env.local.example Normal file
View file

@ -0,0 +1,43 @@
################################################################################
### 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.
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=

7
.eslintrc.json Normal file
View file

@ -0,0 +1,7 @@
{
"extends": "next/core-web-vitals",
"rules": {
"react/no-unescaped-entities": "off",
"@next/next/no-page-custom-font": "off"
}
}

View file

@ -1,28 +0,0 @@
name: release
on:
push:
branches:
- main
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 }}

57
.gitignore vendored
View file

@ -1,21 +1,42 @@
#big stuff
node_modules/
.cache/
.parcel-cache/
# dependencies
/node_modules
/.pnp
.pnp.js
# env variables
*.env
# testing
/coverage
# CSS files
public/css/*.css
public/css/*.css.map
# next.js
/.next/
/out/
#misc stuff
fetchers/*
!fetchers/movie.js
controllers/movieControllers.js
routes/movieRoutes.js
views/pug/homepage.pug
views/pug/movies.pug
views/pug/_card.pug
utils/constants.js
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# typescript
*.tsbuildinfo
next-env.d.ts
#just dev stuff
dev/*
# other lockfiles
yarn.lock
package-lock.json
# docker
docker-compose.yml
dump.rdb

9
.prettierrc Normal file
View file

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

36
.versionrc Normal file
View file

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

View file

@ -1,9 +1,105 @@
# 0.1.0 (2022-05-21)
# Changelog
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
## [3.2.0](https://github.com/zyachel/libremdb/compare/v3.1.1...v3.2.0) (2023-10-28)
### Features
* add review section ([30dac07](https://github.com/zyachel/libremdb/commit/30dac07ba33dbe4331a5c9fa6cd2c332100868df))
* **list:** add list route ([97f1432](https://github.com/zyachel/libremdb/commit/97f1432ac5d23206229d806b7cb3e04af6dec36f))
## [3.1.1](https://github.com/zyachel/libremdb/compare/v3.1.0...v3.1.1) (2023-10-14)
### Bug Fixes
* **card:** fix long attributes in cards under 'Known For' section ([736d680](https://github.com/zyachel/libremdb/commit/736d6802430a3f4f364915f3df93fc548a51ebf1))
* **error:** fix incorrect 'view on IMDb' link on error page ([0aea2f4](https://github.com/zyachel/libremdb/commit/0aea2f47dad6eb78e319ea1abd8c444f2cba4424))
* **media proxy:** fix 304 response code with body error ([c610ef4](https://github.com/zyachel/libremdb/commit/c610ef4d1be39c122715a0eb200155537e7d6abf))
* **name:** fix name route crash ([38ed0c6](https://github.com/zyachel/libremdb/commit/38ed0c62177532b93f61af4172ffa6e5b9995bdc))
* **name:** fix route crash for some ids ([e91c313](https://github.com/zyachel/libremdb/commit/e91c313f127632f1bd44d190af71bc841bbe87b7))
* **title:** fix a crash in title route ([21a1c83](https://github.com/zyachel/libremdb/commit/21a1c83d95b703fa08cdb96c206626f22d5366c9))
## [3.1.0](https://github.com/zyachel/libremdb/compare/v3.0.0...v3.1.0) (2023-05-21)
### Features
* **cache:** implement caching of routes ([c53c88d](https://github.com/zyachel/libremdb/commit/c53c88db9bf98258547e2ca512f864800821cb1f))
### Bug Fixes
* **form:** fix hydration error ([8599ae2](https://github.com/zyachel/libremdb/commit/8599ae2c5ac11f2818f56c9f7de7666a38b4386c))
* **name:** fix a couple of crashes in name and title route ([8d9b663](https://github.com/zyachel/libremdb/commit/8d9b6630a576b7e8331eb5431cd90d02733b4917))
## [3.0.0](https://github.com/zyachel/libremdb/compare/v2.4.0...v3.0.0) (2023-04-15)
### ⚠ BREAKING CHANGES
* **title:** older versions won't work, at least for title route
### Features
* add info related to the current instance ([2c5d2f8](https://github.com/zyachel/libremdb/commit/2c5d2f86e46a52223f07d573b152bad5174ee2d9))
* **route:** add name route ([75732e0](https://github.com/zyachel/libremdb/commit/75732e00869f9777e87e767a48648996345f02f7))
### Bug Fixes
* **title:** fix title page crash ([8ce02d0](https://github.com/zyachel/libremdb/commit/8ce02d02364c8e1f03a8b16594bc20ee6766a8c6))
# [2.4.0](https://github.com/zyachel/libremdb/compare/v2.3.1...v2.4.0) (2023-01-22)
### Bug Fixes
* fix app crash ([71d1d5b](https://github.com/zyachel/libremdb/commit/71d1d5b34e2866729ae0c96c59ea51e8d1a3dcca))
### Features
* add error boundary ([5cc2ef0](https://github.com/zyachel/libremdb/commit/5cc2ef02cec0b31c5d449e189a054fbef5801f60))
## [2.3.1](https://github.com/zyachel/libremdb/compare/v2.3.0...v2.3.1) (2023-01-15)
### Bug Fixes
* fix unseekable videos on webkit-based browsers ([a32785c](https://github.com/zyachel/libremdb/commit/a32785ce00b638e9079f0924fd9b00f98c077348))
# [2.3.0](https://github.com/zyachel/libremdb/compare/v2.2.2...v2.3.0) (2022-12-31)
### Bug Fixes
* couple css improvements for webkit-based browsers ([81eaf2f](https://github.com/zyachel/libremdb/commit/81eaf2fd5e5980c0c4d59a8805cf541fa8fe51f9))
### Features
* **search:** add basic search functionality ([0cff34a](https://github.com/zyachel/libremdb/commit/0cff34a766b09ba17be2a89f6290889dbf225436))
## [2.2.2](https://github.com/zyachel/libremdb/compare/v2.2.1...v2.2.2) (2022-12-10)
### Bug Fixes
* app crash on qutebrowser ([78b14ec](https://github.com/zyachel/libremdb/commit/78b14ec07955d29403b8b5ae0d449f38eea2bbc5))
## [2.2.1](https://github.com/zyachel/libremdb/compare/v2.2.0...v2.2.1) (2022-12-01)
### Bug Fixes
* **title:** fix site crash ([dd75df0](https://github.com/zyachel/libremdb/commit/dd75df01eb7c03d8945a8bd20ed231a66bd88b8f))

35
Dockerfile Normal file
View file

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

178
README.md
View file

@ -1,108 +1,186 @@
# libremdb
A FOSS alternative front-end to IMDb.
A free & open source IMDb front-end.
Inspired by projects like [teddit](https://codeberg.org/teddit/teddit), [nitter](https://github.com/zedeus/nitter) and [many others](#similar-projects).
<br/>
<img src="./public/img/misc/preview.png" title="image showing matrix movie info on libremdb" width="1000" />
| | |
| -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| <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" /> |
---
## Features
## Some Features
- No ads or tracking
Browse any movie info without being tracked or bombarded by annoying ads.
- No connection to IMDb
All requests go through the backend; client never talks to IMDb except for a bunch of requests to `m.media-amazon.com` for images.
- No JavaScript required
Just a few linesof code(~30) to save theme preference, which itself is optional.
- Modern interface
Modern interface with curated colors supporting both dark and light themes.
- Responsive design
Be it your small mobile or big computer screen, it's fully responsive.
- Lightweight
_[Up movie page](https://imdb.com/title/tt1049413/)_
| | libremdb | IMDb |
| --------------- | -------- | ------ |
| no. of requests | ~35 | ~280 |
| data consumed | 1.35MB | 7.75MB |
(tested on Firefox v104; without scroll; simulated regular 4g)
| Network tab stats | libremdb | IMDb |
| ------------------------ | -------- | ------ |
| no. of requests | 22 | 180 |
| data transfered(gzipped) | 468KB | 1.88MB |
| load event fired in | 6.22s | 10.01s |
---
## FAQs
## Instances
<!-- 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/)
| 2. Onion | | |
| [ld.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion](http://ld.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion) | US | Operated by [~vern](https://vern.cc) |
| 3. I2P | | |
| [vernz3ubrntql4wrgyrssd6u3qzi36zrhz2agbo6vibzbs5olk2q.b32.i2p](http://vernz3ubrntql4wrgyrssd6u3qzi36zrhz2agbo6vibzbs5olk2q.b32.i2p) | US | Operated by [~vern](https://vern.cc) |
---
## Questions you might have
- How do I use it?
Replace `imdb.com` in any IMDb URL with any of the instances. For example: '[imdb.com/title/tt1049413](https://imdb.com/title/tt1049413/)' to '[libremdb.iket.me/title/tt1049413](https://libremdb.iket.me/title/tt1049413/)'.
To avoid changing the URLs manually, you can use [extensions](#automatic-redirection).
- Why is it so slow?
Whenever you request info about a movie/show on libremdb, 4 trips are made(2 between your browser and libremdb's server, and 2 between libremdb's server and IMDb's server) instead of the usual 2 trips when you visit a website. For this reason there's a noticable delay. This is a bit of inconvenience you'll have to face should you wish to use this website.
- It doesn't have all routes.
I think most of the people just check IMDb to get a quick glimpse of a movie/show. That's why there is just one route for now. However, I will try to implement other important routes if time allows. Keep an eye on [To-Do](#to-do) section.
I'll implement more with time :)
- Why is it connecting to `m.media-amazon.com`?
For now, images 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. I'd recommend using a VPN for mitigating this risk.
- Is content served from third-parties, like Amazon?
Nope, libremdb proxies all image and video requests through the instance to avoid exposing your IP address, browser information and other personally identifiable metadata ([Contributor](https://github.com/httpjamesm)).
- Why not just use IMDb?
Refer to the [features section](#features) above.
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.
- Why did you deploy it on heroku? Why not just buy your own domain name?
It's just a proof-of-concept for now. However, if you'd like to do so, you are very welcome.
---
## Privacy
In short: libremdb doesn't collect any data at all.
- Information collected:
None.
- Data you directly provide: None.
- Data you passively provide: Heroku might log some things(like IP address). So, consider hosting your own instance or using a VPN.
- Data stored in your browser: To remember theme preferences, the website stores a key named 'theme' in Local Storage provided by your browser. Apart from that, there is nothing stored in your browser.
- Information stored in your browser:
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)).
---
## To-Do
### soon
- [ ] add advanced search route
- [ ] add did you know and reviews on movie info page
- [ ] implement routes for reviews, quotes, goofs, trivia and crazy credits
- [x] add did you know and reviews on movie info page
- [x] add a way to see trailer and other videos
- [ ] implement movie specific routes like:
### at a later stage
- [ ] reviews(including critic reviews)
- [ ] video & image gallery
- [ ] sections under 'did you know'
- [ ] release info
- [ ] parental guide
- [ ] use redis
- [ ] implement a better installation method
- [ ] serve images from libremdb itself
- [ ] add a way to see trailer and other videos
- [ ] implement other trivial routes
- [ ] implement other routes like:
- [ ] lists
- [ ] moviemeter
- [x] person info(includes directors and actors)
- [ ] company info
- [ ] user info
- [X] use redis, or any other caching strategy
- [x] implement a better installation method
- [x] serve images and videos from libremdb itself
---
## Installation
As libremdb is made with Next.js, you can deploy it anywhere where Next.js is supported. Below are a few other methods:
### Manual
1. Install Node.js and Git.
for Node.js, visit [their website](https://nodejs.org/en/).
for Git, run `sudo apt install git` if you're on a Debain-based distro. Else visit [their website](https://git-scm.com/).
for Git, run `sudo apt install git` if you're on a Debian-based distro. Else visit [their website](https://git-scm.com/).
2. Clone and set up the repo.
2. Install redis(optional).
You can install redis from [here](https://redis.io).
3. Clone and set up the repo.
```bash
git clone https://github.com/zyachel/libremdb.git
git clone https://github.com/zyachel/libremdb.git # replace github.com with codeberg.org if you wish so.
cd libremdb
cp config.env.template config.env # you can make necessary changes
# if you use npm, change 'pnpm' to 'npm run' here as well as in package.json
# change the configuration file to your liking.
cp .env.local.example .env.local
# 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.
libremdb will start running at http://localhost:3000.
To change port, modify the last command like this: `pnpm start -- -p <port-number>`.
---
### Docker (Local)
## similar projects
You can build the docker image using the provided Dockerfile(thanks to [@httpjamesm](https://github.com/httpjamesm)) and set it up using the [example docker-compose file](./docker-compose.example.yml).
Change the docker-compose file to your liking and run `docker-compose up -d` to start the container, that's all!
### Docker (Built)
There's a [docker image](https://github.com/PussTheCat-org/docker-libremdb-quay) made by [@TheFrenchGhosty](https://github.com/TheFrenchGhosty) for [PussTheCat.org's instance](https://libremdb.pussthecat.org). You can use that as well.
## Miscellaneous
### Automatic redirection
- [Redirector](https://github.com/einaregilsson/Redirector)
config:
```
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\/(.*)
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)
Teddit is an alternative Reddit front-end focused on privacy.
@ -110,27 +188,23 @@ libremdb will start running at http://localhost:3000.
Nitter is a free and open source alternative Twitter front-end focused on privacy.
- [Bibliogram](https://sr.ht/~cadence/bibliogram/)
Bibliogram is an alternative front-end for Instagram.
- [Invidious](https://invidious.org)
- [Invidious](https://invidious.io)
Invidious is an alternative front-end to YouTube.
- [Libreddit](https://github.com/spikecodes/libreddit)
Libreddit is an alternative private front-end to Reddit.
- [Scribe](https://git.sr.ht/~edwardloveall/scribe)
Scribe is an alternative Medium frontend.
- [other cool projects &rarr;](https://github.com/mendel5/alternative-front-ends)
- [full list &rarr;](https://github.com/digitalblossom/alternative-frontends)
---
## Contact
| \[matrix\] | email |
| :--------------------------------------------------------: | :------------------------------------------------------: |
| <img src="./public/img/contact/matrix.png" width="120" /> | <img src="./public/img/contact/email.png" width="120" /> |
| [@ninal:matrix.org](https://matrix.to/#/@ninal:matrix.org) | [aricla@protonmail.com](mailto:aricla@protonmail.com) |
I'm availabe on [[matrix]](https://matrix.to/#/@ninal:matrix.org) and [email](mailto:aricla@protonmail.com) in case you wish to contact me personally.
---
## License
Licensed under GNU AGPLv3.
Refer to [License](/LICENSE) for full legalese.
See [License](./LICENSE) for full legalese.

58
app.js
View file

@ -1,58 +0,0 @@
const express = require('express');
const path = require('path');
const morgan = require('morgan');
const helmet = require('helmet');
const compression = require('compression');
const app = express();
// const movieRouter = require('./routes/movieRoutes');
const viewRouter = require('./routes/viewRoutes');
const globalErrorHandler = require('./controllers/errorControllers');
const { AppError } = require('./utils/errorUtils');
//-------------------------------------------------------------------------//
// GLOBAL MIDDLEWARES
//-------------------------------------------------------------------------//
app.use(compression()); // for compressing response bodies
app.use(
// for making the app more secure by setting some security headers(like CORS)
helmet({
contentSecurityPolicy: {
directives: {
// only allowing images from 'm.media-amazon.com' as crossorigin
'img-src': ["'self'", 'm.media-amazon.com'],
},
},
// 'crossorigin' attribute is needed in the img tag where images are fetched from 'm.media-amazon.com' if the policy below is set to true. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy for more details
crossOriginEmbedderPolicy: false,
})
);
app.set('view engine', 'pug'); // setting pug as a view engine
app.set('views', path.join(__dirname, 'views/pug')); // directory from where html template will be sourced
app.use(express.static(path.join(__dirname, 'public'))); // directory from where files like css, images, fonts, will be sourced
if (process.env.NODE_ENV === 'development') app.use(morgan('dev')); // for logging requests
// app.use(express.json({ limit: '3mb' })); // for parsing json
//-------------------------------------------------------------------------//
// ROUTER MIDDLEWARES
//-------------------------------------------------------------------------//
// app.use('/api/v1/movies', movieRouter); // sub-router for movie related endpoints
app.use('/', viewRouter); // for html pages
//-------------------------------------------------------------------------//
// GLOBAL ERROR HANDLING
//-------------------------------------------------------------------------//
app.all('*', (req, res, next) => {
next(
new AppError(
`the route you requested(${req.originalUrl}) doesn't exist`,
404
)
);
});
app.use(globalErrorHandler); // handling errors in every middleware
module.exports = app;

View file

@ -1,8 +0,0 @@
# default port is 3000
PORT=3000
# url
URL=http://localhost:3000
# enviorment
NODE_ENV=production
# change image quality
IMAGE_QUALITY=500

View file

@ -1,94 +0,0 @@
const { AppError } = require('../utils/errorUtils');
//----------------------------------------------------------------------------------------------------------//
// ERROR HANDLING FUNCTIONS ACC TO ENVIRONMENTS
//----------------------------------------------------------------------------------------------------------//
// for errors during development
const devErrorHandler = (err, req, res) => {
req.originalUrl.startsWith('/api/')
? // 1) FOR API
res.status(err.statusCode).json({
status: err.status,
message: err.message,
stack: err.stack,
err,
})
: // 2) FOR RENDERED PAGES
res.render('error', {
title: 'an error occured',
page: 'error',
message: err.message,
statusCode: err.statusCode,
stack: err.stack,
});
};
// for errors during production
const prodErrorHandler = (err, req, res) => {
if (req.originalUrl.startsWith('/api/'))
// 1) FOR API
// for trusted, operational errors
err.isOperational
? res
.status(err.statusCode)
.json({ status: err.status, message: err.message })
: // for untrusted errors sending a generic message
res
.status(500)
.json({ status: 'fail', message: 'something went wrong!' });
// 2) FOR RENDERED PAGES
err.isOperational
? res.render('error', {
title: 'something went wrong',
page: 'error',
message: err.message,
statusCode: err.statusCode,
})
: res.render('error', {
title: 'an error occured',
page: 'error',
message: 'something went wrong!',
statusCode: 500,
});
// err;
};
//-------------------------------------------------------------------------------------------------------------//
// MAIN EXPRESS ERROR HANDLING MIDDLEWARE
//-------------------------------------------------------------------------------------------------------------//
const globalErrorHandler = (err, req, res, next) => {
// setting some defaults on the error in case they don't exist already
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
// logging error to the console
console.log('\x1b[31m%s\x1b[0m', '🔴️ ERROR:', err); // with red color
// sending error to the client
// for dev environment
if (process.env.NODE_ENV === 'development') devErrorHandler(err, req, res);
// for prod environment
else if (process.env.NODE_ENV === 'production') {
let error = { ...err };
// marking some special errors as operational
// axios errors
if (err.isAxiosError) {
// if connection can't be established to imdb for some reason
if (err.code === 'ENETUNREACH' || err.code === 'ENOTFOUND')
error = new AppError(
'there was some problem fetching data from IMDb',
500
);
// in case the url is wrong(like bad title id)
if (err.response)
error = new AppError(err.response.statusText, err.response.status);
}
prodErrorHandler(error, req, res);
}
};
module.exports = globalErrorHandler;

View file

@ -1,73 +0,0 @@
const { AppError, catchErrors } = require('../utils/errorUtils');
// const { genres, popularGenres } = require('../utils/constants');
const getMovie = require('../fetchers/movie');
exports.overview = (req, res, next) => res.redirect(301, '/about');
exports.about = catchErrors(async (req, res, next) => {
res.render('about', { title: 'About', page: 'About' });
});
exports.privacy = catchErrors(async (req, res, next) => {
res.render('privacy', { title: 'Privacy', page: 'Privacy' });
});
exports.contact = catchErrors(async (req, res, next) => {
res.render('contact', { title: 'Contact', page: 'Contact' });
});
exports.similarProjects = catchErrors(async (req, res, next) => {
res.render('similarProjects', {
title: 'Similar Projects',
page: 'Similar Projects',
});
});
exports.movie = catchErrors(async (req, res, next) => {
const movie = await getMovie(req.params.title, +process.env.IMAGE_QUALITY);
res.render('movie', {
movie,
title: movie.basic.name,
page: movie.basic.name,
});
});
/*
exports.searchGenres = catchErrors(async (req, res, next) => {
// fetching top 10 rated movies to show in homepage
let movies;
const genresQueriedStr = req.query.genres;
const genresQueriedArr = genresQueriedStr?.split(',');
if (!genresQueriedArr) {
movies = await moviesDataFetcher('sci-fi,action');
return res.render('homepage', {
title: 'Home Page',
page: 'Home Page',
movies: movies.slice(-10),
genres,
popularGenres,
});
}
const numResults = +req.query.results || 10;
const numPage = +req.query.page || 1;
const resultsToskip = numResults * (numPage - 1);
movies = await moviesDataFetcher(genresQueriedStr);
movies = movies.slice(resultsToskip, resultsToskip + numResults);
const totalNumResults = movies.length;
if (!movies.length) return next(new AppError('no movies found', 404));
const [curPage, totalPages] = [numPage, totalNumResults / numResults];
res.render('movies', {
title: genresQueriedArr.join(', '),
page: 'Movies',
genres: genresQueriedArr,
movies,
curPage,
totalPages,
});
});
*/

View file

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

View file

@ -1,256 +0,0 @@
const cheerio = require('cheerio');
const axiosInstance = require('../utils/axiosInstance');
// cleans images links and adds custom quality(optional).
const cleanLink = (link, quality = null) => {
if (!link) return '';
let cleanedLink = link.match(
/https:\/\/m\.media-amazon\.com\/images\/M\/[^.]*/gm
)?.[0];
if (!cleanedLink) return '';
cleanedLink += quality ? `.UX${quality}.jpeg` : '.jpeg';
return cleanedLink;
};
const getMovie = async (title, quality = null) => {
try {
const res = await axiosInstance(`/title/${title}`);
const $ = cheerio.load(res.data);
// basic info
const name = $('h1').text();
const metadata = $('ul[data-testid=hero-title-block__metadata] li')
.map((i, el) =>
$(el).children().length ? $(el).children().first().text() : $(el).text()
)
.toArray();
const rating = $('[data-testid=hero-rating-bar__aggregate-rating__score]')
.children()
.first()
.text();
const numVotes = $('[data-testid=hero-rating-bar__aggregate-rating__score]')
.siblings()
.last()
.text();
const popularity = $('[data-testid="hero-rating-bar__popularity__score"]')
.first()
.text();
const plotBrief = $('span[data-testid=plot-xl]').contents().first().text();
let poster = $('[data-testid="hero-media__poster"] img').attr('src');
poster = cleanLink(poster, quality);
const reviewScores = [];
$(
'ul[data-testid="reviewContent-all-reviews"] li a span.three-Elements'
).each((i, el) =>
reviewScores.push([
$(el).children().first().text(),
$(el).children().last().text(),
])
);
// media
const images = $('section[data-testid="Photos"] .ipc-photo img')
.map((i, el) => {
let image = $(el).attr('src');
return cleanLink(image, quality);
})
.toArray();
const imagesLinkExternal = $('[data-testid=photos-title]').attr('href');
const videoThumbnail = $('.ipc-slate__slate-image img').attr('src');
const videoLinkExternal = $('.hero-media__slate-overlay').attr('href');
const videosLinkExternal = $('[data-testid=videos-title]').attr('href');
// cast
const actors = $(
'section[data-testid="title-cast"] div[data-testid=title-cast-item]'
)
.map((i, el) => {
const name = $(el)
.find('a[data-testid="title-cast-item__actor"]')
.text();
let avatar = $(el).find('img').attr('src');
avatar = cleanLink(avatar, quality);
const characterName = $(el)
.find('a[data-testid=cast-item-characters-link] span')
.first()
.text();
return { name, avatar, characterName };
})
.toArray();
const directors = $(
'section[data-testid=title-cast] ul li:nth-of-type(1) li'
)
.map((i, el) => $(el).find('a').text())
.toArray();
const writers = $('section[data-testid=title-cast] ul li:nth-of-type(2) li')
.map((i, el) => $(el).find('a').text())
.toArray();
// storyline
const plotExpanded = $(
'section[data-testid="Storyline"] [data-testid=storyline-plot-summary]'
)
.contents()
.first()
.text();
const tagline = $(
'section[data-testid="Storyline"] [data-testid=storyline-taglines] li'
)
.first()
.text();
const genres = $(
'section[data-testid="Storyline"] [data-testid=storyline-genres] li'
)
.map((i, el) => $(el).text())
.toArray();
const parentalGuidance = $(
'[data-testid="storyline-certificate"] li'
).text();
// reviews
const reviewSummary = $(
'section[data-testid="UserReviews"] span[data-testid="review-summary"]'
).text();
const reviewRating = $(
'section[data-testid="UserReviews"] div[data-testid="review-featured-header"]'
)
.children()
.last()
.text();
const reviewComment = $(
'section[data-testid="UserReviews"] div[data-testid="review-overflow"]'
).text();
// details
const releaseDate = $(
'section[data-testid="Details"] li[data-testid="title-details-releasedate"] li'
).text();
const countries = $(
'section[data-testid="Details"] li[data-testid="title-details-origin"] li'
)
.map((i, el) => $(el).text())
.toArray();
const officialSite = $(
'section[data-testid="Details"] li[data-testid="title-details-officialsites"] li a'
)
.first()
.attr('href');
const languages = $(
'section[data-testid="Details"] li[data-testid="title-details-languages"] li'
)
.map((i, el) => $(el).text())
.toArray();
const alternateTitle = $(
'section[data-testid="Details"] li[data-testid="title-details-akas"] li'
)
.first()
.text();
const filmingLocations = $(
'section[data-testid="Details"] li[data-testid="title-details-filminglocations"] li'
)
.map((i, el) => $(el).text())
.toArray();
const companies = $(
'section[data-testid="Details"] li[data-testid="title-details-companies"] li'
)
.map((i, el) => $(el).text())
.toArray();
// technical specs
const runtime = $(
'section[data-testid="TechSpecs"] li[data-testid="title-techspec_runtime"] div'
).text();
const color = $(
'section[data-testid="TechSpecs"] li[data-testid="title-techspec_color"] div'
).text();
const aspectRatio = $(
'section[data-testid="TechSpecs"] li[data-testid="title-techspec_aspectratio"] div'
).text();
const sound = $(
'section[data-testid="TechSpecs"] li[data-testid="title-techspec_soundmix"] li'
)
.map((i, el) => $(el).text())
.toArray();
// boxoffice
const budget = $(
'section[data-testid="BoxOffice"] [data-testid="title-boxoffice-section"] li[data-testid="title-boxoffice-budget"] li'
).text();
const grossDomestic = $(
'section[data-testid="BoxOffice"] [data-testid="title-boxoffice-section"] li[data-testid="title-boxoffice-grossdomestic"] li'
).text();
const openingWeekendDomestic = $(
'section[data-testid="BoxOffice"] [data-testid="title-boxoffice-section"] li[data-testid="title-boxoffice-openingweekenddomestic"] li'
)
.map((i, el) => $(el).text())
.toArray();
const grossWorldwide = $(
'section[data-testid="BoxOffice"] [data-testid="title-boxoffice-section"] li[data-testid="title-boxoffice-cumulativeworldwidegross"] li'
).text();
// making data object
const data = {
basic: {
name,
poster,
metadata,
rating,
numVotes,
popularity,
genres,
plotBrief,
directors,
writers,
},
media: {
poster,
images,
imagesLinkExternal,
videoThumbnail,
videoLinkExternal,
videosLinkExternal,
},
topCast: { actors, directors, writers },
storyline: { plotExpanded, tagline, genres, parentalGuidance },
details: {
releaseDate,
countries,
officialSite,
languages,
alternateTitle,
filmingLocations,
companies,
},
technicalSpecs: {
runtime,
color,
aspectRatio,
sound,
},
boxoffice: {
budget,
openingWeekendDomestic,
grossDomestic,
grossWorldwide,
},
reviews: {
reviewScores,
review: {
reviewComment,
reviewSummary,
reviewRating,
},
},
};
// returning data
return data;
} catch (err) {
console.log(err.response);
throw err;
}
};
module.exports = getMovie;

26
next.config.mjs Normal file
View file

@ -0,0 +1,26 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
async redirects() {
return [
{
source: '/',
destination: '/find',
permanent: true,
},
];
},
images: {
domains: ['m.media-amazon.com'],
},
experimental: {
images: {
allowFutureImage: true,
},
isrMemoryCacheSize: 20 * 1024 * 1024,
},
poweredByHeader: false,
};
export default nextConfig;

View file

@ -1,42 +1,41 @@
{
"name": "libremdb",
"version": "0.1.0",
"description": "a FOSS alternative front-end to IMDb",
"main": "server.js",
"scripts": {
"sass:watch": "sass views/sass/main.scss public/css/styles.css --watch",
"sass:build": "sass views/sass/main.scss public/css/styles.css --style=compressed",
"prod": " (nodemon server.js) & (pnpm sass:watch)",
"dev": " (NODE_ENV=development nodemon server.js) & (pnpm sass:watch)",
"start": "(mkdir -p public/css) && (npm run sass:build) && (node server.js)"
},
"version": "3.2.0",
"description": "a free & open source IMDb front-end",
"private": true,
"type": "module",
"author": "libremdb-contributors",
"license": "AGPL-3.0-or-later",
"repository": {
"type": "git",
"url": "https://github.com/zyachel/libremdb/"
},
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"axios": "^0.26.1",
"cheerio": "^1.0.0-rc.10",
"compression": "^1.7.4",
"dotenv": "^16.0.0",
"express": "^4.17.3",
"helmet": "^5.0.2",
"morgan": "^1.10.0",
"pug": "^3.0.2",
"sass": "^1.50.0"
"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"
},
"devDependencies": {
"@types/express": "^4.17.13",
"@types/morgan": "^1.9.3",
"nodemon": "^2.0.15"
"@types/node": "18.7.3",
"@types/react": "18.0.17",
"@types/react-dom": "18.0.6",
"eslint": "8.22.0",
"eslint-config-next": "12.2.5",
"sass": "^1.62.1",
"typescript": "4.7.4"
},
"nodemonConfig": {
"ignore": [
"node_modules/*",
"public/*"
]
},
"pnpm": {
"overrides": {
"json-schema@<0.4.0": ">=0.4.0"
}
"engines": {
"node": ">=16.5.0",
"pnpm": ">=8.0.0"
}
}
}

3247
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#b91d47</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
public/icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

10
public/icon.svg Normal file
View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="85" height="85" version="1.1" viewBox="0 0 85 85" xmlns="http://www.w3.org/2000/svg">
<style>
@media (prefers-color-scheme: dark) { .logo-icon { fill: #e85e8f }}
</style>
<g transform="matrix(.489 0 0 .489 -10.9 -57.8)" class="logo-icon" fill="#b80040">
<path d="m181 245-35.7 26.3c-4.68 3.46-10.5 5.37-16.5 5.37h-91.8c-2.31 0-4-1.92-4-4.23v-25.4c0-2.31 1.92-4.24 4-4.24l14.6 6e-3 12.3-9.99c5.32-4.26 12.5-6.94 19.6-6.94h42.1c5.16 0 9.23 4.6 8.37 9.89-0.695 4.17-4.6 7.04-8.83 7.04h-20.5c-2.31 0-4 1.92-4 4s1.92 4 4 4h31.9l31.7-23.3c4.71-3.49 11.3-2.47 14.8 2.24 3.7 5.18 2.69 11.8-2.02 15.3z"/>
<path d="m106 133a44.5 43.7 0 0 1 44.5 43.7 44.5 43.7 0 0 1-44.5 43.7 44.5 43.7 0 0 1-44.5-43.7 44.5 43.7 0 0 1 44.5-43.7m0 8.74a11.1 10.9 0 0 0-11.1 10.9 11.1 10.9 0 0 0 11.1 10.9 11.1 10.9 0 0 0 11.1-10.9 11.1 10.9 0 0 0-11.1-10.9m-33.8 24.2c-1.91 5.73 1.29 11.9 7.12 13.8 5.88 1.84 12.2-1.27 14-7.04 1.91-5.73-1.29-11.9-7.12-13.8-5.83-1.84-12.2 1.27-14 7.04m67.7-0.132c-1.91-5.64-8.19-8.74-14-6.91-5.88 1.84-9.08 8-7.17 13.8 1.91 5.73 8.19 8.87 14 6.99 5.83-1.84 9.04-8.04 7.17-13.9m-54.8 39.2c4.94 3.58 12 2.49 15.5-2.4 3.61-4.9 2.49-11.7-2.49-15.3-4.94-3.54-11.9-2.45-15.5 2.4-3.61 4.9-2.49 11.7 2.49 15.3m41.8 0c4.99-3.54 6.1-10.4 2.49-15.3-3.61-4.9-10.6-5.99-15.5-2.4-4.99 3.54-6.1 10.4-2.49 15.3 3.52 4.9 10.6 5.94 15.5 2.4m-20.9-34.8a6.68 6.56 0 0 0-6.68 6.56 6.68 6.56 0 0 0 6.68 6.56 6.68 6.56 0 0 0 6.68-6.56 6.68 6.56 0 0 0-6.68-6.56z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 571 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

View file

@ -1,70 +0,0 @@
<svg width="0" height="0" class="hidden">
<!--main logo-->
<symbol aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 152.35 143.74" id="icon-logo">
<g transform="translate(-32.882 -133.08)">
<path d="m181.17 245.31-35.745 26.329c-4.6831 3.4634-10.478 5.3684-16.536 5.3684h-91.81c-2.3148 0-3.9952-1.9172-3.9952-4.2307v-25.397c0-2.3146 1.9182-4.2386 3.9952-4.2386l14.65 6e-3 12.303-9.9854c5.3181-4.2595 12.467-6.945 19.611-6.945h42.095c5.1594 0 9.226 4.5958 8.3661 9.8875-0.69453 4.1672-4.5958 7.0432-8.8292 7.0432h-20.463c-2.3148 0-3.9952 1.918-3.9952 3.9952 0 2.0772 1.9182 3.9952 3.9952 3.9952h31.909l31.671-23.328c4.7096-3.4898 11.327-2.4717 14.798 2.2402 3.6962 5.1803 2.6908 11.795-2.0188 15.261z"></path>
<path d="m106 133.08a44.526 43.709 0 0 1 44.526 43.709 44.526 43.709 0 0 1-44.526 43.709 44.526 43.709 0 0 1-44.526-43.709 44.526 43.709 0 0 1 44.526-43.709m0 8.7418a11.131 10.927 0 0 0-11.131 10.927 11.131 10.927 0 0 0 11.131 10.927 11.131 10.927 0 0 0 11.131-10.927 11.131 10.927 0 0 0-11.131-10.927m-33.84 24.171c-1.9146 5.7259 1.2908 11.889 7.1241 13.769 5.8774 1.8357 12.156-1.2671 14.026-7.0372 1.9146-5.726-1.2908-11.889-7.1241-13.769-5.833-1.8357-12.155 1.2671-14.026 7.0372m67.724-0.13216c-1.9146-5.6385-8.1927-8.7418-14.025-6.9061-5.8774 1.8359-9.0832 7.9989-7.1686 13.769 1.9146 5.7259 8.1927 8.8729 14.025 6.9934 5.833-1.8357 9.0388-8.0425 7.1686-13.856m-54.767 39.164c4.9424 3.5842 11.977 2.4914 15.539-2.404 3.6067-4.8954 2.4935-11.714-2.4933-15.255-4.9424-3.5404-11.933-2.4476-15.54 2.404-3.6065 4.8956-2.4935 11.714 2.4935 15.255m41.81 0c4.9868-3.5404 6.0999-10.359 2.4933-15.255-3.6066-4.8954-10.597-5.9882-15.539-2.404-4.987 3.5406-6.1 10.359-2.4935 15.255 3.5175 4.8954 10.597 5.9444 15.54 2.404m-20.927-34.793a6.6789 6.5564 0 0 0-6.6788 6.5563 6.6789 6.5564 0 0 0 6.6788 6.5565 6.6789 6.5564 0 0 0 6.6789-6.5565 6.6789 6.5564 0 0 0-6.6789-6.5563z"></path>
</g>
</symbol>
<!--theme switcher icon-->
<symbol aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-theme-switcher">
<path d="M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256s256-114.6 256-256S397.4 0 256 0zM256 448V64c105.9 0 192 86.13 192 192S361.9 448 256 448z"></path>
</symbol>
<!--miscellaneous logos-->
<symbol aria-hidden="true" focusable="false" role="img" 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 aria-hidden="true" focusable="false" role="img" 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 aria-hidden="true" focusable="false" role="img" 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 aria-hidden="true" focusable="false" role="img" 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 aria-hidden="true" focusable="false" role="img" 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 aria-hidden="true" focusable="false" role="img" 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 aria-hidden="true" focusable="false" role="img" 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 aria-hidden="true" focusable="false" role="img" 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 aria-hidden="true" focusable="false" role="img" 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 aria-hidden="true" focusable="false" role="img" 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 aria-hidden="true" focusable="false" role="img" 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 aria-hidden="true" focusable="false" role="img" 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 aria-hidden="true" focusable="false" role="img" 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>
<symbol aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-palette">
<path d="M2.53,19.65L3.87,20.21V11.18L1.44,17.04C1.03,18.06 1.5,19.23 2.53,19.65M22.03,15.95L17.07,4C16.76,3.23 16.03,2.77 15.26,2.75C15,2.75 14.73,2.79 14.47,2.9L7.1,5.95C6.35,6.26 5.89,7 5.87,7.75C5.86,8 5.91,8.29 6,8.55L11,20.5C11.29,21.28 12.03,21.74 12.81,21.75C13.07,21.75 13.33,21.7 13.58,21.6L20.94,18.55C21.96,18.13 22.45,16.96 22.03,15.95M7.88,8.75A1,1 0 0,1 6.88,7.75A1,1 0 0,1 7.88,6.75C8.43,6.75 8.88,7.2 8.88,7.75C8.88,8.3 8.43,8.75 7.88,8.75M5.88,19.75A2,2 0 0,0 7.88,21.75H9.33L5.88,13.41V19.75Z"></path>
</symbol>
<symbol aria-hidden="true" focusable="false" role="img" 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 aria-hidden="true" focusable="false" role="img" 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 aria-hidden="true" focusable="false" role="img" 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 aria-hidden="true" focusable="false" role="img" 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: 14 KiB

View file

@ -1,35 +0,0 @@
const rootEl = document.documentElement;
const input = document.querySelector('.theme-switcher__input');
// adding this property insures that the media query for dark mode in css won't be executed. We'll use media query from js instead. Toggle without this line would also work just fine. But then light would actually mean dark and vice-versa.
rootEl.setAttribute('js', 'enabled');
// function for adding or removing the theme(and checked state for checkbox) accordingly
const toggleTheme = theme => {
if (theme === 'dark') {
rootEl.setAttribute('theme', 'dark');
input.setAttribute('checked', '');
} else if (theme === 'light') {
rootEl.removeAttribute('theme');
input.removeAttribute('checked');
}
};
// storing user and browser preferences
const userPrefersTheme = typeof Storage ? localStorage.getItem('theme') : null;
const browserPrefersDarkTheme = window.matchMedia(
'(prefers-color-scheme: dark)'
).matches;
// setting theme taking into account calculated preferences
if (userPrefersTheme) toggleTheme(userPrefersTheme);
else if (browserPrefersDarkTheme) toggleTheme('dark');
// setting theme when user toggles the theme-switcher input(and storing the preference in localStorage)
input.addEventListener('change', e => {
let theme = e.target.checked ? 'dark' : 'light';
toggleTheme(theme);
localStorage.setItem('theme', theme);
});
// localStorage.removeItem("theme");

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

View file

@ -1,3 +1,3 @@
user-agent: *
disallow: /img/
disallow: /contact
User-agent: *
Allow: /about
Disallow: /

View file

@ -1,61 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="576.000000pt" height="576.000000pt" viewBox="0 0 576.000000 576.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,576.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M2600 5594 c-14 -3 -70 -11 -125 -20 -96 -15 -228 -49 -290 -75 -16
-7 -36 -13 -42 -15 -25 -5 -204 -95 -270 -135 -176 -108 -350 -269 -481 -444
-138 -184 -256 -466 -288 -685 -15 -109 -19 -164 -18 -300 1 -139 7 -189 39
-355 3 -16 22 -77 41 -135 151 -452 499 -823 944 -1007 93 -39 232 -84 238
-78 3 2 12 0 21 -4 9 -5 32 -12 51 -15 65 -11 95 -16 130 -22 55 -11 317 -12
395 -3 115 14 143 19 223 40 42 11 84 22 92 24 69 16 330 129 338 148 2 4 8 7
14 7 15 0 109 63 204 136 234 181 421 436 528 720 14 39 28 80 31 92 2 11 9
30 15 41 6 12 9 21 6 21 -3 0 0 15 5 33 5 17 12 46 15 62 2 17 7 37 9 45 17
65 25 344 12 460 -11 101 -25 172 -43 227 -7 23 -15 52 -18 65 -2 13 -15 52
-29 88 -110 289 -279 522 -519 718 -42 34 -81 62 -87 62 -6 0 -11 4 -11 9 0 5
-9 13 -20 16 -11 3 -26 13 -34 20 -28 28 -266 145 -326 160 -3 1 -18 7 -35 14
-16 7 -73 23 -125 36 -87 22 -121 29 -205 41 -41 7 -354 13 -385 8z m255 -336
c28 -6 77 -24 109 -41 184 -97 268 -320 192 -509 -111 -275 -463 -352 -681
-150 -42 39 -95 113 -100 141 -2 9 -4 17 -4 19 -1 1 -1 5 -2 10 0 4 -5 12 -10
18 -14 17 -18 125 -7 184 5 30 23 79 39 111 29 59 121 159 145 159 8 0 14 3
14 8 0 8 74 40 109 47 13 2 26 6 28 8 8 9 120 6 168 -5z m-932 -622 c2 3 24 0
48 -6 24 -6 52 -13 62 -15 49 -11 155 -94 202 -157 34 -46 67 -149 70 -223 4
-78 -40 -213 -84 -256 -9 -10 -29 -31 -45 -48 -98 -108 -298 -147 -446 -87
-105 43 -173 104 -220 201 -125 253 32 549 312 590 24 4 45 8 47 10 1 2 13 0
26 -5 13 -5 25 -7 28 -4z m1769 -1 c211 -16 381 -206 372 -415 -2 -30 -5 -65
-8 -77 -3 -13 -8 -31 -10 -41 -15 -68 -116 -191 -189 -231 -119 -66 -268 -72
-391 -16 -93 42 -161 110 -203 201 -39 87 -47 160 -28 258 34 164 171 293 340
320 21 4 41 9 43 11 3 2 8 2 13 -1 4 -2 32 -6 61 -9z m-834 -458 c7 -2 18 -9
24 -15 7 -7 17 -12 24 -12 7 0 28 -19 48 -41 128 -150 41 -383 -152 -406 -26
-3 -49 -6 -52 -6 -13 -2 -78 17 -103 30 -174 88 -176 346 -4 435 66 34 115 38
215 15z m-548 -562 c90 -14 231 -113 270 -191 8 -16 21 -37 29 -46 8 -10 11
-18 7 -18 -4 0 -2 -8 3 -18 9 -17 24 -101 24 -127 -3 -84 -37 -194 -72 -237
-120 -144 -256 -200 -421 -174 -118 19 -241 107 -297 213 -37 68 -44 95 -48
180 -6 116 34 220 117 302 50 50 151 107 200 113 15 2 29 6 32 9 6 6 104 2
156 -6z m1095 1 c16 -3 59 -20 94 -37 146 -72 234 -219 226 -378 -4 -92 -4
-92 -37 -163 -145 -313 -608 -318 -754 -8 -39 81 -42 96 -42 181 0 71 4 96 29
154 27 65 77 130 137 177 84 67 225 97 347 74z"/>
<path d="M5282 2084 c-74 -15 -167 -64 -228 -119 -6 -5 -22 -18 -35 -28 -85
-62 -248 -181 -266 -195 -12 -9 -32 -25 -45 -34 -13 -9 -95 -70 -183 -134 -88
-65 -162 -120 -165 -123 -3 -4 -23 -18 -45 -33 -22 -14 -51 -35 -65 -47 -14
-11 -93 -69 -175 -130 l-151 -110 -625 2 -626 2 -36 28 c-45 34 -67 74 -67
122 0 60 45 123 100 140 14 5 219 9 455 10 237 2 439 5 450 8 92 24 176 88
220 168 27 49 30 63 30 144 -1 77 -5 97 -27 140 -34 65 -107 132 -171 158 -51
20 -70 20 -915 21 -953 1 -978 0 -1158 -60 -120 -40 -302 -135 -360 -189 -6
-5 -28 -23 -49 -40 -21 -16 -80 -64 -130 -105 -50 -41 -109 -88 -130 -105 -21
-16 -43 -34 -49 -40 -6 -5 -36 -31 -67 -56 l-57 -45 -283 0 c-198 1 -293 -2
-318 -11 -46 -16 -89 -65 -97 -112 -10 -49 -9 -1014 0 -1029 4 -7 4 -12 1 -12
-3 0 4 -18 16 -39 44 -79 -136 -72 1939 -69 1020 2 1864 6 1875 10 11 3 40 11
65 18 70 19 200 72 200 82 0 5 4 7 9 4 9 -6 103 55 246 161 55 41 111 82 123
91 13 9 42 31 65 48 37 28 339 250 392 288 11 8 69 51 128 95 59 45 110 81
113 81 2 0 13 8 23 18 10 9 44 35 75 57 31 22 74 54 95 70 22 17 84 63 138
102 130 95 175 141 210 211 26 53 28 66 28 177 0 115 -1 123 -32 185 -85 173
-259 261 -441 224z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -3,18 +3,18 @@
"short_name": "libremdb",
"icons": [
{
"src": "/android-chrome-192x192.png",
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/android-chrome-512x512.png",
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"theme_color": "#0f2c67",
"background_color": "#0f2c67"
"theme_color": "#b80040",
"background_color": "#ffe5ef"
}

18
public/svg/sadgnu.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 47 KiB

46
public/svg/sprite.svg Normal file
View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1">
<!--main logo-->
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 152.35 143.74" id="icon-logo">
<g transform="translate(-32.882 -133.08)">
<path d="m181.17 245.31-35.745 26.329c-4.6831 3.4634-10.478 5.3684-16.536 5.3684h-91.81c-2.3148 0-3.9952-1.9172-3.9952-4.2307v-25.397c0-2.3146 1.9182-4.2386 3.9952-4.2386l14.65 6e-3 12.303-9.9854c5.3181-4.2595 12.467-6.945 19.611-6.945h42.095c5.1594 0 9.226 4.5958 8.3661 9.8875-0.69453 4.1672-4.5958 7.0432-8.8292 7.0432h-20.463c-2.3148 0-3.9952 1.918-3.9952 3.9952 0 2.0772 1.9182 3.9952 3.9952 3.9952h31.909l31.671-23.328c4.7096-3.4898 11.327-2.4717 14.798 2.2402 3.6962 5.1803 2.6908 11.795-2.0188 15.261z"></path>
<path d="m106 133.08a44.526 43.709 0 0 1 44.526 43.709 44.526 43.709 0 0 1-44.526 43.709 44.526 43.709 0 0 1-44.526-43.709 44.526 43.709 0 0 1 44.526-43.709m0 8.7418a11.131 10.927 0 0 0-11.131 10.927 11.131 10.927 0 0 0 11.131 10.927 11.131 10.927 0 0 0 11.131-10.927 11.131 10.927 0 0 0-11.131-10.927m-33.84 24.171c-1.9146 5.7259 1.2908 11.889 7.1241 13.769 5.8774 1.8357 12.156-1.2671 14.026-7.0372 1.9146-5.726-1.2908-11.889-7.1241-13.769-5.833-1.8357-12.155 1.2671-14.026 7.0372m67.724-0.13216c-1.9146-5.6385-8.1927-8.7418-14.025-6.9061-5.8774 1.8359-9.0832 7.9989-7.1686 13.769 1.9146 5.7259 8.1927 8.8729 14.025 6.9934 5.833-1.8357 9.0388-8.0425 7.1686-13.856m-54.767 39.164c4.9424 3.5842 11.977 2.4914 15.539-2.404 3.6067-4.8954 2.4935-11.714-2.4933-15.255-4.9424-3.5404-11.933-2.4476-15.54 2.404-3.6065 4.8956-2.4935 11.714 2.4935 15.255m41.81 0c4.9868-3.5404 6.0999-10.359 2.4933-15.255-3.6066-4.8954-10.597-5.9882-15.539-2.404-4.987 3.5406-6.1 10.359-2.4935 15.255 3.5175 4.8954 10.597 5.9444 15.54 2.404m-20.927-34.793a6.6789 6.5564 0 0 0-6.6788 6.5563 6.6789 6.5564 0 0 0 6.6788 6.5565 6.6789 6.5564 0 0 0 6.6789-6.5565 6.6789 6.5564 0 0 0-6.6789-6.5563z"></path>
</g>
</symbol>
<!--theme switcher icon-->
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-theme-switcher">
<path d="M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256s256-114.6 256-256S397.4 0 256 0zM256 448V64c105.9 0 192 86.13 192 192S361.9 448 256 448z"></path>
</symbol>
<!--miscellaneous logos-->
<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-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 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 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>
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-palette">
<path d="M2.53,19.65L3.87,20.21V11.18L1.44,17.04C1.03,18.06 1.5,19.23 2.53,19.65M22.03,15.95L17.07,4C16.76,3.23 16.03,2.77 15.26,2.75C15,2.75 14.73,2.79 14.47,2.9L7.1,5.95C6.35,6.26 5.89,7 5.87,7.75C5.86,8 5.91,8.29 6,8.55L11,20.5C11.29,21.28 12.03,21.74 12.81,21.75C13.07,21.75 13.33,21.7 13.58,21.6L20.94,18.55C21.96,18.13 22.45,16.96 22.03,15.95M7.88,8.75A1,1 0 0,1 6.88,7.75A1,1 0 0,1 7.88,6.75C8.43,6.75 8.88,7.2 8.88,7.75C8.88,8.3 8.43,8.75 7.88,8.75M5.88,19.75A2,2 0 0,0 7.88,21.75H9.33L5.88,13.41V19.75Z"></path>
</symbol>
<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>
<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>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

View file

@ -1,17 +0,0 @@
const express = require('express');
const viewController = require('../controllers/viewControllers');
const viewRouter = express.Router();
// generic routes
viewRouter.get('/about', viewController.about);
viewRouter.get('/privacy', viewController.privacy);
viewRouter.get('/contact', viewController.contact);
viewRouter.get('/similar-projects', viewController.similarProjects);
viewRouter.get('/', viewController.overview);
// routes identical to imdb
viewRouter.get('/title/:title', viewController.movie);
// viewRouter.get('/search/title/', viewController.searchGenres);
module.exports = viewRouter;

View file

@ -1,33 +0,0 @@
const dotenv = require('dotenv');
const app = require('./app');
//---------------------------------------------------------------------------//
// ERROR HANDLING IN NODEJS
//---------------------------------------------------------------------------//
// for synchronus errors that are not caught anywhere. Should come before any code
process.on('uncaughtException', err => {
console.log('\x1b[31m%s\x1b[0m', '🔴️ ERROR:', err);
process.exit(1);
});
// for rejected promises that aren't caught anywhere
process.on('unhandledRejection', err => {
console.log('\x1b[31m%s\x1b[0m', '🔴️ ERROR:', err);
server.close(() => process.exit(1)); // shutting the system down gracefully
});
//---------------------------------------------------------------------------//
// LOADING CONFIG FILE VARIABLES
//---------------------------------------------------------------------------//
dotenv.config({ path: './config.env' }); // loading .env variables
//---------------------------------------------------------------------------//
// STARTING SERVER
//---------------------------------------------------------------------------//
const port = process.env.PORT || 3000;
const server = app.listen(port, () =>
console.log(
'\x1b[36m%s\x1b[0m', // for colors lol. Search for ansi escape characters
`🎧️ listening at port ${port}. Env: ${process.env.NODE_ENV}`
)
);

View file

@ -0,0 +1,34 @@
import { useContext } from 'react';
import { themeContext } from 'src/context/theme-context';
import styles from 'src/styles/modules/components/buttons/themeToggler.module.scss';
type Props = {
className: string;
};
const ThemeToggler = (props: Props) => {
const { theme, setTheme } = useContext(themeContext);
const clickHandler = () => {
const themeToSet = theme === 'light' ? 'dark' : 'light';
setTheme(themeToSet);
};
return (
<button
className={`${styles.button} ${props.className}`}
onClick={clickHandler}
>
<span className='visually-hidden'>Change theme</span>
<svg
className={`icon ${styles.icon}`}
focusable='false'
aria-hidden='true'
role='img'
>
<use href='/svg/sprite.svg#icon-theme-switcher'></use>
</svg>
</button>
);
};
export default ThemeToggler;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,84 @@
import Link from 'next/link';
import ThemeToggler from 'src/components/buttons/ThemeToggler';
import styles from 'src/styles/modules/layout/header.module.scss';
type Props = { full?: boolean; originalPath?: string };
const Header = ({ full, originalPath }: Props) => {
return (
<header id='header' className={`${styles.header} ${full ? styles.header__about : ''}`}>
<div className={styles.topbar}>
<Link href='/find'>
<a aria-label='go to homepage' className={styles.logo}>
<svg className={styles.logo__icon} role='img' aria-hidden>
<use href='/svg/sprite.svg#icon-logo'></use>
</svg>
<span className={styles.logo__text}>libremdb</span>
</a>
</Link>
{full && (
<nav className={styles.nav}>
<ul className={styles.nav__list}>
<li className={styles.nav__item}>
<a href='#features' className='link'>
Features
</a>
</li>
<li className={styles.nav__item}>
<a href='#faq' className='link'>
FAQs
</a>
</li>
<li className={styles.nav__item}>
<a href='https://github.com/zyachel/libremdb' className='link'>
Source
</a>
</li>
</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>
</div>
{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{' '}
<a href='https://codeberg.org/teddit/teddit' className='link'>
teddit
</a>
,{' '}
<a href='https://github.com/zedeus/nitter' className='link'>
nitter
</a>
, and{' '}
<a href='https://github.com/digitalblossom/alternative-frontends' className='link'>
many others
</a>
.
</p>
</div>
)}
</header>
);
};
export default Header;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,6 @@
import styles from 'src/styles/modules/components/loaders/progress-bar.module.scss';
const ProgressBar = () => {
return <span className={styles.progress} role='progressbar'></span>;
};
export default ProgressBar;

View file

@ -0,0 +1,86 @@
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';
type Props = {
className: string;
media: Media;
};
// TODO: refactor this component.
const Media = ({ className, media }: Props) => {
return (
<div className={`${className} ${styles.media}`}>
{(media.trailer || !!media.videos.total) && (
<section className={styles.videos}>
<h2 className='heading heading__secondary'>Videos</h2>
<div className={styles.videos__container}>
{media.trailer && (
<div className={styles.trailer}>
<video
aria-label='trailer video'
controls
playsInline
poster={getProxiedIMDbImgUrl(modifyIMDbImg(media.trailer.thumbnail))}
className={styles.trailer__video}
preload='none'
>
{media.trailer.urls.map(source => (
<source
key={source.url}
type={source.mimeType}
src={getProxiedIMDbImgUrl(source.url)}
data-res={source.resolution}
/>
))}
</video>
</div>
)}
{!!media.videos.total &&
media.videos.videos.map(video => (
<Link href={`/video/${video.id}`} key={video.id}>
<a className={styles.video}>
<Image
className={styles.video__img}
src={modifyIMDbImg(video.thumbnail)}
alt=''
fill
sizes='400px'
/>
<p className={styles.video__caption}>
{video.caption} ({video.runtime}s)
</p>
</a>
</Link>
))}
</div>
</section>
)}
{!!media.images.total && (
<section className={styles.images}>
<h2 className='heading heading__secondary'>Images</h2>
<div className={styles.images__container}>
{media.images.images.map(image => (
<figure key={image.id} className={styles.image}>
<Image
className={styles.image__img}
src={modifyIMDbImg(image.url)}
alt=''
fill
sizes='400px'
/>
<figcaption className={styles.image__caption}>{image.caption.plainText}</figcaption>
</figure>
))}
</div>
</section>
)}
</div>
);
};
export default Media;

View file

@ -0,0 +1,42 @@
import Head from 'next/head';
import { ReactNode } from 'react';
type Props = {
title: string;
description?: string;
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' />
<meta httpEquiv='X-UA-Compatible' content='IE=edge' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<title key='title'>{`${title} | libremdb`}</title>
<meta key='desc' name='description' content={description} />
<link rel='icon' href='/favicon.ico' sizes='any' />
<link rel='icon' href='/icon.svg' type='image/svg+xml' />
<link rel='apple-touch-icon' href='/apple-touch-icon.png' />
<link rel='manifest' href='/site.webmanifest' />
<meta name='theme-color' content='#ffe5ef' />
<meta property='og:title' content={title} />
<meta property='og:description' content={description} />
<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()} />
</Head>
);
};
export default Meta;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,110 @@
import { Fragment } from 'react';
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';
type Props = {
className: string;
data: Basic;
};
const Basic = ({ data, className }: Props) => {
const titleType = data.type.id;
const releaseTime =
titleType === 'tvSeries'
? `${data.releaseYear?.start}-${data.releaseYear?.end || 'present'}`
: data.releaseYear?.start;
return (
<CardBasic
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>
{!!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>
);
};
export default Basic;

View file

@ -0,0 +1,32 @@
import { CardCast } from 'src/components/card';
import { Cast } from 'src/interfaces/shared/title';
import styles from 'src/styles/modules/components/title/cast.module.scss';
type Props = {
className: string;
cast: Cast;
};
const Cast = ({ className, cast }: Props) => {
if (!cast.length) return null;
return (
<section className={`${className} ${styles.container}`}>
<h2 className='heading heading__secondary'>Cast</h2>
<ul className={styles.cast}>
{cast.map(member => (
<CardCast
key={member.id}
link={`/name/${member.id}`}
name={member.name}
image={member.image}
characters={member.characters}
attributes={member.attributes}
/>
))}
</ul>
</section>
);
};
export default Cast;

View file

@ -0,0 +1,105 @@
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';
type Props = {
data: DidYouKnow;
};
const DidYouKnow = ({ data }: Props) => {
if (!Object.keys(data).length) return <></>;
return (
<section className={styles.didYouKnow}>
<h2 className='heading heading__secondary'>Did you know</h2>
<div className={styles.container}>
{data.trivia && (
<div className={styles.item}>
<h3 className='heading heading__tertiary'>Trivia</h3>
<div
className={styles.item__desc}
dangerouslySetInnerHTML={{ __html: data.trivia.html }}
></div>
</div>
)}
{data.goofs && (
<div className={styles.item}>
<h3 className='heading heading__tertiary'>Goofs</h3>
<div
className={styles.item__desc}
dangerouslySetInnerHTML={{ __html: data.goofs.html }}
></div>
</div>
)}
{data.quotes?.lines.length && (
// html spec says not to use blockquote & cite for conversations, even though it seems a perfect choice here.
// see 'note' part https://html.spec.whatwg.org/multipage/grouping-content.html#the-blockquote-element
<div className={styles.item}>
<h3 className='heading heading__tertiary'>Quotes</h3>
{data.quotes.lines.map((line, i) => (
<div className={styles.quotes} key={i}>
<p className={styles.quote}>
{line.name && (
<Link href={`/name/${line.id}`}>
<a className={'link'}>{line.name}</a>
</Link>
)}
{line.stageDirection && <i> [{line.stageDirection}] </i>}
{line.text && <span>: {line.text}</span>}
</p>
</div>
))}
</div>
)}
{data.crazyCredits && (
<div className={styles.item}>
<h3 className='heading heading__tertiary'>Crazy credits</h3>
<div
className={styles.item__desc}
dangerouslySetInnerHTML={{ __html: data.crazyCredits.html }}
></div>
</div>
)}
{data.alternativeVersions && (
<div className={styles.item}>
<h3 className='heading heading__tertiary'>Alternate versions</h3>
<div
className={styles.item__desc}
dangerouslySetInnerHTML={{
__html: data.alternativeVersions.html,
}}
></div>
</div>
)}
{data.connections && (
<div className={styles.item}>
<h3 className='heading heading__tertiary'>Connections</h3>
<p className={styles.item__desc}>
<span>{data.connections.startText} </span>
<Link href={`/title/${data.connections.title.id}`}>
<a className={'link'}>{data.connections.title.text}</a>
</Link>
<span> ({data.connections.title.year})</span>
</p>
</div>
)}
{data.soundTrack && (
<div className={styles.item}>
<h3 className='heading heading__tertiary'>Soundtracks</h3>
<div className={styles.list}>
<p>{data.soundTrack.title}</p>
{data.soundTrack.htmls &&
data.soundTrack.htmls.map(html => (
<div
key={html}
className={styles.item__desc}
dangerouslySetInnerHTML={{ __html: html }}
></div>
))}
</div>
</div>
)}
</div>
</section>
);
};
export default DidYouKnow;

View file

@ -0,0 +1,329 @@
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';
type Props = {
info: Info;
className: string;
};
const Info = ({ info, className }: Props) => {
const router = useRouter();
const { titleId } = router.query;
const { boxOffice, details, meta, keywords, technicalSpecs, accolades } =
info;
return (
<div className={`${className} ${styles.info}`}>
{meta.infoEpisode && (
<section className={styles.episodeInfo}>
<h2 className="heading heading__secondary">Episode info</h2>
<div className={styles.episodeInfo__container}>
{meta.infoEpisode.numSeason && (
<p className={styles.series}>
<span>Season: </span>
<span>{meta.infoEpisode.numSeason}</span>
</p>
)}
{meta.infoEpisode.numEpisode && (
<p>
<span>Episode: </span>
<span>{meta.infoEpisode.numEpisode}</span>
</p>
)}
<p>
<span>Series: </span>
<span>
<Link href={`/title/${meta.infoEpisode.series.id}`}>
<a className={'link'}>{meta.infoEpisode.series.title}</a>
</Link>
<span>
{' '}
({meta.infoEpisode.series.startYear}-
{meta.infoEpisode.series.endYear || 'present'})
</span>
</span>
</p>
{meta.infoEpisode.prevId && (
<p>
<Link href={`/title/${meta.infoEpisode.prevId}`}>
<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>
</Link>
</p>
)}
</div>
</section>
)}
{meta.infoSeries && (
<section className={styles.seriesInfo}>
<h2 className="heading heading__secondary">Series info</h2>
<div className={styles.seriesInfo__container}>
<p>
<span>Total Seasons: </span>
<span>{meta.infoSeries.seasons.length}</span>
</p>
<p>
<span>Total Years: </span>
<span>{meta.infoSeries.years.length}</span>
</p>
<p>
<span>Total Episodes: </span>
<span>{meta.infoSeries.totalEpisodes}</span>
</p>
<p>
<Link href={`/title/${titleId}/episodes`}>
<a className="link">See all Episodes</a>
</Link>
</p>
</div>
</section>
)}
<section className={styles.accolades}>
<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>
</Link>
</p>
)}
{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={`/title/${titleId}/awards`}>
<a className="link">View all awards</a>
</Link>
</p>
</div>
</section>
{!!keywords.total && (
<section className={styles.keywords}>
<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, '-')}`}
>
<a className="link">{word}</a>
</Link>
</li>
))}
</ul>
</section>
)}
{!!Object.keys(details).length && (
<section className={styles.details}>
<h2 className="heading heading__secondary">Details</h2>
<div className={styles.details__container}>
{details.releaseDate && (
<p>
<span>Release date: </span>
<time dateTime={details.releaseDate.date}>
{details.releaseDate.date}
</time>
<span> ({details.releaseDate.country.text})</span>
</p>
)}
{details.countriesOfOrigin && (
<p>
<span>Countries of origin: </span>
{details.countriesOfOrigin.map((country, i) => (
<span key={country.id}>
{!!i && ', '}
<Link
href={`/search/title/?country_of_origin=${country.id}`}
>
<a className="link">{country.text}</a>
</Link>
</span>
))}
</p>
)}
{details.officialSites && (
<p>
<span>Official sites: </span>
{details.officialSites.sites.map((site, i) => (
<span key={site.url}>
{!!i && ', '}
<a href={site.url} className="link">
{site.name}
</a>
</span>
))}
</p>
)}
{details.languages?.length && (
<p>
<span>Languages: </span>
{details.languages.map((lang, i) => (
<span key={lang.id}>
{!!i && ', '}
<Link href={`/search/title/?primary_language=${lang.id}`}>
<a className="link">{lang.text}</a>
</Link>
</span>
))}
</p>
)}
{details.alsoKnownAs && (
<p>
<span>Also known as: </span>
<span>{details.alsoKnownAs}</span>
</p>
)}
{details.filmingLocations?.total && (
<p>
<span>Filming locations: </span>
{details.filmingLocations.locations.map((loc, i) => (
<span key={loc}>
{!!i && ', '}
<Link href={`/search/title/?locations=${loc}`}>
<a className="link">{loc}</a>
</Link>
</span>
))}
</p>
)}
{!!details.production?.total && (
<p>
<span>Production companies: </span>
{details.production.companies.map((co, i) => (
<span key={co.id}>
{!!i && ', '}
<Link href={`/company/${co.id}`}>
<a className="link">{co.name}</a>
</Link>
</span>
))}
</p>
)}
</div>
</section>
)}
{!!Object.keys(boxOffice).length && (
<section className={styles.boxoffice}>
<h2 className="heading heading__secondary">Box office</h2>
<div className={styles.boxoffice__container}>
{boxOffice.budget && (
<p>
<span>Budget: </span>
<span>
{formatMoney(
boxOffice.budget.amount,
boxOffice.budget.currency
)}
</span>
</p>
)}
{boxOffice.grossUs && (
<p>
<span>Gross US & Canada: </span>
<span>
{formatMoney(
boxOffice.grossUs.amount,
boxOffice.grossUs.currency
)}
</span>
</p>
)}
{boxOffice.openingGrossUs && (
<p>
<span>Opening weekend US & Canada: </span>
<span>
{formatMoney(
boxOffice.openingGrossUs.amount,
boxOffice.openingGrossUs.currency
)}
<span> ({boxOffice.openingGrossUs.date})</span>
</span>
</p>
)}
{boxOffice.gross && (
<p>
<span>Gross worldwide: </span>
<span>
{formatMoney(
boxOffice.gross.amount,
boxOffice.gross.currency
)}
</span>
</p>
)}
</div>
</section>
)}
{!!Object.keys(technicalSpecs).length && (
<section className={styles.technical}>
<h2 className="heading heading__secondary">Technical specs</h2>
<div className={styles.technical__container}>
{technicalSpecs.runtime && (
<p>
<span>Runtime: </span>
<span>{formatTime(technicalSpecs.runtime)}</span>
</p>
)}
{!!technicalSpecs.colorations?.length && (
<p>
<span> Color: </span>
<span>
{technicalSpecs.colorations.map((color, i) => (
<span key={color.id}>
{!!i && ', '}
<Link href={`/search/title/?colors=${color.id}`}>
<a className="link">{color.name}</a>
</Link>
</span>
))}
</span>
</p>
)}
{!!technicalSpecs.soundMixes?.length && (
<p>
<span>Sound mix: </span>
<span>
{technicalSpecs.soundMixes?.map((sound, i) => (
<span key={sound.id}>
{!!i && ', '}
<Link href={`/search/title/?sound_mixes=${sound.id}`}>
<a className="link">{sound.name}</a>
</Link>
</span>
))}
</span>
</p>
)}
{!!technicalSpecs.aspectRatios?.length && (
<p>
<span>Aspect ratio: </span>
<span>{technicalSpecs.aspectRatios.join(', ')}</span>
</p>
)}
</div>
</section>
)}
</div>
);
};
export default Info;

View file

@ -0,0 +1,32 @@
import { CardTitle } from 'src/components/card';
import { MoreLikeThis } from 'src/interfaces/shared/title';
import styles from 'src/styles/modules/components/title/more-like-this.module.scss';
type Props = {
className: string;
data: MoreLikeThis;
};
const MoreLikeThis = ({ className, data }: Props) => {
if (!data.length) return null;
return (
<section className={`${className} ${styles.morelikethis}`}>
<h2 className='heading heading__secondary'>More like this</h2>
<ul className={styles.container}>
{data.map(title => (
<CardTitle
key={title.id}
link={`/title/${title.id}`}
name={title.title}
titleType={title.type.text}
image={title.poster?.url}
year={title.releaseYear}
ratings={title.ratings}
/>
))}
</ul>
</section>
);
};
export default MoreLikeThis;

View file

@ -0,0 +1,82 @@
import { useRouter } 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';
type Props = {
reviews: Reviews;
};
const Reviews = ({ reviews }: Props) => {
const router = useRouter();
const { titleId } = router.query;
return (
<section className={styles.reviews}>
<h2 className="heading heading__secondary">Reviews</h2>
{reviews.featuredReview && (
<article className={styles.reviews__reviewContainer}>
<details className={styles.review}>
<summary className={styles.review__summary}>
<strong>{reviews.featuredReview.review.summary}</strong>
</summary>
<div
className={styles.review__text}
dangerouslySetInnerHTML={{
__html: reviews.featuredReview.review.html,
}}
></div>
</details>
<footer className={styles.review__metadata}>
<p>
{reviews.featuredReview.rating && (
<span>Rated {reviews.featuredReview.rating}/10</span>
)}
<span>
{' '}
by{' '}
<Link href={`/user/${reviews.featuredReview.reviewer.id}`}>
<a className="link">{reviews.featuredReview.reviewer.name}</a>
</Link>
</span>
<span> on {reviews.featuredReview.date}.</span>
</p>
<p>
<span>
{formatNumber(reviews.featuredReview.votes.up)} upvotes
</span>
<span>
, {formatNumber(reviews.featuredReview.votes.down)} downvotes
</span>
</p>
</footer>
</article>
)}
<div className={styles.reviews__stats}>
<p>
<Link href={`/title/${titleId}/reviews`}>
<a className="link">
{formatNumber(reviews.numUserReviews)} User reviews
</a>
</Link>
</p>
<p>
<Link href={`/title/${titleId}/externalreviews`}>
<a className="link">
{formatNumber(reviews.numCriticReviews)} Critic reviews
</a>
</Link>
</p>
<p>
<Link href={`/title/${titleId}/criticreviews`}>
<a className="link"> {reviews.metacriticScore} Metascore</a>
</Link>
</p>
</div>
</section>
);
};
export default Reviews;

View file

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

View file

@ -0,0 +1,58 @@
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 browserPrefersDarkTheme = window.matchMedia(
'(prefers-color-scheme: dark)'
).matches;
if (userPrefersTheme) return userPrefersTheme;
else if (browserPrefersDarkTheme) return 'dark';
else return 'light';
};
const updateMetaTheme = () => {
const meta = document.querySelector(
'meta[name="theme-color"]'
) as HTMLMetaElement;
const footerClr = window.getComputedStyle(document.body).backgroundColor;
meta.content = footerClr;
};
const initialContext = {
theme: '',
setTheme: (theme: ReturnType<typeof getInitialTheme>) => { },
};
export const themeContext = createContext(initialContext);
const ThemeProvider = ({ children }: { children: ReactNode }) => {
const [curTheme, setCurTheme] = useState(getInitialTheme);
const setTheme = (theme: typeof curTheme) => {
setCurTheme(theme);
if (isLocalStorageAvailable()) window.localStorage.setItem('theme', theme);
document.documentElement.dataset.theme = theme;
updateMetaTheme();
};
const providerValue = {
theme: curTheme,
setTheme: setTheme,
};
return (
<themeContext.Provider value={providerValue}>
{children}
</themeContext.Provider>
);
};
export default ThemeProvider;

View file

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

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