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.
9
.env.local.example
Normal file
|
@ -0,0 +1,9 @@
|
|||
# required fields
|
||||
# used for meta tags. e.g: 'https://libremdb.iket.me' don't add end slash.
|
||||
NEXT_PUBLIC_URL=
|
||||
|
||||
# optional fields. uncomment them and add the values if you wish so.
|
||||
# default useragent for requesting data from imdb is 'axios/0.27.2'
|
||||
# AXIOS_USERAGENT=
|
||||
# default accept header is 'application/json, text/plain, */*'
|
||||
# AXIOS_ACCEPT=
|
7
.eslintrc.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals",
|
||||
"rules": {
|
||||
"react/no-unescaped-entities": "off",
|
||||
"@next/next/no-page-custom-font": "off"
|
||||
}
|
||||
}
|
1
.github/workflows/release.yml
vendored
|
@ -3,6 +3,7 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- main
|
||||
- next
|
||||
|
||||
jobs:
|
||||
changelog-and-release:
|
||||
|
|
49
.gitignore
vendored
|
@ -1,21 +1,34 @@
|
|||
#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/*
|
||||
|
|
90
README.md
|
@ -1,61 +1,58 @@
|
|||
# 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" />
|
||||
_(This is a rewrite of libremdb in Next.js. The information below corresponds to this branch only. I'll make this branch default sometime later.)_
|
||||
| | |
|
||||
| -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| <img src="./public/img/misc/preview.png" title="screenshot (desktop screen, light mode)" width="1500" /> | <img src="./public/img/misc/preview2.png" title="screenshot (mobile screen, dark mode)" width="385" /> |
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
|
||||
---
|
||||
|
||||
## Instances
|
||||
|
||||
| Instance URL | Region | Notes |
|
||||
| -------------------------------- | ------------- | --------------------------------------------------------- |
|
||||
| https://libremdb.herokuapp.com | United States | Official; Hosted on Heroku |
|
||||
| https://libremdb.pussthecat.org | Germany | Operated by [PussTheCat.org](https://pussthecat.org/) |
|
||||
| https://libremdbeu.herokuapp.com | Europe | Operated by [toyboatcash](https://github.com/toyboatcash) |
|
||||
| https://lmdb.tokhmi.xyz/ | U.S. | Operated by [Tokhmi](https://tokhmi.xyz) |
|
||||
| https://libremdb.esmailelbob.xyz/ | Canada | Operated by [Esmail EL BoB](https://esmailelbob.xyz) |
|
||||
| http://libremdb.lqs5fjmajyp7rvp4qvyubwofzi6d4imua7vs237rkc4m5qogitqwrgyd.onion/ | Canada | Operated by [Esmail EL BoB](https://esmailelbob.xyz) |
|
||||
| ------------------------ | ------ | -------------- |
|
||||
| https://libremdb.iket.me | Canada | Operated by me |
|
||||
|
||||
---
|
||||
|
||||
## FAQs
|
||||
## Questions you might have
|
||||
|
||||
- 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.
|
||||
- I see connection being made to some Amazon domains.
|
||||
For now, images and videos are directly served from Amazon. If I have enough time in the future, I'll implement a way to serve the images from libremdb instead.
|
||||
|
||||
- Will amazon track me then?
|
||||
They may log your IP address. I'd recommend using a VPN for mitigating this risk.
|
||||
- Will Amazon track me then?
|
||||
They may log your IP address, useragent, and other such
|
||||
identifiers. I'd recommend using a VPN, or accessing the website through TOR for mitigating this risk.
|
||||
|
||||
- Why not just use IMDb?
|
||||
Refer to the [features section](#features) above.
|
||||
|
@ -66,12 +63,14 @@ Inspired by projects like [teddit](https://codeberg.org/teddit/teddit), [nitter]
|
|||
|
||||
## Privacy
|
||||
|
||||
In short: libremdb doesn't collect any data at all.
|
||||
- Information collected:
|
||||
None.
|
||||
|
||||
- Data you directly provide: None.
|
||||
- Data you passively provide: A stack trace is logged to console on the server if you hit some error route.
|
||||
- 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.
|
||||
- Data collected by other services: libremdb connects to `m.media-amazon.com` for fetching images. So, Amazon might log your IP address. If you use the official instance which is deployed on Heroku, then Heroku might also log you IP address. You're advised to follow due precaution.
|
||||
- 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:
|
||||
libremdb connects to 'media-amazon.com' and 'media-imdb.com' for fetching images and videos. So, Amazon might log your IP address, and other information(such as http headers) sent by your browser.
|
||||
|
||||
---
|
||||
|
||||
|
@ -80,21 +79,23 @@ In short: libremdb doesn't collect any data at all.
|
|||
### soon
|
||||
|
||||
- [ ] add advanced search route
|
||||
- [ ] add did you know and reviews on movie info page
|
||||
- [x] add did you know and reviews on movie info page
|
||||
- [ ] implement routes for reviews, quotes, goofs, trivia and crazy credits
|
||||
|
||||
### at a later stage
|
||||
|
||||
- [ ] use redis
|
||||
- [x] implement a better installation method
|
||||
- [ ] implement a better installation method
|
||||
- [ ] serve images from libremdb itself
|
||||
- [ ] add a way to see trailer and other videos
|
||||
- [x] add a way to see trailer and other videos
|
||||
- [ ] implement other trivial routes
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
@ -104,11 +105,13 @@ In short: libremdb doesn't collect any data at all.
|
|||
2. Clone and set up the repo.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/zyachel/libremdb.git # replace gituhb.com with codeberg.org if you wish so.
|
||||
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
|
||||
# optional configuration
|
||||
cp .env.local.example .env.local
|
||||
# replace 'pnpm' with yarn or npm if you use those
|
||||
pnpm install
|
||||
pnpm build
|
||||
pnpm start
|
||||
```
|
||||
|
||||
|
@ -116,7 +119,7 @@ libremdb will start running at http://localhost:3000.
|
|||
|
||||
### Docker
|
||||
|
||||
There's a [docker image](https://github.com/PussTheCat-org/docker-libremdb-quay) made by [@TheFrenchGhosty](https://github.com/TheFrenchGhosty) for [PussTheCat.org's instance](https://libremdb.pussthecat.org). You can use that in case you wish to use docker.
|
||||
No image available yet.
|
||||
|
||||
---
|
||||
|
||||
|
@ -124,21 +127,17 @@ There's a [docker image](https://github.com/PussTheCat-org/docker-libremdb-quay)
|
|||
|
||||
### Automatic redirection
|
||||
|
||||
Use any of these extensions to automatically redirect IMDb URLs to libremdb:
|
||||
|
||||
- [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\/title\/([^\?]*)
|
||||
Redirect to: https://libremdb.herokuapp.com/title/$2
|
||||
Include pattern: https?:\/\/(www\.)?imdb\.com\/([^\?]*)
|
||||
Redirect to: https://libremdb.iket.me/$2
|
||||
Pattern type: Regular Expression
|
||||
```
|
||||
|
||||
- [LibRedirect](https://github.com/libredirect/libredirect/)
|
||||
|
||||
### Similar projects
|
||||
|
||||
- [Teddit](https://codeberg.org/teddit/teddit)
|
||||
|
@ -159,10 +158,7 @@ Use any of these extensions to automatically redirect IMDb URLs to libremdb:
|
|||
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
|
|
69
app.js
|
@ -1,69 +0,0 @@
|
|||
const express = require('express');
|
||||
const path = require('path');
|
||||
const morgan = require('morgan');
|
||||
const helmet = require('helmet');
|
||||
const compression = require('compression');
|
||||
const dotenv = require('dotenv');
|
||||
|
||||
const viewRouter = require('./routes/viewRoutes');
|
||||
const globalErrorHandler = require('./controllers/errorControllers');
|
||||
const { AppError } = require('./utils/errorUtils');
|
||||
|
||||
const app = express();
|
||||
|
||||
//---------------------------------------------------------------------------//
|
||||
// LOADING CONFIG FILE VARIABLES
|
||||
//---------------------------------------------------------------------------//
|
||||
dotenv.config({ path: './config.env' }); // loading .env variables
|
||||
|
||||
//-------------------------------------------------------------------------//
|
||||
// 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'), {
|
||||
maxAge: process.env.CACHE_PERIOD || '1h',
|
||||
})
|
||||
); // 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;
|
|
@ -1,10 +0,0 @@
|
|||
# default port is 3000
|
||||
PORT=3000
|
||||
# url
|
||||
URL=http://localhost:3000
|
||||
# enviorment
|
||||
NODE_ENV=production
|
||||
# change image quality
|
||||
IMAGE_QUALITY=500
|
||||
# cache duration for static assets(eg: '2.5 days', 3600, 2m)
|
||||
CACHE_PERIOD=1h
|
|
@ -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;
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
*/
|
|
@ -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;
|
5
next-env.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
25
next.config.mjs
Normal file
|
@ -0,0 +1,25 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
swcMinify: true,
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: '/',
|
||||
destination: '/about',
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
images: {
|
||||
domains: ['m.media-amazon.com'],
|
||||
},
|
||||
experimental: {
|
||||
images: {
|
||||
allowFutureImage: true,
|
||||
},
|
||||
isrMemoryCacheSize: 20 * 1024 * 1024,
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
57
package.json
|
@ -1,47 +1,36 @@
|
|||
{
|
||||
"name": "libremdb",
|
||||
"version": "0.1.2",
|
||||
"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)"
|
||||
},
|
||||
"description": "a free & open source IMDb front-end",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"author": "libremdb-contributors",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"cheerio": "1.0.0-rc.11",
|
||||
"compression": "^1.7.4",
|
||||
"dotenv": "^16.0.1",
|
||||
"express": "^4.18.1",
|
||||
"helmet": "^5.1.0",
|
||||
"morgan": "^1.10.0",
|
||||
"pug": "^3.0.2",
|
||||
"sass": "^1.52.2"
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"next": "12.2.5",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"sharp": "^0.31.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/morgan": "^1.9.3",
|
||||
"nodemon": "^2.0.16"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
"node_modules/*",
|
||||
"public/*"
|
||||
]
|
||||
"@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.54.4",
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.5.0",
|
||||
"npm": ">= 8.5.5",
|
||||
"pnpm": "^7.1.8"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"json-schema@<0.4.0": ">=0.4.0"
|
||||
}
|
||||
"node": ">=16.5.0",
|
||||
"pnpm": ">=7.0.0"
|
||||
}
|
||||
}
|
2677
pnpm-lock.yaml
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 1.8 KiB |
|
@ -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>
|
Before Width: | Height: | Size: 1 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 4.2 KiB |
BIN
public/fonts/RedHatDisplay-Italic-VariableFont_wght.ttf
Normal file
BIN
public/fonts/RedHatDisplay-VariableFont_wght.ttf
Normal file
BIN
public/icon-192.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
public/icon-512.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
10
public/icon.svg
Normal 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 |
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 571 KiB After Width: | Height: | Size: 805 KiB |
BIN
public/img/misc/preview2.png
Normal file
After Width: | Height: | Size: 1.6 MiB |
|
@ -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");
|
Before Width: | Height: | Size: 4 KiB |
|
@ -1,3 +1,3 @@
|
|||
User-agent: *
|
||||
Allow: /about
|
||||
Disallow: /
|
||||
|
||||
|
|
|
@ -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 |
|
@ -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
After Width: | Height: | Size: 47 KiB |
|
@ -1,6 +1,8 @@
|
|||
<svg width="0" height="0" class="hidden">
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<svg version="1.1">
|
||||
<!--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">
|
||||
<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>
|
||||
|
@ -8,63 +10,64 @@
|
|||
</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">
|
||||
<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 aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-cancel">
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-cancel">
|
||||
<path d="M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256s256-114.6 256-256S397.4 0 256 0zM64 256c0-41.4 13.3-79.68 35.68-111.1l267.4 267.4C335.7 434.7 297.4 448 256 448C150.1 448 64 361.9 64 256zM412.3 367.1L144.9 99.68C176.3 77.3 214.6 64 256 64c105.9 0 192 86.13 192 192C448 297.4 434.7 335.7 412.3 367.1z"></path>
|
||||
</symbol>
|
||||
<symbol aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" id="icon-code-document">
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" id="icon-code-document">
|
||||
<path d="M162.1 257.8c-7.812-7.812-20.47-7.812-28.28 0l-48 48c-7.812 7.812-7.812 20.5 0 28.31l48 48C137.8 386.1 142.9 388 148 388s10.23-1.938 14.14-5.844c7.812-7.812 7.812-20.5 0-28.31L128.3 320l33.86-33.84C169.1 278.3 169.1 265.7 162.1 257.8zM365.3 93.38l-74.63-74.64C278.6 6.742 262.3 0 245.4 0H64C28.65 0 0 28.65 0 64l.0065 384c0 35.34 28.65 64 64 64H320c35.2 0 64-28.8 64-64V138.6C384 121.7 377.3 105.4 365.3 93.38zM336 448c0 8.836-7.164 16-16 16H64.02c-8.838 0-16-7.164-16-16L48 64.13c0-8.836 7.164-16 16-16h160L224 128c0 17.67 14.33 32 32 32h79.1V448zM221.9 257.8c-7.812 7.812-7.812 20.5 0 28.31L255.7 320l-33.86 33.84c-7.812 7.812-7.812 20.5 0 28.31C225.8 386.1 230.9 388 236 388s10.23-1.938 14.14-5.844l48-48c7.812-7.812 7.812-20.5 0-28.31l-48-48C242.3 250 229.7 250 221.9 257.8z"></path>
|
||||
</symbol>
|
||||
<symbol aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" id="icon-computer-home">
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" id="icon-computer-home">
|
||||
<path d="M218.3 8.486C230.6-2.829 249.4-2.829 261.7 8.486L469.7 200.5C476.4 206.7 480 215.2 480 224H336C316.9 224 299.7 232.4 288 245.7V208C288 199.2 280.8 192 272 192H208C199.2 192 192 199.2 192 208V272C192 280.8 199.2 288 208 288H271.1V416H112C85.49 416 64 394.5 64 368V256H32C18.83 256 6.996 247.9 2.198 235.7C-2.6 223.4 .6145 209.4 10.3 200.5L218.3 8.486zM336 256H560C577.7 256 592 270.3 592 288V448H624C632.8 448 640 455.2 640 464C640 490.5 618.5 512 592 512H303.1C277.5 512 255.1 490.5 255.1 464C255.1 455.2 263.2 448 271.1 448H303.1V288C303.1 270.3 318.3 256 336 256zM352 304V448H544V304H352z"></path>
|
||||
</symbol>
|
||||
<symbol aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" id="icon-link-slash">
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" id="icon-link-slash">
|
||||
<path d="M485.1 354.9l113.5-113.5c55.21-55.21 55.21-144.7 0-199.9C570.1 13.8 534.8 0 498.6 0s-72.36 13.8-99.96 41.41l-43.36 43.36c15.11 8.012 29.47 17.58 41.91 30.02c3.146 3.146 5.898 6.518 8.742 9.838l37.96-37.96C458.5 72.05 477.1 64 498.6 64s40.1 8.047 54.71 22.66c14.61 14.61 22.66 34.04 22.66 54.71s-8.049 40.1-22.66 54.71l-119 119l-30.09-23.59c21.49-51.28 12.12-112.4-29.63-154.1C346.1 109.8 310.8 96 274.6 96c-29.6 0-58.93 9.752-83.83 28.23L38.81 5.109C34.41 1.672 29.19 0 24.03 0c-7.125 0-14.19 3.156-18.91 9.187c-8.188 10.44-6.375 25.53 4.062 33.7l591.1 463.1c10.5 8.203 25.56 6.328 33.69-4.078c8.188-10.44 6.375-25.53-4.062-33.7L485.1 354.9zM350.8 249.6L244.3 166.2C253.8 162.2 264 160 274.6 160c20.67 0 40.1 8.049 54.71 22.66c14.62 14.61 22.66 34.04 22.66 54.71C352 241.5 351.4 245.6 350.8 249.6zM234 387.4l-37.96 37.96C181.5 439.1 162 448 141.4 448c-20.67 0-40.1-8.047-54.71-22.66c-14.61-14.61-22.66-34.04-22.66-54.71s8.049-40.1 22.66-54.71l84.83-84.83L120.7 191.3L41.41 270.7c-55.21 55.21-55.21 144.7 0 199.9C69.01 498.2 105.2 512 141.4 512c36.18 0 72.36-13.8 99.96-41.41l43.36-43.36c-15.11-8.012-29.47-17.58-41.91-30.02C239.6 394.1 236.9 390.7 234 387.4zM265.4 374.6C293 402.2 329.2 416 365.4 416c11.98 0 23.84-2.082 35.51-5.111L224.6 272.7C223.9 309.5 237.3 346.5 265.4 374.6z"></path>
|
||||
</symbol>
|
||||
<symbol aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" id="icon-eye-slash">
|
||||
<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 aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-feather">
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-feather">
|
||||
<path d="M483.4 244.2L351.9 287.1h97.74c-9.874 10.62 3.75-3.125-46.24 46.87l-147.6 49.12h98.24c-74.99 73.12-194.6 70.62-246.8 54.1l-66.14 65.99c-9.374 9.374-24.6 9.374-33.98 0s-9.374-24.6 0-33.98l259.5-259.2c6.249-6.25 6.249-16.37 0-22.62c-6.249-6.249-16.37-6.249-22.62 0l-178.4 178.2C58.78 306.1 68.61 216.7 129.1 156.3l85.74-85.68c90.62-90.62 189.8-88.27 252.3-25.78C517.8 95.34 528.9 169.7 483.4 244.2z"></path>
|
||||
</symbol>
|
||||
<symbol aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-fast-forward">
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-fast-forward">
|
||||
<path d="M52.51 440.6l171.5-142.9V214.3L52.51 71.41C31.88 54.28 0 68.66 0 96.03v319.9C0 443.3 31.88 457.7 52.51 440.6zM308.5 440.6l192-159.1c15.25-12.87 15.25-36.37 0-49.24l-192-159.1c-20.63-17.12-52.51-2.749-52.51 24.62v319.9C256 443.3 287.9 457.7 308.5 440.6z"></path>
|
||||
</symbol>
|
||||
<symbol aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-graph-rising">
|
||||
<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 aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" id="icon-rating">
|
||||
<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 aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-rewind">
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="icon-rewind">
|
||||
<path d="M459.5 71.41l-171.5 142.9v83.45l171.5 142.9C480.1 457.7 512 443.3 512 415.1V96.03C512 68.66 480.1 54.28 459.5 71.41zM203.5 71.41L11.44 231.4c-15.25 12.87-15.25 36.37 0 49.24l192 159.1c20.63 17.12 52.51 2.749 52.51-24.62v-319.9C255.1 68.66 224.1 54.28 203.5 71.41z"></path>
|
||||
</symbol>
|
||||
<symbol aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-like-dislike">
|
||||
<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 aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" id="icon-person-slash">
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" id="icon-person-slash">
|
||||
<path d="M95.1 477.3c0 19.14 15.52 34.67 34.66 34.67h378.7c5.625 0 10.73-1.65 15.42-4.029L264.9 304.3C171.3 306.7 95.1 383.1 95.1 477.3zM630.8 469.1l-277.1-217.9c54.69-14.56 95.18-63.95 95.18-123.2C447.1 57.31 390.7 0 319.1 0C250.2 0 193.7 55.93 192.3 125.4l-153.4-120.3C34.41 1.672 29.19 0 24.03 0C16.91 0 9.845 3.156 5.127 9.187c-8.187 10.44-6.375 25.53 4.062 33.7L601.2 506.9c10.5 8.203 25.56 6.328 33.69-4.078C643.1 492.4 641.2 477.3 630.8 469.1z"></path>
|
||||
</symbol>
|
||||
<symbol aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-image-slash">
|
||||
<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 aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-palette">
|
||||
<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 aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-responsive">
|
||||
<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 aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-legal">
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-legal">
|
||||
<path d="M12,3C10.73,3 9.6,3.8 9.18,5H3V7H4.95L2,14C1.53,16 3,17 5.5,17C8,17 9.56,16 9,14L6.05,7H9.17C9.5,7.85 10.15,8.5 11,8.83V20H2V22H22V20H13V8.82C13.85,8.5 14.5,7.85 14.82,7H17.95L15,14C14.53,16 16,17 18.5,17C21,17 22.56,16 22,14L19.05,7H21V5H14.83C14.4,3.8 13.27,3 12,3M12,5A1,1 0 0,1 13,6A1,1 0 0,1 12,7A1,1 0 0,1 11,6A1,1 0 0,1 12,5M5.5,10.25L7,14H4L5.5,10.25M18.5,10.25L20,14H17L18.5,10.25Z"></path>
|
||||
</symbol>
|
||||
<symbol aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-code-block">
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-code-block">
|
||||
<path d="M5,3H7V5H5V10A2,2 0 0,1 3,12A2,2 0 0,1 5,14V19H7V21H5C3.93,20.73 3,20.1 3,19V15A2,2 0 0,0 1,13H0V11H1A2,2 0 0,0 3,9V5A2,2 0 0,1 5,3M19,3A2,2 0 0,1 21,5V9A2,2 0 0,0 23,11H24V13H23A2,2 0 0,0 21,15V19A2,2 0 0,1 19,21H17V19H19V14A2,2 0 0,1 21,12A2,2 0 0,1 19,10V5H17V3H19M12,15A1,1 0 0,1 13,16A1,1 0 0,1 12,17A1,1 0 0,1 11,16A1,1 0 0,1 12,15M8,15A1,1 0 0,1 9,16A1,1 0 0,1 8,17A1,1 0 0,1 7,16A1,1 0 0,1 8,15M16,15A1,1 0 0,1 17,16A1,1 0 0,1 16,17A1,1 0 0,1 15,16A1,1 0 0,1 16,15Z"></path>
|
||||
</symbol>
|
||||
<symbol aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-ads-slash">
|
||||
<symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="icon-ads-slash">
|
||||
<path d="M12.2 9L10.2 7H13C14.1 7 15 7.9 15 9V11.8L13 9.8V9H12.2M23 9V7H19C17.9 7 17 7.9 17 9V11C17 12.1 17.9 13 19 13H21V15H18.2L20.2 17H21C22.1 17 23 16.1 23 15V13C23 11.9 22.1 11 21 11H19V9H23M22.1 21.5L20.8 22.8L14.4 16.4C14.1 16.7 13.6 17 13 17H9V10.9L7 8.9V17H5V13H3V17H1V9C1 7.9 1.9 7 3 7H5.1L1.1 3L2.4 1.7L22.1 21.5M5 9H3V11H5V9M13 14.9L11 12.9V15H13V14.9Z"></path>
|
||||
</symbol>
|
||||
|
||||
</svg>
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 13 KiB |
|
@ -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;
|
27
server.js
|
@ -1,27 +0,0 @@
|
|||
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
|
||||
});
|
||||
|
||||
//---------------------------------------------------------------------------//
|
||||
// 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}`
|
||||
)
|
||||
);
|
48
src/components/Error/ErrorInfo.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
import Link from 'next/link';
|
||||
import Layout from '../../layouts/Layout';
|
||||
import Meta from '../Meta/Meta';
|
||||
|
||||
import styles from '../../styles/modules/components/error/error-info.module.scss';
|
||||
|
||||
// for details regarding the svg, go to sadgnu.svg file
|
||||
// description copied verbatim from https://www.gnu.org/graphics/sventsitsky-sadgnu.html
|
||||
// 404 idea from ninamori.org 404 page.
|
||||
|
||||
const ErrorInfo = ({ message = 'Not found, sorry.', statusCode = 404 }) => {
|
||||
return (
|
||||
<>
|
||||
<Meta
|
||||
title={`${message} (${statusCode})`}
|
||||
description='you encountered an error page!'
|
||||
/>
|
||||
<Layout className={styles.error}>
|
||||
<svg
|
||||
className={styles.gnu}
|
||||
focusable='false'
|
||||
role='img'
|
||||
aria-labelledby='gnu-title gnu-desc'
|
||||
>
|
||||
<title id='gnu-title'>GNU and Tux</title>
|
||||
<desc id='gnu-desc'>
|
||||
A pencil drawing of a big gnu and a small penguin, both very sad.
|
||||
GNU is despondently sitting on a bench, and Tux stands beside him,
|
||||
looking down and patting him on the back.
|
||||
</desc>
|
||||
<use href='/svg/sadgnu.svg#sad-gnu'></use>
|
||||
</svg>
|
||||
<h1 className={`heading heading__primary ${styles.heading}`}>
|
||||
<span>{message}</span>
|
||||
<span> ({statusCode})</span>
|
||||
</h1>
|
||||
<p className={styles.back}>
|
||||
Go back to{' '}
|
||||
<Link href='/about'>
|
||||
<a className='link'>the homepage</a>
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default ErrorInfo;
|
40
src/components/Meta/Meta.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import Head from 'next/head';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
description?: string;
|
||||
imgUrl?: string;
|
||||
};
|
||||
|
||||
const Meta = ({
|
||||
title,
|
||||
description = 'libremdb, a free & open source IMDb front-end.',
|
||||
imgUrl = 'icon.svg',
|
||||
}: Props) => {
|
||||
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={`${process.env.NEXT_PUBLIC_URL}/${imgUrl}`}
|
||||
/>
|
||||
</Head>
|
||||
);
|
||||
};
|
||||
export default Meta;
|
35
src/components/buttons/ThemeToggler.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { useContext } from 'react';
|
||||
import { themeContext } from '../../context/theme-context';
|
||||
|
||||
import styles from '../../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}`}
|
||||
aria-label='Change theme'
|
||||
onClick={clickHandler}
|
||||
>
|
||||
<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;
|
6
src/components/loaders/ProgressBar.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import styles from '../../styles/modules/components/loaders/progress-bar.module.scss';
|
||||
|
||||
const ProgressBar = () => {
|
||||
return <span className={styles.progress} role='progressbar'></span>;
|
||||
};
|
||||
export default ProgressBar;
|
153
src/components/title/Basic.tsx
Normal file
|
@ -0,0 +1,153 @@
|
|||
import { Fragment } from 'react';
|
||||
import Image from 'next/future/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { formatNumber, formatTime, modifyIMDbImg } from '../../utils/helpers';
|
||||
import { Basic } from '../../interfaces/shared/title';
|
||||
import styles from '../../styles/modules/components/title/basic.module.scss';
|
||||
|
||||
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 (
|
||||
<section
|
||||
// role is valid but not known to jsx-a11y
|
||||
// aria-description={`basic info for '${data.title}'`}
|
||||
// style={{ backgroundImage: data.poster && `url(${data.poster?.url})` }}
|
||||
className={`${styles.container} ${className}`}
|
||||
>
|
||||
<div
|
||||
className={styles.imageContainer}
|
||||
style={{
|
||||
backgroundImage:
|
||||
data.poster && `url(${modifyIMDbImg(data.poster.url, 300)})`,
|
||||
}}
|
||||
>
|
||||
{data.poster ? (
|
||||
<Image
|
||||
className={styles.image}
|
||||
src={modifyIMDbImg(data.poster.url)}
|
||||
alt={data.poster.caption}
|
||||
priority
|
||||
fill
|
||||
sizes='300px'
|
||||
/>
|
||||
) : (
|
||||
<svg className={styles.image__NA}>
|
||||
<use href='/svg/sprite.svg#icon-image-slash' />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<h1 className={`${styles.title} heading heading__primary`}>
|
||||
{data.title}
|
||||
</h1>
|
||||
<ul className={styles.meta} aria-label='quick facts'>
|
||||
{data.status.id !== 'released' && (
|
||||
<li className={styles.meta__text}>{data.status.text}</li>
|
||||
)}
|
||||
<li className={styles.meta__text}>{data.type.name}</li>
|
||||
{data.releaseYear && (
|
||||
<li className={styles.meta__text}>{releaseTime}</li>
|
||||
)}
|
||||
{data.ceritficate && (
|
||||
<li className={styles.meta__text}>{data.ceritficate}</li>
|
||||
)}
|
||||
{data.runtime && (
|
||||
<li className={styles.meta__text}>{formatTime(data.runtime)}</li>
|
||||
)}
|
||||
</ul>
|
||||
<div className={styles.ratings}>
|
||||
{data.ratings.avg && (
|
||||
<>
|
||||
<p className={styles.rating}>
|
||||
<span className={styles.rating__num}>{data.ratings.avg}</span>
|
||||
<svg className={styles.rating__icon}>
|
||||
<use href='/svg/sprite.svg#icon-rating'></use>
|
||||
</svg>
|
||||
<span className={styles.rating__text}> Avg. rating</span>
|
||||
</p>
|
||||
<p className={styles.rating}>
|
||||
<span className={styles.rating__num}>
|
||||
{formatNumber(data.ratings.numVotes)}
|
||||
</span>
|
||||
<svg className={styles.rating__icon}>
|
||||
<use href='/svg/sprite.svg#icon-like-dislike'></use>
|
||||
</svg>
|
||||
<span className={styles.rating__text}> No. of votes</span>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{data.ranking && (
|
||||
<p className={styles.rating}>
|
||||
<span className={styles.rating__num}>
|
||||
{formatNumber(data.ranking.position)}
|
||||
</span>
|
||||
<svg className={styles.rating__icon}>
|
||||
<use href='/svg/sprite.svg#icon-graph-rising'></use>
|
||||
</svg>
|
||||
<span className={styles.rating__text}>
|
||||
{' '}
|
||||
Popularity (
|
||||
<span className={styles.rating__sub}>
|
||||
{data.ranking.direction === 'UP'
|
||||
? `\u2191${formatNumber(data.ranking.change)}`
|
||||
: data.ranking.direction === 'DOWN'
|
||||
? `\u2193${formatNumber(data.ranking.change)}`
|
||||
: ''}
|
||||
</span>
|
||||
)
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!!data.genres.length && (
|
||||
<p className={styles.genres}>
|
||||
<span className={styles.genres__heading}>Genres: </span>
|
||||
{data.genres.map((genre, i) => (
|
||||
<Fragment key={genre.id}>
|
||||
{i > 0 && ', '}
|
||||
<Link href={`/search/title?genres=${genre.id}`}>
|
||||
<a className={styles.link}>{genre.text}</a>
|
||||
</Link>
|
||||
</Fragment>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
{
|
||||
<p className={styles.overview}>
|
||||
<span className={styles.overview__heading}>Plot: </span>
|
||||
<span className={styles.overview__text}>{data.plot || '-'}</span>
|
||||
</p>
|
||||
}
|
||||
{data.primaryCrew.map(crewType => (
|
||||
<p className={styles.crewType} key={crewType.type.id}>
|
||||
<span className={styles.crewType__heading}>
|
||||
{`${crewType.type.category}: `}
|
||||
</span>
|
||||
{crewType.crew.map((crew, i) => (
|
||||
<Fragment key={crew.id}>
|
||||
{i > 0 && ', '}
|
||||
<Link href={`/name/${crew.id}`}>
|
||||
<a className={styles.link}>{crew.name}</a>
|
||||
</Link>
|
||||
</Fragment>
|
||||
))}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Basic;
|
56
src/components/title/Cast.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import Image from 'next/future/image';
|
||||
import Link from 'next/link';
|
||||
import { Cast } from '../../interfaces/shared/title';
|
||||
import { modifyIMDbImg } from '../../utils/helpers';
|
||||
|
||||
import styles from '../../styles/modules/components/title/cast.module.scss';
|
||||
|
||||
type Props = {
|
||||
className: string;
|
||||
cast: Cast;
|
||||
};
|
||||
|
||||
const Cast = ({ className, cast }: Props) => {
|
||||
if (!cast.length) return <></>;
|
||||
|
||||
return (
|
||||
<section className={`${className} ${styles.container}`}>
|
||||
<h2 className='heading heading__secondary'>Cast</h2>
|
||||
<ul className={styles.cast}>
|
||||
{cast.map(member => (
|
||||
<li key={member.id} className={styles.member}>
|
||||
<div className={styles.member__imgContainer}>
|
||||
{member.image ? (
|
||||
<Image
|
||||
src={modifyIMDbImg(member.image, 400)}
|
||||
alt=''
|
||||
fill
|
||||
className={styles.member__img}
|
||||
sizes='200px'
|
||||
/>
|
||||
) : (
|
||||
<svg className={styles.member__imgNA}>
|
||||
<use href='/svg/sprite.svg#icon-image-slash' />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.member__textContainer}>
|
||||
<p>
|
||||
<Link href={`/name/${member.id}`}>
|
||||
<a className={styles.member__name}>{member.name}</a>
|
||||
</Link>
|
||||
</p>
|
||||
<p className={styles.member__role}>
|
||||
{member.characters?.join(', ')}
|
||||
{member.attributes && (
|
||||
<span> ({member.attributes.join(', ')})</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
export default Cast;
|
106
src/components/title/DidYouKnow.tsx
Normal file
|
@ -0,0 +1,106 @@
|
|||
import Link from 'next/link';
|
||||
import { Fragment } from 'react';
|
||||
import { DidYouKnow } from '../../interfaces/shared/title';
|
||||
import styles from '../../styles/modules/components/title/did-you-know.module.scss';
|
||||
|
||||
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;
|
333
src/components/title/Info.tsx
Normal file
|
@ -0,0 +1,333 @@
|
|||
import Link from 'next/link';
|
||||
import { NextRouter } from 'next/router';
|
||||
import { Info } from '../../interfaces/shared/title';
|
||||
import { formatMoney, formatTime } from '../../utils/helpers';
|
||||
|
||||
import styles from '../../styles/modules/components/title/info.module.scss';
|
||||
|
||||
type Props = {
|
||||
info: Info;
|
||||
className: string;
|
||||
router: NextRouter;
|
||||
};
|
||||
|
||||
const Info = ({ info, className, router }: Props) => {
|
||||
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.replaceAll(
|
||||
' ',
|
||||
'-'
|
||||
)}`}
|
||||
>
|
||||
<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;
|
90
src/components/title/Media.tsx
Normal file
|
@ -0,0 +1,90 @@
|
|||
import Image from 'next/future/image';
|
||||
import Link from 'next/link';
|
||||
import { NextRouter } from 'next/router';
|
||||
import { Media } from '../../interfaces/shared/title';
|
||||
import { modifyIMDbImg } from '../../utils/helpers';
|
||||
|
||||
import styles from '../../styles/modules/components/title/media.module.scss';
|
||||
|
||||
type Props = {
|
||||
className: string;
|
||||
media: Media;
|
||||
router: NextRouter;
|
||||
};
|
||||
|
||||
const Media = ({ className, media, router }: 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 key={router.asPath} className={styles.trailer}>
|
||||
<video
|
||||
aria-label='trailer video'
|
||||
// it's a relatively new tag. hence jsx-all1 complains
|
||||
aria-description={media.trailer.caption}
|
||||
controls
|
||||
playsInline
|
||||
poster={modifyIMDbImg(media.trailer.thumbnail)}
|
||||
className={styles.trailer__video}
|
||||
>
|
||||
{media.trailer.urls.map(source => (
|
||||
<source
|
||||
key={source.url}
|
||||
type={source.mimeType}
|
||||
src={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;
|
64
src/components/title/MoreLikeThis.tsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
import Image from 'next/future/image';
|
||||
import Link from 'next/link';
|
||||
import { MoreLikeThis } from '../../interfaces/shared/title';
|
||||
import { formatNumber, modifyIMDbImg } from '../../utils/helpers';
|
||||
import styles from '../../styles/modules/components/title/more-like-this.module.scss';
|
||||
|
||||
type Props = {
|
||||
className: string;
|
||||
data: MoreLikeThis;
|
||||
};
|
||||
|
||||
const MoreLikeThis = ({ className, data }: Props) => {
|
||||
if (!data.length) return <></>;
|
||||
|
||||
return (
|
||||
<section className={`${className} ${styles.morelikethis}`}>
|
||||
<h2 className='heading heading__secondary'>More like this</h2>
|
||||
<ul className={styles.container}>
|
||||
{data.map(title => (
|
||||
<li key={title.id}>
|
||||
<Link href={`/title/${title.id}`}>
|
||||
<a className={styles.item}>
|
||||
<div className={styles.item__imgContainer}>
|
||||
{title.poster ? (
|
||||
<Image
|
||||
src={modifyIMDbImg(title.poster.url, 400)}
|
||||
alt=''
|
||||
fill
|
||||
className={styles.item__img}
|
||||
sizes='200px'
|
||||
/>
|
||||
) : (
|
||||
<svg className={styles.item__imgNA}>
|
||||
<use href='/svg/sprite.svg#icon-image-slash' />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.item__textContainer}>
|
||||
<h3 className={`heading ${styles.item__heading}`}>
|
||||
{title.title}
|
||||
</h3>
|
||||
{title.ratings.avg && (
|
||||
<p className={styles.item__rating}>
|
||||
<span className={styles.item__ratingNum}>
|
||||
{title.ratings.avg}
|
||||
</span>
|
||||
<svg className={styles.item__ratingIcon}>
|
||||
<use href='/svg/sprite.svg#icon-rating'></use>
|
||||
</svg>
|
||||
<span>
|
||||
({formatNumber(title.ratings.numVotes)} votes)
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
export default MoreLikeThis;
|
82
src/components/title/Reviews.tsx
Normal file
|
@ -0,0 +1,82 @@
|
|||
import { NextRouter } from 'next/router';
|
||||
import Link from 'next/link';
|
||||
import { Reviews } from '../../interfaces/shared/title';
|
||||
import { formatNumber } from '../../utils/helpers';
|
||||
import styles from '../../styles/modules/components/title/reviews.module.scss';
|
||||
|
||||
type Props = {
|
||||
reviews: Reviews;
|
||||
router: NextRouter;
|
||||
};
|
||||
|
||||
const Reviews = ({ reviews, router }: Props) => {
|
||||
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;
|
55
src/context/theme-context.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
import React, { useState, createContext, ReactNode } from 'react';
|
||||
|
||||
const getInitialTheme = () => {
|
||||
// for server-side rendering, as window isn't availabe there
|
||||
if (typeof window === 'undefined') return 'light';
|
||||
|
||||
const userPrefersTheme = window.localStorage.getItem('theme') || null;
|
||||
const 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: string) => {},
|
||||
};
|
||||
|
||||
export const themeContext = createContext(initialContext);
|
||||
|
||||
const ThemeProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [curTheme, setCurTheme] = useState(getInitialTheme);
|
||||
|
||||
const setTheme = (theme: string) => {
|
||||
setCurTheme(theme);
|
||||
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;
|
832
src/interfaces/misc/rawTitle.ts
Normal file
|
@ -0,0 +1,832 @@
|
|||
export default interface RawTitle {
|
||||
props: {
|
||||
pageProps: {
|
||||
aboveTheFoldData: {
|
||||
id: string;
|
||||
productionStatus: {
|
||||
currentProductionStage: {
|
||||
id: string;
|
||||
text: string;
|
||||
};
|
||||
productionStatusHistory?: Array<{
|
||||
status: {
|
||||
id: string;
|
||||
text: string;
|
||||
};
|
||||
}>;
|
||||
restriction?: {
|
||||
restrictionReason: Array<string>;
|
||||
unrestrictedTotal: number;
|
||||
};
|
||||
};
|
||||
canHaveEpisodes: boolean;
|
||||
series?: {
|
||||
episodeNumber: {
|
||||
episodeNumber: number;
|
||||
seasonNumber: number;
|
||||
};
|
||||
nextEpisode: {
|
||||
id: string;
|
||||
};
|
||||
previousEpisode:
|
||||
| {
|
||||
id: string;
|
||||
}
|
||||
| undefined;
|
||||
series: {
|
||||
id: string;
|
||||
titleText: {
|
||||
text: string;
|
||||
};
|
||||
originalTitleText: {
|
||||
text: string;
|
||||
};
|
||||
titleType: {
|
||||
id: string;
|
||||
};
|
||||
releaseYear: {
|
||||
year: number;
|
||||
endYear: any;
|
||||
};
|
||||
};
|
||||
};
|
||||
titleText: {
|
||||
text: string;
|
||||
};
|
||||
titleType: {
|
||||
text: string;
|
||||
id: string;
|
||||
isSeries: boolean;
|
||||
isEpisode: boolean;
|
||||
};
|
||||
originalTitleText: {
|
||||
text: string;
|
||||
};
|
||||
certificate?: {
|
||||
rating: string;
|
||||
};
|
||||
releaseYear?: {
|
||||
year: number;
|
||||
endYear: any;
|
||||
};
|
||||
releaseDate?: {
|
||||
day: number;
|
||||
month: number;
|
||||
year: number;
|
||||
};
|
||||
runtime?: {
|
||||
seconds: number;
|
||||
};
|
||||
canRate: {
|
||||
isRatable: boolean;
|
||||
};
|
||||
ratingsSummary: {
|
||||
aggregateRating?: number;
|
||||
voteCount: number;
|
||||
};
|
||||
meterRanking?: {
|
||||
currentRank: number;
|
||||
rankChange: {
|
||||
changeDirection: string;
|
||||
difference: number;
|
||||
};
|
||||
};
|
||||
primaryImage?: {
|
||||
id: string;
|
||||
width: number;
|
||||
height: number;
|
||||
url: string;
|
||||
caption: {
|
||||
plainText: string;
|
||||
};
|
||||
};
|
||||
images: {
|
||||
total: number;
|
||||
};
|
||||
videos: {
|
||||
total: number;
|
||||
};
|
||||
primaryVideos: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
id: string;
|
||||
isMature: boolean;
|
||||
contentType: {
|
||||
id: string;
|
||||
displayName: {
|
||||
value: string;
|
||||
};
|
||||
};
|
||||
thumbnail: {
|
||||
url: string;
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
runtime: {
|
||||
value: number;
|
||||
};
|
||||
description: {
|
||||
value: string;
|
||||
language: string;
|
||||
};
|
||||
name: {
|
||||
value: string;
|
||||
language: string;
|
||||
};
|
||||
playbackURLs: Array<{
|
||||
displayName: {
|
||||
value: string;
|
||||
language: string;
|
||||
};
|
||||
mimeType: string;
|
||||
url: string;
|
||||
}>;
|
||||
previewURLs: Array<{
|
||||
displayName: {
|
||||
value: string;
|
||||
language: string;
|
||||
};
|
||||
mimeType: string;
|
||||
url: string;
|
||||
}>;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
externalLinks: {
|
||||
total: number;
|
||||
};
|
||||
metacritic?: {
|
||||
metascore: {
|
||||
score: number;
|
||||
};
|
||||
};
|
||||
keywords: {
|
||||
total: number;
|
||||
edges: Array<{
|
||||
node: {
|
||||
text: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
genres: {
|
||||
genres: Array<{
|
||||
text: string;
|
||||
id: string;
|
||||
}>;
|
||||
};
|
||||
plot?: {
|
||||
plotText?: {
|
||||
plainText: string;
|
||||
};
|
||||
language?: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
plotContributionLink: {
|
||||
url: string;
|
||||
};
|
||||
credits: {
|
||||
total: number;
|
||||
};
|
||||
principalCredits: Array<{
|
||||
totalCredits: number;
|
||||
category: {
|
||||
text: string;
|
||||
id: string;
|
||||
};
|
||||
credits: Array<{
|
||||
name: {
|
||||
nameText: {
|
||||
text: string;
|
||||
};
|
||||
id: string;
|
||||
};
|
||||
attributes?: Array<{
|
||||
text: string;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
reviews: {
|
||||
total: number;
|
||||
};
|
||||
criticReviewsTotal: {
|
||||
total: number;
|
||||
};
|
||||
triviaTotal: {
|
||||
total: number;
|
||||
};
|
||||
meta: {
|
||||
canonicalId: string;
|
||||
publicationStatus: string;
|
||||
};
|
||||
castPageTitle: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
name: {
|
||||
nameText: {
|
||||
text: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
creatorsPageTitle: Array<{
|
||||
credits: Array<{
|
||||
name: {
|
||||
nameText: {
|
||||
text: string;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
}>;
|
||||
directorsPageTitle: Array<{
|
||||
credits: Array<{
|
||||
name: {
|
||||
nameText: {
|
||||
text: string;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
}>;
|
||||
countriesOfOrigin?: {
|
||||
countries: Array<{
|
||||
id: string;
|
||||
}>;
|
||||
};
|
||||
production: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
company: {
|
||||
id: string;
|
||||
companyText: {
|
||||
text: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
featuredReviews: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
author: {
|
||||
nickName: string;
|
||||
};
|
||||
summary: {
|
||||
originalText: string;
|
||||
};
|
||||
text: {
|
||||
originalText: {
|
||||
plainText: string;
|
||||
};
|
||||
};
|
||||
authorRating: number;
|
||||
submissionDate: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
};
|
||||
mainColumnData: {
|
||||
id: string;
|
||||
wins: {
|
||||
total: number;
|
||||
};
|
||||
nominations: {
|
||||
total: number;
|
||||
};
|
||||
prestigiousAwardSummary?: {
|
||||
nominations: number;
|
||||
wins: number;
|
||||
award: {
|
||||
text: string;
|
||||
id: string;
|
||||
event: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
ratingsSummary: {
|
||||
topRanking?: {
|
||||
id: string;
|
||||
text: {
|
||||
value: string;
|
||||
};
|
||||
rank: number;
|
||||
};
|
||||
};
|
||||
episodes?: {
|
||||
episodes: {
|
||||
total: number;
|
||||
};
|
||||
seasons: Array<{
|
||||
number: number;
|
||||
}>;
|
||||
years: Array<{
|
||||
year: number;
|
||||
}>;
|
||||
totalEpisodes: {
|
||||
total: number;
|
||||
};
|
||||
topRated: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
ratingsSummary: {
|
||||
aggregateRating: number;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
};
|
||||
videos: {
|
||||
total: number;
|
||||
};
|
||||
videoStrip: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
id: string;
|
||||
contentType: {
|
||||
displayName: {
|
||||
value: string;
|
||||
};
|
||||
};
|
||||
name: {
|
||||
value: string;
|
||||
};
|
||||
runtime: {
|
||||
value: number;
|
||||
};
|
||||
thumbnail: {
|
||||
height: number;
|
||||
url: string;
|
||||
width: number;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
titleMainImages: {
|
||||
total: number;
|
||||
edges: Array<{
|
||||
node: {
|
||||
id: string;
|
||||
url: string;
|
||||
caption: {
|
||||
plainText: string;
|
||||
};
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
productionStatus: {
|
||||
currentProductionStage: {
|
||||
id: string;
|
||||
text: string;
|
||||
};
|
||||
productionStatusHistory?: Array<{
|
||||
status: {
|
||||
id: string;
|
||||
text: string;
|
||||
};
|
||||
}>;
|
||||
restriction?: {
|
||||
restrictionReason: Array<string>;
|
||||
};
|
||||
};
|
||||
primaryImage?: {
|
||||
id: string;
|
||||
};
|
||||
imageUploadLink?: {
|
||||
url: string;
|
||||
};
|
||||
titleType: {
|
||||
id: string;
|
||||
canHaveEpisodes: boolean;
|
||||
};
|
||||
cast: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
name: {
|
||||
id: string;
|
||||
nameText: {
|
||||
text: string;
|
||||
};
|
||||
primaryImage?: {
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
};
|
||||
attributes?: Array<{
|
||||
text: string;
|
||||
}>;
|
||||
characters?: Array<{
|
||||
name: string;
|
||||
}>;
|
||||
episodeCredits: {
|
||||
total: number;
|
||||
yearRange?: {
|
||||
year: number;
|
||||
endYear: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
creators: Array<{
|
||||
totalCredits: number;
|
||||
category: {
|
||||
text: string;
|
||||
};
|
||||
credits: Array<{
|
||||
name: {
|
||||
id: string;
|
||||
nameText: {
|
||||
text: string;
|
||||
};
|
||||
};
|
||||
attributes: any;
|
||||
}>;
|
||||
}>;
|
||||
directors: Array<{
|
||||
totalCredits: number;
|
||||
category: {
|
||||
text: string;
|
||||
};
|
||||
credits: Array<{
|
||||
name: {
|
||||
id: string;
|
||||
nameText: {
|
||||
text: string;
|
||||
};
|
||||
};
|
||||
attributes: any;
|
||||
}>;
|
||||
}>;
|
||||
writers: Array<{
|
||||
totalCredits: number;
|
||||
category: {
|
||||
text: string;
|
||||
};
|
||||
credits: Array<{
|
||||
name: {
|
||||
id: string;
|
||||
nameText: {
|
||||
text: string;
|
||||
};
|
||||
};
|
||||
attributes?: Array<{
|
||||
text: string;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
isAdult: boolean;
|
||||
moreLikeThisTitles: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
id: string;
|
||||
titleText: {
|
||||
text: string;
|
||||
};
|
||||
titleType: {
|
||||
id: string;
|
||||
text: string;
|
||||
};
|
||||
originalTitleText: {
|
||||
text: string;
|
||||
};
|
||||
primaryImage?: {
|
||||
id: string;
|
||||
width: number;
|
||||
height: number;
|
||||
url: string;
|
||||
};
|
||||
releaseYear?: {
|
||||
year: number;
|
||||
endYear?: number;
|
||||
};
|
||||
ratingsSummary: {
|
||||
aggregateRating?: number;
|
||||
voteCount: number;
|
||||
};
|
||||
runtime?: {
|
||||
seconds: number;
|
||||
};
|
||||
certificate?: {
|
||||
rating: string;
|
||||
};
|
||||
canRate: {
|
||||
isRatable: boolean;
|
||||
};
|
||||
titleCardGenres: {
|
||||
genres: Array<{
|
||||
text: string;
|
||||
}>;
|
||||
};
|
||||
canHaveEpisodes: boolean;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
triviaTotal: {
|
||||
total: number;
|
||||
};
|
||||
trivia: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
text: {
|
||||
plaidHtml: string;
|
||||
};
|
||||
trademark: any;
|
||||
relatedNames: any;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
goofsTotal: {
|
||||
total: number;
|
||||
};
|
||||
goofs: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
text: {
|
||||
plaidHtml: string;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
quotesTotal: {
|
||||
total: number;
|
||||
};
|
||||
quotes: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
lines: Array<{
|
||||
characters?: Array<{
|
||||
character: string;
|
||||
name?: {
|
||||
id: string;
|
||||
};
|
||||
}>;
|
||||
text: string;
|
||||
stageDirection?: string;
|
||||
}>;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
crazyCredits: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
text: {
|
||||
plaidHtml: string;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
alternateVersions: {
|
||||
total: number;
|
||||
edges: Array<{
|
||||
node: {
|
||||
text: {
|
||||
plaidHtml: string;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
connections: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
associatedTitle: {
|
||||
id: string;
|
||||
releaseYear: {
|
||||
year: number;
|
||||
};
|
||||
titleText: {
|
||||
text: string;
|
||||
};
|
||||
originalTitleText: {
|
||||
text: string;
|
||||
};
|
||||
series: any;
|
||||
};
|
||||
category: {
|
||||
text: string;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
soundtrack: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
text: string;
|
||||
comments?: Array<{
|
||||
plaidHtml: string;
|
||||
}>;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
titleText: {
|
||||
text: string;
|
||||
};
|
||||
originalTitleText: {
|
||||
text: string;
|
||||
};
|
||||
releaseYear?: {
|
||||
year: number;
|
||||
};
|
||||
reviews: {
|
||||
total: number;
|
||||
};
|
||||
featuredReviews: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
id: string;
|
||||
author: {
|
||||
nickName: string;
|
||||
userId: string;
|
||||
};
|
||||
summary: {
|
||||
originalText: string;
|
||||
};
|
||||
text: {
|
||||
originalText: {
|
||||
plaidHtml: string;
|
||||
};
|
||||
};
|
||||
authorRating: number;
|
||||
submissionDate: string;
|
||||
helpfulness: {
|
||||
upVotes: number;
|
||||
downVotes: number;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
canRate: {
|
||||
isRatable: boolean;
|
||||
};
|
||||
iframeAddReviewLink: {
|
||||
url: string;
|
||||
};
|
||||
faqsTotal: {
|
||||
total: number;
|
||||
};
|
||||
faqs: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
id: string;
|
||||
question: {
|
||||
plainText: string;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
releaseDate?: {
|
||||
day: number;
|
||||
month: number;
|
||||
year: number;
|
||||
country: {
|
||||
id: string;
|
||||
text: string;
|
||||
};
|
||||
};
|
||||
countriesOfOrigin?: {
|
||||
countries: Array<{
|
||||
id: string;
|
||||
text: string;
|
||||
}>;
|
||||
};
|
||||
detailsExternalLinks: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
url: string;
|
||||
label: string;
|
||||
externalLinkRegion?: {
|
||||
text: string;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
total: number;
|
||||
};
|
||||
spokenLanguages: {
|
||||
spokenLanguages: Array<{
|
||||
id: string;
|
||||
text: string;
|
||||
}>;
|
||||
};
|
||||
akas: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
text: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
filmingLocations: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
text: string;
|
||||
};
|
||||
}>;
|
||||
total: number;
|
||||
};
|
||||
production: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
company: {
|
||||
id: string;
|
||||
companyText: {
|
||||
text: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
companies: {
|
||||
total: number;
|
||||
};
|
||||
productionBudget?: {
|
||||
budget: {
|
||||
amount: number;
|
||||
currency: string;
|
||||
};
|
||||
};
|
||||
lifetimeGross?: {
|
||||
total: {
|
||||
amount: number;
|
||||
currency: string;
|
||||
};
|
||||
};
|
||||
openingWeekendGross?: {
|
||||
gross: {
|
||||
total: {
|
||||
amount: number;
|
||||
currency: string;
|
||||
};
|
||||
};
|
||||
weekendEndDate: string;
|
||||
};
|
||||
worldwideGross?: {
|
||||
total: {
|
||||
amount: number;
|
||||
currency: string;
|
||||
};
|
||||
};
|
||||
technicalSpecifications: {
|
||||
soundMixes: {
|
||||
items: Array<{
|
||||
id: string;
|
||||
text: string;
|
||||
attributes: Array<any>;
|
||||
}>;
|
||||
};
|
||||
aspectRatios: {
|
||||
items: Array<{
|
||||
aspectRatio: string;
|
||||
attributes: Array<any>;
|
||||
}>;
|
||||
};
|
||||
colorations: {
|
||||
items: Array<{
|
||||
conceptId: string;
|
||||
text: string;
|
||||
attributes: Array<any>;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
runtime?: {
|
||||
seconds: number;
|
||||
};
|
||||
series?: {
|
||||
series: {
|
||||
runtime?: {
|
||||
seconds: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
canHaveEpisodes: boolean;
|
||||
contributionQuestions: {
|
||||
contributionLink: {
|
||||
url: string;
|
||||
};
|
||||
edges: Array<{
|
||||
node: {
|
||||
entity: {
|
||||
primaryImage?: {
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
caption: {
|
||||
plainText: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
questionId: string;
|
||||
questionText: {
|
||||
plainText: string;
|
||||
};
|
||||
contributionLink: {
|
||||
url: string;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
5
src/interfaces/shared/error.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export type AppError = {
|
||||
message: string;
|
||||
statusCode: number;
|
||||
stack?: any;
|
||||
};
|
25
src/interfaces/shared/title.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import cleanTitle from '../../utils/cleaners/title';
|
||||
import title from '../../utils/fetchers/title';
|
||||
|
||||
export type AxiosTitleRes = Awaited<ReturnType<typeof title>>;
|
||||
|
||||
// for full title
|
||||
type Title = ReturnType<typeof cleanTitle>;
|
||||
export type { Title as default };
|
||||
|
||||
export type Basic = Title['basic'];
|
||||
|
||||
export type Media = Title['media'];
|
||||
|
||||
export type Cast = Title['cast'];
|
||||
|
||||
export type DidYouKnow = Title['didYouKnow'];
|
||||
|
||||
export type Info = Pick<
|
||||
Title,
|
||||
'meta' | 'accolades' | 'keywords' | 'details' | 'boxOffice' | 'technicalSpecs'
|
||||
>;
|
||||
|
||||
export type Reviews = Title['reviews'];
|
||||
|
||||
export type MoreLikeThis = Title['moreLikeThis'];
|
52
src/layouts/Footer.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { FC } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import styles from '../styles/modules/layout/footer.module.scss';
|
||||
|
||||
const Footer: FC = () => {
|
||||
const { pathname } = useRouter();
|
||||
const className = (link: string) =>
|
||||
pathname === link ? styles.nav__linkActive : styles.nav__link;
|
||||
|
||||
return (
|
||||
<footer id='footer' className={styles.footer}>
|
||||
<nav aria-label='primary navigation' className={styles.nav}>
|
||||
<ul className={styles.list}>
|
||||
<li className={styles.nav__item}>
|
||||
<Link href='/about'>
|
||||
<a className={className('/about')}>About</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.nav__item}>
|
||||
<Link href='/privacy'>
|
||||
<a className={className('/privacy')}>Privacy</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.nav__item}>
|
||||
<Link href='/contact'>
|
||||
<a className={className('/contact')}>Contact</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.nav__item}>
|
||||
<a href='#' className={styles.nav__link}>
|
||||
Back to top
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<p className={styles.licence}>
|
||||
Licensed under
|
||||
<a
|
||||
className={styles.nav__link}
|
||||
href='https://www.gnu.org/licenses/agpl-3.0-standalone.html'
|
||||
>
|
||||
GNU AGPLv3
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
86
src/layouts/Header.tsx
Normal file
|
@ -0,0 +1,86 @@
|
|||
import { ReactNode } from 'react';
|
||||
// import dynamic from 'next/dynamic';
|
||||
import Link from 'next/link';
|
||||
import styles from '../styles/modules/layout/header.module.scss';
|
||||
import ThemeToggler from '../components/buttons/ThemeToggler';
|
||||
|
||||
// const ThemeToggler = dynamic(
|
||||
// () => import('../components/buttons/ThemeToggler'),
|
||||
// { ssr: false }
|
||||
// );
|
||||
|
||||
type Props = { full?: boolean; children?: ReactNode };
|
||||
|
||||
const Header = (props: Props) => {
|
||||
return (
|
||||
<header
|
||||
id='header'
|
||||
className={`${styles.header} ${props.full ? styles.header__about : ''}`}
|
||||
>
|
||||
<div className={styles.topbar}>
|
||||
<Link href='/about'>
|
||||
<a aria-label='go to homepage' className={styles.logo}>
|
||||
<svg
|
||||
className={styles.logo__icon}
|
||||
focusable='false'
|
||||
role='img'
|
||||
aria-hidden='true'
|
||||
>
|
||||
<use href='/svg/sprite.svg#icon-logo'></use>
|
||||
</svg>
|
||||
<span className={styles.logo__text}>libremdb</span>
|
||||
</a>
|
||||
</Link>
|
||||
{props.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>
|
||||
)}
|
||||
<ThemeToggler className={styles.themeToggler} />
|
||||
</div>
|
||||
{props.full && (
|
||||
<div className={styles.hero}>
|
||||
<h1 className={`heading heading__primary ${styles.hero__text}`}>
|
||||
A free & open source IMDb front-end
|
||||
</h1>
|
||||
<p className={styles.hero__more}>
|
||||
inspired by projects like
|
||||
<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;
|
23
src/layouts/Layout.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
import Footer from './Footer';
|
||||
import Header from './Header';
|
||||
|
||||
type Props = {
|
||||
full?: boolean;
|
||||
children: React.ReactNode;
|
||||
className: string;
|
||||
};
|
||||
|
||||
const Layout = ({ full, children, className }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<Header full={full} />
|
||||
<main id='main' className={`main ${className}`}>
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
7
src/pages/404.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
import ErrorInfo from '../components/Error/ErrorInfo';
|
||||
|
||||
const Error404 = () => {
|
||||
return <ErrorInfo />;
|
||||
};
|
||||
|
||||
export default Error404;
|
6
src/pages/500.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import ErrorInfo from '../components/Error/ErrorInfo';
|
||||
|
||||
const Error500 = () => {
|
||||
return <ErrorInfo message='server messed up, sorry.' statusCode={500} />;
|
||||
};
|
||||
export default Error500;
|
40
src/pages/_app.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { AppProps } from 'next/app';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import ProgressBar from '../components/loaders/ProgressBar';
|
||||
import ThemeProvider from '../context/theme-context';
|
||||
|
||||
import '../styles/main.scss';
|
||||
|
||||
const ModifiedApp = ({ Component, pageProps }: AppProps) => {
|
||||
// for showing progress bar
|
||||
// could've used nprogress package, but didn't feel like it
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleStart = useCallback(() => setIsLoading(true), []);
|
||||
const handleEnd = useCallback(() => setIsLoading(false), []);
|
||||
|
||||
useEffect(() => {
|
||||
router.events.on('routeChangeStart', handleStart);
|
||||
router.events.on('routeChangeComplete', handleEnd);
|
||||
router.events.on('routeChangeError', handleEnd);
|
||||
|
||||
return () => {
|
||||
router.events.off('routeChangeStart', handleStart);
|
||||
router.events.off('routeChangeComplete', handleEnd);
|
||||
router.events.off('routeChangeError', handleEnd);
|
||||
};
|
||||
}, [router, handleStart, handleEnd]);
|
||||
//
|
||||
|
||||
return (
|
||||
<ThemeProvider>
|
||||
{isLoading && <ProgressBar />}
|
||||
<Component {...pageProps} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModifiedApp;
|
38
src/pages/_document.tsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
import Document, { Html, Head, Main, NextScript } from 'next/document';
|
||||
|
||||
// for preventing Flash of inAccurate coloR Theme(fart)
|
||||
// chris coyier came up with that acronym(https://css-tricks.com/flash-of-inaccurate-color-theme-fart/)
|
||||
const setInitialTheme = `
|
||||
document.documentElement.dataset.js = true;
|
||||
document.documentElement.dataset.theme = (() => {
|
||||
const userPrefersTheme = window.localStorage.getItem('theme') || null;
|
||||
const browserPrefersDarkTheme = window.matchMedia(
|
||||
'(prefers-color-scheme: dark)'
|
||||
).matches;
|
||||
if (userPrefersTheme) return userPrefersTheme;
|
||||
else if (browserPrefersDarkTheme) return 'dark';
|
||||
else return 'light';
|
||||
})();
|
||||
`;
|
||||
|
||||
const ModifiedDocument = class extends Document {
|
||||
static async getInitialProps(ctx: any) {
|
||||
const initialProps = await Document.getInitialProps(ctx);
|
||||
return { ...initialProps };
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Html lang='en'>
|
||||
<Head />
|
||||
<body>
|
||||
<script dangerouslySetInnerHTML={{ __html: setInitialTheme }} />
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ModifiedDocument;
|
176
src/pages/about/index.tsx
Normal file
|
@ -0,0 +1,176 @@
|
|||
/* eslint-disable react/no-unescaped-entities */
|
||||
import Link from 'next/link';
|
||||
import Meta from '../../components/Meta/Meta';
|
||||
import Layout from '../../layouts/Layout';
|
||||
|
||||
import styles from '../../styles/modules/pages/about/about.module.scss';
|
||||
|
||||
const About = () => {
|
||||
return (
|
||||
<>
|
||||
<Meta
|
||||
title='About'
|
||||
description='libremdb is a free & open source IMDb front-end. It allows you to see information about movies, tv shows, video games without any ads or tracking.'
|
||||
/>
|
||||
<Layout full className={styles.about}>
|
||||
<section id='features' className={styles.features}>
|
||||
<h2
|
||||
className={`heading heading__secondary ${styles.features__heading}`}
|
||||
>
|
||||
Some features
|
||||
</h2>
|
||||
<ul className={styles.features__list}>
|
||||
<li className={styles.feature}>
|
||||
<svg
|
||||
aria-hidden='true'
|
||||
focusable='false'
|
||||
role='img'
|
||||
className={styles.feature__icon}
|
||||
>
|
||||
<use href='/svg/sprite.svg#icon-eye-slash'></use>
|
||||
</svg>
|
||||
<h3
|
||||
className={`heading heading__tertiary ${styles.feature__heading}`}
|
||||
>
|
||||
No ads or tracking
|
||||
</h3>
|
||||
<p className={styles.feature__text}>
|
||||
Browse any movie info without being tracked or bombarded by
|
||||
annoying ads.
|
||||
</p>
|
||||
</li>
|
||||
<li className={styles.feature}>
|
||||
<svg
|
||||
aria-hidden='true'
|
||||
focusable='false'
|
||||
role='img'
|
||||
className={styles.feature__icon}
|
||||
>
|
||||
<use href='/svg/sprite.svg#icon-palette'></use>
|
||||
</svg>
|
||||
<h3
|
||||
className={`heading heading__tertiary ${styles.feature__heading}`}
|
||||
>
|
||||
Modern interface
|
||||
</h3>
|
||||
<p className={styles.feature__text}>
|
||||
Modern interface with curated colors supporting both dark and
|
||||
light themes.
|
||||
</p>
|
||||
</li>
|
||||
<li className={styles.feature}>
|
||||
<svg
|
||||
aria-hidden='true'
|
||||
focusable='false'
|
||||
role='img'
|
||||
className={styles.feature__icon}
|
||||
>
|
||||
<use href='/svg/sprite.svg#icon-responsive'></use>
|
||||
</svg>
|
||||
<h3
|
||||
className={`heading heading__tertiary ${styles.feature__heading}`}
|
||||
>
|
||||
Responsive design
|
||||
</h3>
|
||||
<p className={styles.feature__text}>
|
||||
Be it your small mobile or big computer screen, it's fully
|
||||
responsive.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section id='faq' className={styles.faqs}>
|
||||
<h2 className={`heading heading__secondary ${styles.faqs__heading}`}>
|
||||
Questions you may have
|
||||
</h2>
|
||||
<div className={styles.faqs__list}>
|
||||
<details className={styles.faq}>
|
||||
<summary className={styles.faq__summary}>Why is it slow?</summary>
|
||||
<p className={styles.faq__description}>
|
||||
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.
|
||||
</p>
|
||||
</details>
|
||||
<details className={styles.faq}>
|
||||
<summary className={styles.faq__summary}>
|
||||
It doesn't have all routes.
|
||||
</summary>
|
||||
<p className={styles.faq__description}>
|
||||
I'll implement more with time :)
|
||||
</p>
|
||||
</details>
|
||||
<details className={styles.faq}>
|
||||
<summary className={styles.faq__summary}>
|
||||
I see connection being made to some Amazon domains.
|
||||
</summary>
|
||||
<p className={styles.faq__description}>
|
||||
For now, images and videos are directly served from Amazon. If I
|
||||
have enough time in the future, I'll implement a way to serve
|
||||
the images from libremdb instead.
|
||||
</p>
|
||||
</details>
|
||||
<details className={styles.faq}>
|
||||
<summary className={styles.faq__summary}>
|
||||
Will Amazon track me then?
|
||||
</summary>
|
||||
<p className={styles.faq__description}>
|
||||
They may log your IP address, useragent, and other such
|
||||
identifiers. I'd recommend using a VPN, or accessing the website
|
||||
through TOR for mitigating this risk.
|
||||
</p>
|
||||
</details>
|
||||
<details className={styles.faq}>
|
||||
<summary className={styles.faq__summary}>
|
||||
Why not just use IMDb?
|
||||
</summary>
|
||||
<p className={styles.faq__description}>
|
||||
Refer to the{' '}
|
||||
<a className='link' href='#features'>
|
||||
features section
|
||||
</a>{' '}
|
||||
above.
|
||||
</p>
|
||||
</details>
|
||||
<details className={styles.faq}>
|
||||
<summary className={styles.faq__summary}>
|
||||
Why didn't you use other databases like TMDB or OMDb?
|
||||
</summary>
|
||||
<p className={styles.faq__description}>
|
||||
IMDb simply has superior dataset compared to all other
|
||||
alternatives. With that being said, I'd encourage you to check
|
||||
out those alternatives too.
|
||||
</p>
|
||||
</details>
|
||||
<details className={styles.faq}>
|
||||
<summary className={styles.faq__summary}>
|
||||
Your website name is quite, ehm, lame.
|
||||
</summary>
|
||||
<p className={styles.faq__description}>
|
||||
Let's just say I'm not very good at naming things.
|
||||
</p>
|
||||
</details>
|
||||
<details className={styles.faq}>
|
||||
<summary className={styles.faq__summary}>
|
||||
I have some ideas/features/suggestions.
|
||||
</summary>
|
||||
<p className={styles.faq__description}>
|
||||
That's great! I've a couple of{' '}
|
||||
<Link href='/contact'>
|
||||
<a className='link'>contact methods</a>
|
||||
</Link>
|
||||
. Send your beautiful suggestions(or complaints), or just drop a
|
||||
hi.
|
||||
</p>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
49
src/pages/contact/index.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import Meta from '../../components/Meta/Meta';
|
||||
import Layout from '../../layouts/Layout';
|
||||
|
||||
import styles from '../../styles/modules/pages/contact/contact.module.scss';
|
||||
|
||||
const Contact = () => {
|
||||
return (
|
||||
<>
|
||||
<Meta
|
||||
title='Contact'
|
||||
description='Contact page of libremdb, a free & open source IMDb front-end.'
|
||||
/>
|
||||
<Layout className=''>
|
||||
<section className={styles.contact}>
|
||||
<h1 className={`heading heading__primary ${styles.contact__heading}`}>
|
||||
Contact
|
||||
</h1>
|
||||
|
||||
<div className={styles.list}>
|
||||
<p className={styles.item}>
|
||||
You can use{' '}
|
||||
<a href='https://github.com/zyachel/libremdb' className='link'>
|
||||
GitHub
|
||||
</a>{' '}
|
||||
or{' '}
|
||||
<a href='https://codeberg.org/zyachel/libremdb' className='link'>
|
||||
Codeberg
|
||||
</a>{' '}
|
||||
for general issues, questions, or requests.
|
||||
</p>
|
||||
<p className={styles.item}>
|
||||
In case you wish to contact me personally, I'm reachable via{' '}
|
||||
<a className='link' href='https://matrix.to/#/@ninal:matrix.org'>
|
||||
[matrix]
|
||||
</a>{' '}
|
||||
and{' '}
|
||||
<a className='link' href='mailto:aricla@protonmail.com'>
|
||||
email
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Contact;
|
74
src/pages/privacy/index.tsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
import Meta from '../../components/Meta/Meta';
|
||||
import Layout from '../../layouts/Layout';
|
||||
|
||||
import styles from '../../styles/modules/pages/privacy/privacy.module.scss';
|
||||
|
||||
const Privacy = () => {
|
||||
return (
|
||||
<>
|
||||
<Meta
|
||||
title='Privacy'
|
||||
description='Privacy policy of libremdb, a free & open source IMDb front-end.'
|
||||
/>
|
||||
<Layout className={styles.privacy}>
|
||||
<section className={styles.policy}>
|
||||
<h1 className={`heading heading__primary ${styles.policy__heading}`}>
|
||||
Privacy Policy
|
||||
</h1>
|
||||
<div className={styles.list}>
|
||||
<div className={styles.item}>
|
||||
<h2
|
||||
className={`heading heading__secondary ${styles.item__heading}`}
|
||||
>
|
||||
Information collected
|
||||
</h2>
|
||||
<p className={styles.item__text}>No information is collected.</p>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<h2
|
||||
className={`heading heading__secondary ${styles.item__heading}`}
|
||||
>
|
||||
Information stored in your browser
|
||||
</h2>
|
||||
<p className={styles.item__text}>
|
||||
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.
|
||||
</p>
|
||||
<p className={styles.item__text}>
|
||||
To permamently disable libremdb from storing your theme
|
||||
prefrences, either turn off JavaScript or disable access to
|
||||
Local Storage for libremdb.
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<h2
|
||||
className={`heading heading__secondary ${styles.item__heading}`}
|
||||
>
|
||||
Information collected by other services
|
||||
</h2>
|
||||
<p className={styles.item__text}>
|
||||
libremdb connects to 'media-amazon.com' and 'media-imdb.com' for
|
||||
fetching images and videos. So, Amazon might log your IP
|
||||
address, and other information(such as http headers) sent by
|
||||
your browser.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer className={styles.metadata}>
|
||||
<p>
|
||||
Last updated on <time>10 september, 2022.</time>
|
||||
</p>
|
||||
<p>
|
||||
You can see the full revision history of this privacy policy on
|
||||
GitHub, or Codeberg.
|
||||
</p>
|
||||
</footer>
|
||||
</section>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Privacy;
|
108
src/pages/title/[titleId]/index.tsx
Normal file
|
@ -0,0 +1,108 @@
|
|||
// external
|
||||
import { GetServerSideProps, GetStaticProps, GetStaticPaths } from 'next';
|
||||
import { useRouter } from 'next/router';
|
||||
// local
|
||||
import Meta from '../../../components/Meta/Meta';
|
||||
import Layout from '../../../layouts/Layout';
|
||||
import title from '../../../utils/fetchers/title';
|
||||
// components
|
||||
import ErrorInfo from '../../../components/Error/ErrorInfo';
|
||||
import Basic from '../../../components/title/Basic';
|
||||
import Media from '../../../components/title/Media';
|
||||
import Cast from '../../../components/title/Cast';
|
||||
import DidYouKnow from '../../../components/title/DidYouKnow';
|
||||
import Info from '../../../components/title/Info';
|
||||
import Reviews from '../../../components/title/Reviews';
|
||||
import MoreLikeThis from '../../../components/title/MoreLikeThis';
|
||||
// misc
|
||||
import Title from '../../../interfaces/shared/title';
|
||||
import { AppError } from '../../../interfaces/shared/error';
|
||||
// styles
|
||||
import styles from '../../../styles/modules/pages/title/title.module.scss';
|
||||
|
||||
type Props = { data: Title; error: null } | { error: AppError; data: null };
|
||||
|
||||
// TO-DO: make a wrapper page component to display errors, if present in props
|
||||
const TitleInfo = ({ data, error }: Props) => {
|
||||
const router = useRouter();
|
||||
|
||||
if (error)
|
||||
return <ErrorInfo message={error.message} statusCode={error.statusCode} />;
|
||||
|
||||
const info = {
|
||||
meta: data.meta,
|
||||
keywords: data.keywords,
|
||||
details: data.details,
|
||||
boxOffice: data.boxOffice,
|
||||
technicalSpecs: data.technicalSpecs,
|
||||
accolades: data.accolades,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta
|
||||
title={`${data.basic.title} (${
|
||||
data.basic.releaseYear?.start || data.basic.type.name
|
||||
})`}
|
||||
description={data.basic.plot || undefined}
|
||||
/>
|
||||
|
||||
<Layout className={styles.title}>
|
||||
<Basic data={data.basic} className={styles.basic} />
|
||||
<Media className={styles.media} media={data.media} router={router} />
|
||||
<Cast className={styles.cast} cast={data.cast} />
|
||||
<div className={styles.textarea}>
|
||||
<DidYouKnow data={data.didYouKnow} />
|
||||
<Reviews reviews={data.reviews} router={router} />
|
||||
</div>
|
||||
<Info className={styles.infoarea} info={info} router={router} />
|
||||
<MoreLikeThis className={styles.related} data={data.moreLikeThis} />
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// TO-DO: make a getServerSideProps wrapper for handling errors
|
||||
export const getServerSideProps: GetServerSideProps = async ctx => {
|
||||
const titleId = ctx.params!.titleId as string;
|
||||
|
||||
try {
|
||||
const data = await title(titleId);
|
||||
|
||||
return { props: { data, error: null } };
|
||||
} catch (error: any) {
|
||||
const { message, statusCode } = error;
|
||||
ctx.res.statusCode = statusCode;
|
||||
ctx.res.statusMessage = message;
|
||||
|
||||
return { props: { error: { message, statusCode }, data: null } };
|
||||
}
|
||||
};
|
||||
|
||||
export default TitleInfo;
|
||||
|
||||
// could've used getStaticProps instead of getServerSideProps, but meh.
|
||||
/*
|
||||
export const getStaticProps: GetStaticProps = async ctx => {
|
||||
const titleId = ctx.params!.titleId as string;
|
||||
try {
|
||||
const data = await title(titleId);
|
||||
|
||||
return {
|
||||
props: { data, error: null },
|
||||
revalidate: 60 * 60 * 24, // 1 day
|
||||
};
|
||||
} catch (error) {
|
||||
// console.log(error);
|
||||
|
||||
return { notFound: true };
|
||||
}
|
||||
};
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = () => {
|
||||
return {
|
||||
paths: [{ params: { titleId: 'tt0133093' } }],
|
||||
fallback: 'blocking',
|
||||
};
|
||||
};
|
||||
*/
|
1
src/styles/abstracts/_index.scss
Normal file
|
@ -0,0 +1 @@
|
|||
@forward './mixins';
|
80
src/styles/abstracts/_mixins.scss
Normal file
|
@ -0,0 +1,80 @@
|
|||
@use 'sass:map';
|
||||
@use './variables' as v;
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
// ROOT MIXINS
|
||||
////////////////////////////////////////////////////////////////
|
||||
// MIXINS TO TURN SCSS VARIABLES INTO CSS VARIABLES:
|
||||
@mixin typescale($mode) {
|
||||
// getting appropriate type scale
|
||||
$type-scale: map.get(v.$font-sizes, $mode);
|
||||
// making variables out of it
|
||||
@each $key, $value in $type-scale {
|
||||
--fs-#{$key}: #{$value};
|
||||
}
|
||||
}
|
||||
|
||||
@mixin typography {
|
||||
$other-vars: v.$misc;
|
||||
@each $key, $value in $other-vars {
|
||||
--#{$key}: #{$value};
|
||||
}
|
||||
}
|
||||
|
||||
@mixin spacers {
|
||||
@each $var, $value in v.$space {
|
||||
--spacer-#{$var}: #{$value};
|
||||
}
|
||||
}
|
||||
|
||||
@mixin colors($color: 'light') {
|
||||
$color-map: map-get(v.$themes, $color);
|
||||
|
||||
@each $prop, $val in $color-map {
|
||||
--clr-#{$prop}: #{$val};
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
// REUSABLE MIXINS
|
||||
////////////////////////////////////////////////////////////////
|
||||
// 1. mixin to handle known and unknown breakpoints:
|
||||
@mixin bp($given-breakpoint, $min: false) {
|
||||
// just assigning the given value to a new variable since we're going to change it conditionally;
|
||||
$breakpoint: $given-breakpoint;
|
||||
// if $breakpoints map contains the given variable then getting it's value
|
||||
@if map.has-key(v.$breakpoints, $breakpoint) {
|
||||
$breakpoint: map.get(v.$breakpoints, $breakpoint);
|
||||
}
|
||||
// and then using it for media query. This will also work for straight out values(50em or 800px, for example)
|
||||
$expr: 'max-width: #{$breakpoint}';
|
||||
@if ($min) {
|
||||
$expr: 'min-width: #{$breakpoint}';
|
||||
}
|
||||
|
||||
@media screen and ($expr) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. for prettifying links
|
||||
@mixin prettify-link($clr: currentColor, $clr-line: $clr, $animate: true) {
|
||||
$height: 0.1em;
|
||||
|
||||
text-decoration: none;
|
||||
color: $clr;
|
||||
background: linear-gradient(to left, $clr-line, $clr-line) no-repeat right
|
||||
bottom;
|
||||
|
||||
@if ($animate) {
|
||||
background-size: 0 $height;
|
||||
transition: background-size 200ms ease;
|
||||
|
||||
&:where(:hover, :focus-visible) {
|
||||
background-size: 100% $height;
|
||||
background-position-x: left;
|
||||
}
|
||||
} @else {
|
||||
background-size: 100% $height;
|
||||
}
|
||||
}
|
3
src/styles/abstracts/variables/_index.scss
Normal file
|
@ -0,0 +1,3 @@
|
|||
@forward './misc';
|
||||
@forward './typography';
|
||||
@forward './themes';
|
28
src/styles/abstracts/variables/_misc.scss
Normal file
|
@ -0,0 +1,28 @@
|
|||
// 8 pt spacer
|
||||
$space: (
|
||||
0: 0.4rem,
|
||||
1: 0.8rem,
|
||||
2: 1.6rem,
|
||||
3: 2.4rem,
|
||||
4: 3.2rem,
|
||||
5: 4rem,
|
||||
6: 4.8rem,
|
||||
7: 5.6rem,
|
||||
8: 6.4rem,
|
||||
9: 7.2rem,
|
||||
10: 8rem,
|
||||
);
|
||||
|
||||
$breakpoints: (
|
||||
'bp-1200': 75em,
|
||||
'bp-900': 56.25em,
|
||||
'bp-700': 43.75em,
|
||||
'bp-450': 28.125em,
|
||||
);
|
||||
|
||||
// 1. colors
|
||||
$clr-primary: hsl(240, 31%, 25%);
|
||||
$clr-secondary: hsl(344, 79%, 40%);
|
||||
$clr-tertiary: hsl(176, 43%, 46%);
|
||||
$clr-quatenary: hsl(204, 4%, 23%);
|
||||
$clr-quintenary: hsl(0, 0%, 100%);
|
62
src/styles/abstracts/variables/_themes.scss
Normal file
|
@ -0,0 +1,62 @@
|
|||
$_light: (
|
||||
// 1. text
|
||||
// 1.1 for headings
|
||||
text-accent: hsl(240, 31%, 25%),
|
||||
// 1.2 for base text
|
||||
text: hsl(0, 0%, 24%),
|
||||
// 1.3 for subtle text like metadata
|
||||
text-muted: hsl(204, 4%, 35%),
|
||||
// 2. bg
|
||||
// 2.1 for cards, headers, footers,
|
||||
bg-accent: hsl(339, 100%, 97%),
|
||||
// 2.2 for base bg
|
||||
bg: hsl(0, 0%, 100%),
|
||||
// 2.3 for hover state of cards
|
||||
bg-muted: rgb(255, 229, 239),
|
||||
// 3. links
|
||||
// 3.1 the default one.
|
||||
link: hsl(219, 100%, 20%),
|
||||
link-muted: hsl(344, 79%, 40%),
|
||||
// 4. for icons, borders
|
||||
fill: hsl(339, 100%, 36%),
|
||||
// 4.2 for borders, primarily
|
||||
fill-muted: hsl(0, 0%, 80%),
|
||||
// shadows on cards
|
||||
shadow: 0 0 1rem hsla(0, 0%, 0%, 0.2),
|
||||
// keyboard, focus hightlight
|
||||
highlight: hsl(176, 43%, 46%),
|
||||
// for gradient behind hero text on about page.
|
||||
gradient:
|
||||
(
|
||||
radial-gradient(
|
||||
at 23% 32%,
|
||||
hsla(344, 79%, 40%, 0.15) 0px,
|
||||
transparent 70%
|
||||
),
|
||||
radial-gradient(at 72% 55%, hsla(344, 79%, 40%, 0.2) 0px, transparent 50%)
|
||||
)
|
||||
);
|
||||
|
||||
$_dark: (
|
||||
text-accent: hsl(0, 0%, 100%),
|
||||
text: hsl(0, 0%, 96%),
|
||||
text-muted: hsl(0, 0%, 80%),
|
||||
bg-accent: hsl(221, 39%, 15%),
|
||||
bg: hsl(221, 39%, 11%),
|
||||
bg-muted: rgb(20, 28, 46),
|
||||
link: hsl(339, 95%, 80%),
|
||||
link-muted: hsl(344, 79%, 80%),
|
||||
fill: hsl(339, 75%, 64%),
|
||||
fill-muted: hsl(0, 0, 35%),
|
||||
shadow: hsla(0, 0%, 0%, 1),
|
||||
highlight: hsl(176, 43%, 46%),
|
||||
gradient: (
|
||||
radial-gradient(at 23% 32%, hsla(344, 79%, 40%, 0.04) 0px, transparent 70%),
|
||||
radial-gradient(at 72% 55%, hsla(344, 79%, 40%, 0.05) 0px, transparent 50%),
|
||||
),
|
||||
);
|
||||
|
||||
$themes: (
|
||||
light: $_light,
|
||||
dark: $_dark,
|
||||
);
|
56
src/styles/abstracts/variables/_typography.scss
Normal file
|
@ -0,0 +1,56 @@
|
|||
////////////////////////////////////////////////////////////////
|
||||
// RAW VARIABLES
|
||||
////////////////////////////////////////////////////////////////
|
||||
|
||||
// 1. type scale
|
||||
// see more at https://type-scale.com/
|
||||
// 1.33
|
||||
$_perfect-fourth: (
|
||||
0: 6rem,
|
||||
1: 5rem,
|
||||
2: 3.8rem,
|
||||
3: 2.8rem,
|
||||
4: 2.1rem,
|
||||
5: 1.6rem,
|
||||
// 6: 1.2rem,
|
||||
);
|
||||
// 1.25
|
||||
$_major-third: (
|
||||
0: 4.9rem,
|
||||
1: 3.9rem,
|
||||
2: 3.1rem,
|
||||
3: 2.5rem,
|
||||
4: 2rem,
|
||||
5: 1.6rem,
|
||||
// 6: 1.3rem,
|
||||
);
|
||||
|
||||
// 2 font families
|
||||
$_ff-sans: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui,
|
||||
helvetica neue, Cantarell, Ubuntu, roboto, noto, arial, sans-serif;
|
||||
$_ff-serif: Iowan Old Style, Apple Garamond, Baskerville, Times New Roman,
|
||||
Droid Serif, Times, Source Serif Pro, serif, Apple Color Emoji, Segoe UI Emoji,
|
||||
Segoe UI Symbol;
|
||||
$_ff-mono: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
// TO EXPORT
|
||||
////////////////////////////////////////////////////////////////
|
||||
|
||||
$font-sizes: (
|
||||
desktop: $_perfect-fourth,
|
||||
mobile: $_major-third,
|
||||
);
|
||||
|
||||
$misc: (
|
||||
ff-base: $_ff-sans,
|
||||
ff-accent: (
|
||||
'RedHat Display',
|
||||
$_ff-sans,
|
||||
),
|
||||
fw-thin: 300,
|
||||
fw-base: 400,
|
||||
fw-medium: 500,
|
||||
fw-bold: 700,
|
||||
fw-black: 900,
|
||||
);
|
24
src/styles/base/_base.scss
Normal file
|
@ -0,0 +1,24 @@
|
|||
@use '../abstracts' as helper;
|
||||
|
||||
#__next,
|
||||
html,
|
||||
body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#__next {
|
||||
display: grid;
|
||||
grid-template-rows: min-content 1fr min-content;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--clr-text);
|
||||
background-color: var(--clr-bg);
|
||||
}
|
||||
|
||||
// restricting to 1600px width
|
||||
.main {
|
||||
--max-width: 160rem;
|
||||
width: min(100%, var(--max-width));
|
||||
margin-inline: auto;
|
||||
}
|
5
src/styles/base/_fonts.scss
Normal file
|
@ -0,0 +1,5 @@
|
|||
@font-face {
|
||||
font-family: 'RedHat Display';
|
||||
src: url('../../../public/fonts/RedHatDisplay-VariableFont_wght.ttf');
|
||||
font-display: swap;
|
||||
}
|
27
src/styles/base/_helpers.scss
Normal file
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* Hide text while making it readable for screen readers
|
||||
* 1. Needed in WebKit-based browsers because of an implementation bug;
|
||||
* See: https://code.google.com/p/chromium/issues/detail?id=457146
|
||||
*/
|
||||
.hide-text {
|
||||
overflow: hidden;
|
||||
padding: 0; /* 1 */
|
||||
text-indent: 101%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide element while making it readable for screen readers
|
||||
* Shamelessly borrowed from HTML5Boilerplate:
|
||||
* https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css#L119-L133
|
||||
*/
|
||||
.visually-hidden {
|
||||
border: 0;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
}
|
6
src/styles/base/_index.scss
Normal file
|
@ -0,0 +1,6 @@
|
|||
@forward './reset';
|
||||
// @forward './helpers';
|
||||
@forward './root';
|
||||
@forward './base';
|
||||
@forward './fonts';
|
||||
@forward './typography';
|
54
src/styles/base/_reset.scss
Normal file
|
@ -0,0 +1,54 @@
|
|||
html {
|
||||
scroll-behavior: smooth;
|
||||
font-size: 62.5%;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: inherit;
|
||||
// font: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
box-sizing: border-box;
|
||||
text-rendering: optimizeSpeed;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */
|
||||
ul[role='list'],
|
||||
ol[role='list'] {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* A elements that don't have a class get default styles */
|
||||
a:not([class]) {
|
||||
text-decoration-skip-ink: auto;
|
||||
}
|
||||
|
||||
/* Make images easier to work with */
|
||||
img,
|
||||
picture,
|
||||
svg {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Remove all animations, transitions and smooth scroll for people that prefer not to see them */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html:focus-within {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
27
src/styles/base/_root.scss
Normal file
|
@ -0,0 +1,27 @@
|
|||
@use '../abstracts' as helper;
|
||||
|
||||
:root {
|
||||
@include helper.typography;
|
||||
@include helper.typescale('desktop');
|
||||
@include helper.spacers;
|
||||
@include helper.colors('light');
|
||||
|
||||
// dark themed vars when root has an attribute of theme set to 'dark'
|
||||
&[data-theme='dark'] {
|
||||
@include helper.colors('dark');
|
||||
}
|
||||
|
||||
// styles to be applied when js is disabled
|
||||
&:not([data-js]) {
|
||||
// if the user prefers dark theme
|
||||
@media (prefers-color-scheme: dark) {
|
||||
// using dark theme instead of default one
|
||||
@include helper.colors('dark');
|
||||
}
|
||||
}
|
||||
|
||||
// change typescale for small screens
|
||||
@include helper.bp('bp-700') {
|
||||
@include helper.typescale('mobile');
|
||||
}
|
||||
}
|
22
src/styles/base/_typography.scss
Normal file
|
@ -0,0 +1,22 @@
|
|||
body {
|
||||
font-family: var(--ff-base);
|
||||
font-size: var(--fs-5);
|
||||
}
|
||||
|
||||
.heading {
|
||||
color: var(--clr-text-accent);
|
||||
font-family: var(--ff-accent);
|
||||
font-weight: var(--fw-medium);
|
||||
|
||||
&__primary {
|
||||
font-size: var(--fs-1);
|
||||
}
|
||||
|
||||
&__secondary {
|
||||
font-size: var(--fs-2);
|
||||
}
|
||||
|
||||
&__tertiary {
|
||||
font-size: var(--fs-3);
|
||||
}
|
||||
}
|
1
src/styles/components/_index.scss
Normal file
|
@ -0,0 +1 @@
|
|||
@forward './links';
|
6
src/styles/components/_links.scss
Normal file
|
@ -0,0 +1,6 @@
|
|||
@use '../abstracts' as helper;
|
||||
|
||||
.link,
|
||||
.ipc-md-link {
|
||||
@include helper.prettify-link(var(--clr-link));
|
||||
}
|
4
src/styles/main.scss
Normal file
|
@ -0,0 +1,4 @@
|
|||
@charset "UTF-8";
|
||||
|
||||
@use './base';
|
||||
@use './components';
|
|
@ -0,0 +1,13 @@
|
|||
.button {
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon {
|
||||
// we'll get --dimension var from header.module.scss
|
||||
height: var(--dimension, 4rem);
|
||||
width: var(--dimension, 4rem);
|
||||
|
||||
fill: var(--clr-fill);
|
||||
}
|
33
src/styles/modules/components/error/error-info.module.scss
Normal file
|
@ -0,0 +1,33 @@
|
|||
@use '../../../abstracts/' as helper;
|
||||
|
||||
.error {
|
||||
--doc-whitespace: var(--spacer-8);
|
||||
--comp-whitespace: var(--spacer-5);
|
||||
|
||||
padding: var(--doc-whitespace);
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
gap: var(--spacer-1);
|
||||
|
||||
@include helper.bp('bp-700') {
|
||||
--doc-whitespace: var(--spacer-5);
|
||||
--comp-whitespace: var(--spacer-3);
|
||||
}
|
||||
@include helper.bp('bp-450') {
|
||||
padding: var(--spacer-3);
|
||||
}
|
||||
}
|
||||
|
||||
.gnu {
|
||||
--dim: 30rem;
|
||||
|
||||
height: var(--dim);
|
||||
width: var(--dim);
|
||||
fill: var(--clr-fill);
|
||||
}
|
||||
|
||||
.heading {
|
||||
// justify-self: center;
|
||||
text-align: center;
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
.progress {
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
inset-inline: 0;
|
||||
inset-block-start: 0;
|
||||
height: 4px;
|
||||
width: 100%;
|
||||
background: var(--clr-fill);
|
||||
transform: translateX(-100%);
|
||||
box-shadow: 2px 0 5px var(--clr-fill);
|
||||
border-top-right-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
|
||||
animation: prograte 60s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes prograte {
|
||||
5% {
|
||||
transform: translateX(-40%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-3%);
|
||||
}
|
||||
}
|
172
src/styles/modules/components/title/basic.module.scss
Normal file
|
@ -0,0 +1,172 @@
|
|||
@use '../../../abstracts' as helper;
|
||||
|
||||
.container {
|
||||
margin-inline: auto;
|
||||
background: var(--clr-bg-accent);
|
||||
box-shadow: var(--clr-shadow);
|
||||
border-radius: 5px;
|
||||
overflow: hidden; // for background image
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: minmax(25rem, 30rem) 1fr;
|
||||
|
||||
@include helper.bp('bp-900') {
|
||||
grid-template-columns: none;
|
||||
grid-template-rows: 30rem min-content;
|
||||
}
|
||||
|
||||
@include helper.bp('bp-700') {
|
||||
grid-template-rows: 25rem min-content;
|
||||
}
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
display: flex; // for bringing out image__NA out of blur
|
||||
|
||||
position: relative;
|
||||
height: auto;
|
||||
width: auto;
|
||||
overflow: hidden;
|
||||
|
||||
background-size: cover;
|
||||
background-position: top;
|
||||
place-items: center;
|
||||
|
||||
@include helper.bp('bp-900') {
|
||||
padding: var(--spacer-2);
|
||||
isolation: isolate;
|
||||
|
||||
// for adding layer of color on top of background image
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
var(--clr-bg-accent) 10%,
|
||||
transparent
|
||||
);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
|
||||
@include helper.bp('bp-900') {
|
||||
z-index: 1;
|
||||
object-fit: contain;
|
||||
|
||||
outline: 3px solid var(--clr-fill);
|
||||
outline-offset: 5px;
|
||||
|
||||
max-height: 100%;
|
||||
margin: auto;
|
||||
|
||||
// overrriding nex/future/image defaults
|
||||
height: initial !important;
|
||||
width: initial !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
&__NA {
|
||||
z-index: 1;
|
||||
fill: var(--clr-fill-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
padding: var(--spacer-2) var(--spacer-4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacer-2);
|
||||
|
||||
@include helper.bp('bp-900') {
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
@include helper.bp('bp-450') {
|
||||
gap: var(--spacer-1);
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.meta {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
& * + *::before {
|
||||
content: '\00b7';
|
||||
padding-inline: var(--spacer-0);
|
||||
font-weight: 900;
|
||||
line-height: 0;
|
||||
font-size: var(--fs-5);
|
||||
}
|
||||
|
||||
@include helper.bp('bp-900') {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.ratings {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacer-0) var(--spacer-3);
|
||||
|
||||
@include helper.bp('bp-900') {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.rating {
|
||||
font-size: var(--fs-5);
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, max-content);
|
||||
place-items: center;
|
||||
gap: 0 var(--spacer-0);
|
||||
|
||||
&__num {
|
||||
grid-column: 1 / 2;
|
||||
font-size: 1.8em;
|
||||
font-weight: var(--fw-medium);
|
||||
// line-height: 1;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
--dim: 1.8em;
|
||||
grid-column: -2 / -1;
|
||||
line-height: 1;
|
||||
height: var(--dim);
|
||||
width: var(--dim);
|
||||
display: grid;
|
||||
place-content: center;
|
||||
fill: var(--clr-fill);
|
||||
}
|
||||
|
||||
&__text {
|
||||
grid-column: 1 / -1;
|
||||
font-size: 0.9em;
|
||||
line-height: 1;
|
||||
|
||||
color: var(--clr-text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
@include helper.prettify-link(var(--clr-link));
|
||||
}
|
||||
|
||||
.genres,
|
||||
.overview,
|
||||
.crewType {
|
||||
&__heading {
|
||||
font-weight: var(--fw-bold);
|
||||
}
|
||||
}
|
76
src/styles/modules/components/title/cast.module.scss
Normal file
|
@ -0,0 +1,76 @@
|
|||
@use '../../../abstracts' as helper;
|
||||
|
||||
.container {
|
||||
display: grid;
|
||||
gap: var(--comp-whitespace);
|
||||
margin-inline: auto; // for when cast members are so few that the container doesn't scroll
|
||||
}
|
||||
|
||||
.cast {
|
||||
--max-width: 15rem;
|
||||
--min-height: 35rem;
|
||||
list-style: none;
|
||||
overflow-x: auto;
|
||||
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
gap: var(--spacer-4);
|
||||
padding: 0 var(--spacer-2) var(--spacer-3) var(--spacer-2);
|
||||
|
||||
grid-auto-columns: var(--max-width);
|
||||
min-height: var(--min-height);
|
||||
|
||||
@include helper.bp('bp-700') {
|
||||
--min-height: 30rem;
|
||||
}
|
||||
}
|
||||
|
||||
.member {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: minmax(auto, 70%) min-content auto;
|
||||
justify-items: center;
|
||||
text-align: center;
|
||||
font-size: var(--fs-5);
|
||||
|
||||
overflow: hidden;
|
||||
border-radius: 5px;
|
||||
box-shadow: var(--clr-shadow);
|
||||
background-color: var(--clr-bg-accent);
|
||||
|
||||
&__imgContainer {
|
||||
justify-self: stretch;
|
||||
position: relative;
|
||||
|
||||
// for icon when image is unavailable
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
&__img {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
&__imgNA {
|
||||
fill: var(--clr-fill-muted);
|
||||
height: 40%;
|
||||
}
|
||||
|
||||
&__textContainer {
|
||||
display: grid;
|
||||
gap: var(--spacer-0);
|
||||
padding: var(--spacer-0);
|
||||
// place-content: center;
|
||||
text-align: center;
|
||||
justify-items: center;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
&__name {
|
||||
@include helper.prettify-link(var(--clr-link));
|
||||
}
|
||||
|
||||
&__role {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
.didYouKnow {
|
||||
display: grid;
|
||||
gap: var(--comp-whitespace);
|
||||
}
|
||||
|
||||
.container {
|
||||
display: grid;
|
||||
gap: var(--comp-whitespace);
|
||||
}
|
35
src/styles/modules/components/title/info.module.scss
Normal file
|
@ -0,0 +1,35 @@
|
|||
.info {
|
||||
display: grid;
|
||||
|
||||
gap: var(--doc-whitespace);
|
||||
}
|
||||
|
||||
.episodeInfo,
|
||||
.seriesInfo,
|
||||
.accolades,
|
||||
.keywords,
|
||||
.details,
|
||||
.boxoffice,
|
||||
.technical {
|
||||
display: grid;
|
||||
gap: var(--comp-whitespace);
|
||||
|
||||
&__container {
|
||||
display: grid;
|
||||
gap: var(--spacer-0);
|
||||
|
||||
// for span elements like these: 'release date:'
|
||||
& > p > span:first-of-type {
|
||||
font-weight: var(--fw-bold);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.keywords {
|
||||
&__container {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
flex-wrap: wrap;
|
||||
column-gap: var(--spacer-2);
|
||||
}
|
||||
}
|
101
src/styles/modules/components/title/media.module.scss
Normal file
|
@ -0,0 +1,101 @@
|
|||
@use '../../../abstracts' as helper;
|
||||
|
||||
// grid is better than flexbox, as in flexbox, you specifically have to specify height.
|
||||
|
||||
.media {
|
||||
--min-height: 30rem;
|
||||
--max-width: 50rem;
|
||||
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: 1fr;
|
||||
gap: var(--doc-whitespace); // declared in title.module.scss
|
||||
|
||||
@include helper.bp('bp-1200') {
|
||||
grid-auto-flow: row;
|
||||
grid-auto-columns: initial;
|
||||
}
|
||||
|
||||
@include helper.bp('bp-700') {
|
||||
--min-height: 20rem;
|
||||
--max-width: 30rem;
|
||||
}
|
||||
|
||||
@include helper.bp('bp-450') {
|
||||
// --min-height: 15rem;
|
||||
--max-width: 95%;
|
||||
}
|
||||
}
|
||||
|
||||
// section
|
||||
.images,
|
||||
.videos {
|
||||
display: grid;
|
||||
grid-template-rows: min-content;
|
||||
gap: var(--comp-whitespace); // declared in title.module.scss
|
||||
|
||||
&__container {
|
||||
overflow-x: auto;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
gap: var(--spacer-2);
|
||||
padding: 0 var(--spacer-2) var(--spacer-3) var(--spacer-2);
|
||||
|
||||
grid-auto-columns: var(--max-width);
|
||||
min-height: var(--min-height);
|
||||
}
|
||||
}
|
||||
|
||||
%cardify {
|
||||
overflow: hidden;
|
||||
border-radius: 5px;
|
||||
box-shadow: var(--clr-shadow);
|
||||
}
|
||||
|
||||
// child of .videos
|
||||
.trailer {
|
||||
@extend %cardify;
|
||||
|
||||
&__video {
|
||||
object-fit: cover;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// since it is wrapped in a tag
|
||||
.video {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.video,
|
||||
.image {
|
||||
@extend %cardify;
|
||||
position: relative;
|
||||
|
||||
&__caption {
|
||||
position: absolute;
|
||||
inset-block-end: 0;
|
||||
inset-inline-end: 0;
|
||||
padding: var(--spacer-0);
|
||||
|
||||
font-size: 0.9em;
|
||||
color: var(--clr-text);
|
||||
background: var(--clr-bg);
|
||||
|
||||
// hovering effect only for desktop/stylus users
|
||||
@media (any-hover: hover) and (any-pointer: fine) {
|
||||
transform: translateY(100%);
|
||||
transition: transform 500ms ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
&__img {
|
||||
object-position: top;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
&:hover &__caption {
|
||||
transform: translateY(1%); // 0% is leaving some gap from bottom
|
||||
}
|
||||
}
|
105
src/styles/modules/components/title/more-like-this.module.scss
Normal file
|
@ -0,0 +1,105 @@
|
|||
@use '../../../abstracts' as helper;
|
||||
|
||||
.morelikethis {
|
||||
display: grid;
|
||||
gap: var(--comp-whitespace);
|
||||
}
|
||||
|
||||
.container {
|
||||
--max-width: 20rem;
|
||||
--min-height: 50rem;
|
||||
|
||||
list-style: none;
|
||||
overflow-x: auto;
|
||||
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
gap: var(--spacer-4);
|
||||
padding: 0 var(--spacer-2) var(--spacer-3) var(--spacer-2);
|
||||
|
||||
grid-auto-columns: 20rem;
|
||||
min-height: 50rem;
|
||||
|
||||
> li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@include helper.bp('bp-700') {
|
||||
grid-auto-columns: 17rem;
|
||||
min-height: 37rem;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
overflow: hidden;
|
||||
border-radius: 5px;
|
||||
box-shadow: var(--clr-shadow);
|
||||
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: minmax(auto, 70%) auto;
|
||||
|
||||
background-color: var(--clr-bg-accent);
|
||||
text-decoration: none;
|
||||
color: currentColor;
|
||||
|
||||
&__imgContainer {
|
||||
justify-self: stretch;
|
||||
position: relative;
|
||||
|
||||
// for icon when image is unavailable
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
&__textContainer {
|
||||
display: grid;
|
||||
gap: var(--spacer-1);
|
||||
padding: var(--spacer-1);
|
||||
// place-content: center;
|
||||
text-align: center;
|
||||
justify-items: center;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
&__img {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
&__imgNA {
|
||||
fill: var(--clr-fill-muted);
|
||||
height: 40%;
|
||||
// vertical-align: center;
|
||||
}
|
||||
|
||||
&__heading {
|
||||
}
|
||||
|
||||
&__genres {
|
||||
}
|
||||
|
||||
&__rating {
|
||||
// font-size: 0.9em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacer-0);
|
||||
line-height: 1;
|
||||
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__ratingNum {
|
||||
}
|
||||
|
||||
&__ratingIcon {
|
||||
--dim: 1em;
|
||||
height: var(--dim);
|
||||
width: var(--dim);
|
||||
fill: var(--clr-fill);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--clr-bg-muted);
|
||||
}
|
||||
}
|