diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7a8b9ab --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[core] +autocrlf = false + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..37abf1b --- /dev/null +++ b/.eslintignore @@ -0,0 +1,35 @@ +typings.d.ts +index.d.ts +lib +cypress/e2e/build +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pids +*.pid +*.seed +*.pid.lock +lib-cov +coverage +node_modules +jspm_packages +typings/ +.npm +.env +.env.* +.cache +public +.idea +.vscode +.DS_Store +*.png +*.jpg +*.ico +*.md +*.mdx +*.json +LICENSE +*.txt +*.toml diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 9a4f809..0000000 --- a/.eslintrc +++ /dev/null @@ -1,68 +0,0 @@ -{ - "extends": ["airbnb-base", "prettier"], - "parser": "babel-eslint", - "parserOptions": { - "ecmaVersion": 2017, - "ecmaFeatures": { - "experimentalObjectRestSpread": true, - "impliedStrict": true, - "classes": true - } - }, - "env": { - "browser": true, - "node": true, - "jquery": true, - "jest": true - }, - "rules": { - "no-unused-vars": [ - 1, - { - "argsIgnorePattern": "res|next|stage|^err|on|config" - } - ], - "arrow-body-style": [2, "as-needed"], - "no-param-reassign": [ - 2, - { - "props": false - } - ], - "no-unused-expressions": [ - 1, { - "allowTaggedTemplates": true - } - ], - "no-console": 0, - "no-use-before-define": 0, - "no-underscore-dangle": 0, - "linebreak-style": 0, - "consistent-return": 0, - "import": 0, - "func-names": 0, - "import/no-extraneous-dependencies": 0, - "import/prefer-default-export": 0, - "space-before-function-paren": 0, - "import/extensions": 0, - "quotes": [ - 2, - "single", - { - "avoidEscape": true, - "allowTemplateLiterals": true - } - ], - "indent": ["error", 2, {"SwitchCase": 1}], - "prettier/prettier": [ - "error", - { - "trailingComma": "es5", - "semi": false, - "singleQuote": true, - "printWidth": 120 - } - ] - }, - "plugins": ["prettier"] -} \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..fadc8ab --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,114 @@ +module.exports = { + parser: `@typescript-eslint/parser`, // Specifies the ESLint parser + extends: [`airbnb`, `plugin:import/typescript`, `plugin:prettier/recommended`], + plugins: [`@typescript-eslint`, `prettier`, `react-hooks`], + parserOptions: { + ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features + sourceType: `module`, // Allows for the use of imports + ecmaFeatures: { + jsx: true, + }, + }, + env: { + browser: true, + jest: true, + node: true, + }, + globals: { + __PATH_PREFIX__: true, + graphql: false, + }, + rules: { + "@typescript-eslint/no-unused-vars": [ + 1, + { + argsIgnorePattern: `res|next|stage|^err|on|config|e|_`, + }, + ], + "arrow-body-style": [2, `as-needed`], + "no-param-reassign": [ + 2, + { + props: false, + }, + ], + "no-unused-expressions": [ + 1, + { + allowTaggedTemplates: true, + }, + ], + quotes: `off`, + "@typescript-eslint/quotes": [ + 2, + `backtick`, + { + avoidEscape: true, + }, + ], + "@typescript-eslint/prefer-interface": 0, + "@typescript-eslint/explicit-function-return-type": 0, + "@typescript-eslint/no-use-before-define": 0, + "@typescript-eslint/camelcase": 0, + "@typescript-eslint/no-var-requires": 0, + "@typescript-eslint/no-non-null-assertion": 0, + "@typescript-eslint/no-empty-function": 0, + "@typescript-eslint/explicit-module-boundary-types": 0, + "@typescript-eslint/ban-ts-comment": 0, + "no-console": [`warn`, { allow: [`warn`] }], + "spaced-comment": [2, `always`, { exceptions: [`-`, `+`], markers: [`/`] }], + "no-use-before-define": 0, + "no-plusplus": 0, + "no-continue": 0, + "linebreak-style": 0, + "consistent-return": 0, + import: 0, + camelcase: 1, + "import/no-unresolved": 0, + "func-names": 0, + "import/no-extraneous-dependencies": 0, + "import/prefer-default-export": 0, + "import/no-cycle": 0, + "space-before-function-paren": 0, + "import/extensions": 0, + "import/no-anonymous-default-export": 2, + "react/jsx-one-expression-per-line": 0, + "react/no-danger": 0, + "react/display-name": 0, + "react/react-in-jsx-scope": 0, + "react/jsx-uses-react": 1, + "react/require-default-props": 0, + "react/forbid-prop-types": 0, + "react/no-unescaped-entities": 0, + "react/prop-types": 0, + "react/jsx-props-no-spreading": 0, + "react/jsx-fragments": 0, + "react/jsx-curly-brace-presence": 0, + "react/jsx-pascal-case": 0, + "react/jsx-filename-extension": [ + 1, + { + extensions: [`.js`, `.jsx`, `.tsx`], + }, + ], + "react-hooks/rules-of-hooks": `error`, + "react-hooks/exhaustive-deps": `warn`, + indent: [`error`, 2, { SwitchCase: 1 }], + "prettier/prettier": [ + `error`, + { + trailingComma: `es5`, + semi: false, + singleQuote: false, + printWidth: 120, + }, + ], + "jsx-a11y/href-no-hash": `off`, + "jsx-a11y/anchor-is-valid": [ + `warn`, + { + aspects: [`invalidHref`], + }, + ], + }, +} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..e6d615d --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,8 @@ +# These are supported funding model platforms + +github: [LekoArts] +patreon: lekoarts +open_collective: # Replace with a single Open Collective username +ko_fi: lekoarts +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +custom: # Replace with a single custom sponsorship URL diff --git a/.github/actions/publish-starter/Dockerfile b/.github/actions/publish-starter/Dockerfile new file mode 100644 index 0000000..a4a9c0f --- /dev/null +++ b/.github/actions/publish-starter/Dockerfile @@ -0,0 +1,14 @@ +FROM node:12-slim + +LABEL com.github.actions.name="Publish starter" +LABEL com.github.actions.description="Automatically push subdirectories in a monorepo to their own repositories" +LABEL com.github.actions.icon="package" +LABEL com.github.actions.color="purple" + +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y git && \ + apt-get install -y jq + +COPY "entrypoint.sh" "/entrypoint.sh" +ENTRYPOINT ["/entrypoint.sh"] diff --git a/.github/actions/publish-starter/entrypoint.sh b/.github/actions/publish-starter/entrypoint.sh new file mode 100644 index 0000000..0d7ae43 --- /dev/null +++ b/.github/actions/publish-starter/entrypoint.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +set -e + +FOLDER=$1 +GITHUB_USERNAME=$2 +STARTER_NAME="${3:-name}" +BRANCH_NAME="${4:-main}" +BASE=$(pwd) + +git config --global user.email "lekoarts@gmail.com" +git config --global user.name "$GITHUB_USERNAME" + +echo "Cloning $FOLDER and pushing to $GITHUB_USERNAME" +echo "Using $STARTER_NAME as the package.json key" + +cd $BASE + +NAME=$(cat $FOLDER/package.json | jq --arg name "$STARTER_NAME" -r '.[$name]') +echo " Name: $NAME" +IS_WORKSPACE=$(cat $FOLDER/package.json | jq -r '.workspaces') +CLONE_DIR="__${NAME}__clone__" +echo " Clone dir: $CLONE_DIR" + +# clone, delete files in the clone, and copy (new) files over +# this handles file deletions, additions, and changes seamlessly +git clone --depth 1 https://$API_TOKEN_GITHUB@github.com/$GITHUB_USERNAME/$NAME.git $CLONE_DIR &> /dev/null +cd $CLONE_DIR +find . | grep -v ".git" | grep -v "^\.*$" | xargs rm -rf # delete all files (to handle deletions in monorepo) +cp -r $BASE/$FOLDER/. . + +# generate a new yarn.lock file based on package-lock.json unless you're in a workspace +if [ "$IS_WORKSPACE" = null ]; then + echo " Regenerating yarn.lock" + rm -rf yarn.lock + yarn +fi + +# Commit if there is anything to +if [ -n "$(git status --porcelain)" ]; then + echo " Committing $NAME to $GITHUB_REPOSITORY" + git add . + git commit --message "Update $NAME from $GITHUB_REPOSITORY" + git push origin $BRANCH_NAME + echo " Completed $NAME" +else + echo " No changes, skipping $NAME" +fi diff --git a/.github/workflows/publish-starter.yml b/.github/workflows/publish-starter.yml new file mode 100644 index 0000000..7c8ca22 --- /dev/null +++ b/.github/workflows/publish-starter.yml @@ -0,0 +1,20 @@ +name: Publish Starter +on: + workflow_dispatch: +jobs: + publish-starter: + name: Publish Starter + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Node.js 12.x + uses: actions/setup-node@v1 + with: + node-version: '12' + - name: Publish Starter + uses: ./.github/actions/publish-starter + env: + API_TOKEN_GITHUB: ${{ secrets.API_TOKEN_GITHUB }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + args: example LekoArts starter-name main diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 0000000..c4feb6d --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,15 @@ +name: Testing +on: pull_request +jobs: + testing: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/cache@v2 + with: + path: "**/node_modules" + key: ${{ runner.os }}-12.x-modules-${{ hashFiles('**/yarn.lock') }} + - name: Install dependencies + run: yarn install + - name: Run Jest + run: yarn test:ci diff --git a/.gitignore b/.gitignore index 4ff679e..eaf7982 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,31 @@ -# Logs -logs -*.log - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -/*.js -node_modules -.idea -!index.js \ No newline at end of file +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +**/dist +public +.cache +.vscode +node_modules +**/node_modules +.idea +.env +.env.* +!.env.example diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..37abf1b --- /dev/null +++ b/.prettierignore @@ -0,0 +1,35 @@ +typings.d.ts +index.d.ts +lib +cypress/e2e/build +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pids +*.pid +*.seed +*.pid.lock +lib-cov +coverage +node_modules +jspm_packages +typings/ +.npm +.env +.env.* +.cache +public +.idea +.vscode +.DS_Store +*.png +*.jpg +*.ico +*.md +*.mdx +*.json +LICENSE +*.txt +*.toml diff --git a/LICENSE b/LICENSE index 74292d6..a05e0de 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 Lennart +Copyright (c) 2021 LekoArts Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 9a03f51..08d5135 100644 --- a/README.md +++ b/README.md @@ -1,234 +1,54 @@ -# gatsby-source-tmdb - -Source from [The Movie Database (TMDb)](https://www.themoviedb.org/) API (v3) in Gatsby. - -You can see a **live example** at [tmdb.lekoarts.de](https://tmdb.lekoarts.de/) ([Source Code](https://github.com/LekoArts/gatsby-source-tmdb-example)). - -Built with [moviedb-promise](https://github.com/grantholle/moviedb-promise). - ---- - -## Install - -```bash -npm install --save gatsby-source-tmdb -``` - -## How to use - -### Prerequisites - -First, you need a way to pass environment variables to the build process, so secrets and other secured data aren't committed to source control. I recommend using [`dotenv`][dotenv] which will then expose environment variables. [Read more about dotenv and using environment variables here][envvars]. Then you can _use_ these environment variables and configure your plugin. - -You'll need an `API Key` and `Session ID` from TMDb. - -1. [Create your API Key](https://developers.themoviedb.org/3/getting-started/introduction) -2. [Create your Session ID](https://developers.themoviedb.org/3/authentication/how-do-i-generate-a-session-id) -3. Save both to your environment variable file - -It should look something like this: - -``` -API_KEY=your-api-key-here -SESSION_ID=your-session-id-here -``` - -You can find all information on the API endpoints in the [official The Movie Database API v3 documentation][documentation]. - -### gatsby-config - -#### Minimal - -The plugin sets some defaults for the endpoints and options. Hence you can use it only with the two **mandatory** entries `apiKey` and `sessionID`. You can see the default values in the "All options" overview. - -```JS -module.exports = { - plugins: [ - { - resolve: 'gatsby-source-tmdb', - options: { - // apiKey and sessionID are mandatory - apiKey: process.env.API_KEY, - sessionID: process.env.SESSION_ID, - }, - }, - ], -} -``` - -#### All options - -The plugin exposes some TMDb API options you can modify, e.g. [language][lang], [region][region] and [timezone][time]. - -The `modules` option gives your control over the querying of data from the TMDb API. The names are the function names of [moviedb-promise][moviedb]. By **default** all endpoints (which make sense) are inserted into the arrays. **Note:** Therefore the `modules` option can be used to minimize the data requested. - -The following `gatsby-config` shows all available options with their default values. - -```JS -module.exports = { - plugins: [ - { - resolve: 'gatsby-source-tmdb', - options: { - // apiKey and sessionID are mandatory - - apiKey: process.env.API_KEY, - sessionID: process.env.SESSION_ID, - - // Pass a ISO 639-1 value. Pattern: ([a-z]{2})-([A-Z]{2}) - // Specify the language of titles, descriptions etc. - // Applied to all results - - language: 'en-US', - - // Specify a ISO 3166-1 code. Pattern: [A-Z]{2} - // Will narrow the search to only display results within the specified country - - region: 'US', - - // You can specify what modules to use and which endpoints to grab data from - // If you want to use the default endpoints but deactivate modules, - // set the value of "activate" to true or false. - - modules: { - account: { - activate: true, - endpoints: { - tvs: ['accountFavoriteTv', 'accountRatedTv', 'accountTvWatchlist'], - movies: ['accountFavoriteMovies', 'accountRatedMovies', 'accountMovieWatchlist'], - list: 'accountLists', - }, - }, - misc: { - activate: false, - // The number behind the name specifies the amount of pages you want to pull - // By default it's set to 3 pages as otherwise, e.g. the endpoint "MiscPopularMovies - // would pull ~8000 pages (probably all movies) - // Each page contains 20 items - - endpoints: [ - ['miscUpcomingMovies'], - ['miscNowPlayingMovies'], - ['miscPopularMovies', 2], - ['miscTopRatedMovies', 2], - ['miscTopRatedTvs', 1], - ['miscPopularTvs', 1], - ], - }, - tv: { - activate: false, - endpoints: [['tvAiringToday'], ['tvOnTheAir', 2]], - }, - }, - - // Specify a timezone to offset the day calculation - // e.g. used in tvAiringToday - // See all timezones: https://developers.themoviedb.org/3/configuration/get-timezones - - timezone: 'Europe/London', - - // TMDb allows 40 Requests per 10 seconds - // If you pull a lot of data you could have an error - // telling you that you're over that limit. With this - // option you can do less requests per 10 seconds - - reqPerTenSeconds: 36, - - // Decide whether you want to download images from - // poster_path and backdrop_path URLs or not. - // This can save you a lot of time if you're not using one/both - // of them anyway - - poster: true, - backdrop: false, - }, - }, - ], -} -``` - -### Examples - -**Example 1: You only want `accountFavoriteTv`** - -```JS -modules: { - account: { - activate: true, - endpoints: { - tvs: ['accountFavoriteTv'], - }, - }, -} -``` - -**Example 2: You only want `accountFavoriteTv`, `accountRatedMovies` and `accountMovieWatchlist`** - -```JS -modules: { - account: { - activate: true, - endpoints: { - tvs: ['accountFavoriteTv'], - movies: ['accountRatedMovies', 'accountMovieWatchlist'], - }, - }, -} -``` - -**Example 3: You only want `miscPopularMovies` with 5 pages of results and `miscUpcomingMovies` with 3 pages of results (3 pages is the default)** - -_5 pages x 20 result per page = 100 items_ - -```JS -modules: { - misc: { - activate: true, - endpoints: [ - ['miscUpcomingMovies'], - ['miscPopularMovies', 5], - ], - }, -} -``` - -**Example 4: You only want `accountFavoriteTv` and `tvAiringToday`** - -```JS -modules: { - account: { - activate: true, - endpoints: { - tvs: ['accountFavoriteTv'], - }, - }, - tv: { - activate: true, - endpoints: [['tvAiringToday']], - }, -} -``` - -**Example 5: You don't want `account` but all endpoints from `misc` and `tv`** - -```JS -modules: { - account: { - activate: false, - }, - misc: { - activate: true, - }, - tv: { - activate: true, - }, -} -``` - -[dotenv]: https://github.com/motdotla/dotenv -[envvars]: https://gatsby.app/env-vars -[lang]: https://developers.themoviedb.org/3/getting-started/languages -[region]: https://developers.themoviedb.org/3/getting-started/regions -[time]: https://developers.themoviedb.org/3/configuration/get-timezones -[moviedb]: https://github.com/grantholle/moviedb-promise#complete-list -[documentation]: https://developers.themoviedb.org/3/getting-started/introduction +# gatsby-source-tmdb + +Source from [The Movie Database (TMDb)](https://www.themoviedb.org/) API (v3) in Gatsby. You can leverage any endpoint from the [official documentation](https://developers.themoviedb.org/3/getting-started/introduction) and pull the data directly into Gatsby's GraphQL data layer. Customize the plugin to your needs by providing customized endpoints -- read more about that in the [advanced configuration](https://github.com/LekoArts/gatsby-source-tmdb/tree/master/package/README.md#advanced-configuration) section. + +You can see a **live preview** at [tmdb.lekoarts.de](https://tmdb.lekoarts.de) ([Source Code](https://github.com/LekoArts/gatsby-source-tmdb/tree/master/example)) + +## Install + +```shell +npm install gatsby-source-tmdb +``` + +### Prerequisites + +You'll need an `API Key` and `Session ID` from TMDb. + +1. [Create your API key](https://developers.themoviedb.org/3/getting-started/introduction) +1. [Generate a Session ID](https://developers.themoviedb.org/3/authentication/how-do-i-generate-a-session-id) + +**Recommendation:** Save both values inside an `.env` file as environment variables. [Read more about env vars in Gatsby](https://www.gatsbyjs.com/docs/how-to/local-development/environment-variables/). + +You can find all information on the API endpoints in the [official TMDb v3 documentation](https://developers.themoviedb.org/3/getting-started/introduction). + +### Configure + +Add the plugin to your `gatsby-config.js` + +```js:title=gatsby-config.js +require("dotenv").config() + +module.exports = { + plugins: [ + { + resolve: "gatsby-source-tmdb", + options: { + apiKey: process.env.API_KEY, + sessionID: process.env.SESSION_ID, + } + } + ] +} +``` + +The plugin is **not** requesting all available endpoints by default but only a [selected list](https://github.com/LekoArts/gatsby-source-tmdb/tree/master/package/src/endpoint.ts) of endpoints. It always requests the `/account` & `/configuration` endpoint. Please see the [plugin's README](https://github.com/LekoArts/gatsby-source-tmdb/tree/master/package/README.md) for more detailed information, including options & advanced customization. + +## Acknowledgements + +- [moviedb-promise](https://github.com/grantholle/moviedb-promise) was used for v1 of this plugin and has been really helpful/inspirational when creating v2. Thanks! + +## Support Me + +Thanks for using this project! I'm always interested in seeing what people do with my projects, so don't hesitate to tag me on [Twitter](https://twitter.com/lekoarts_de) and share the project with me. + +Please star this project, share it on Social Media or consider supporting me on [Patreon](https://www.patreon.com/lekoarts) or [GitHub Sponsor](https://github.com/sponsors/LekoArts)! diff --git a/example/.env.example b/example/.env.example new file mode 100644 index 0000000..c3ba1b9 --- /dev/null +++ b/example/.env.example @@ -0,0 +1,2 @@ +GATSBY_API_KEY=xxx +SESSION_ID=xxx diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..82da3ec --- /dev/null +++ b/example/README.md @@ -0,0 +1,18 @@ +# gatsby-starter-tmdb + +An example project using `gatsby-source-tmdb` to build a dashboard of watchlist/favourite TV/movies and custom lists. You can see a **live preview** at [tmdb.lekoarts.de](https://tmdb.lekoarts.de). + +[Deploy to Gatsby Cloud](https://www.gatsbyjs.com/dashboard/deploynow?url=https://github.com/LekoArts/gatsby-starter-tmdb) + +Built with [vanilla-extract](https://github.com/seek-oss/vanilla-extract), [react-query](https://react-query.tanstack.com/). + +This example showcases a combination of build-time and client-only usage of the TMDb API. The `/` (homepage) page is built statically with `gatsby-source-tmdb`. The detailed pages (e.g. `/tv/`) are fetching their data on the client. [Read more about client-only routes](https://www.gatsbyjs.com/docs/reference/routing/file-system-route-api/#creating-client-only-routes). + +## Usage + +1. Clone [gatsby-starter-tmdb](https://github.com/LekoArts/gatsby-starter-tmdb) +1. Run `npm install` +1. Duplicate `.env.example` and rename it to `.env`. +1. Fill out the details required in the newly created `.env` file +1. Adapt any settings in `gatsby-config.js` +1. Adapt image files in `static` diff --git a/example/gatsby-config.js b/example/gatsby-config.js new file mode 100644 index 0000000..71f8ffb --- /dev/null +++ b/example/gatsby-config.js @@ -0,0 +1,55 @@ +require(`dotenv`).config() + +module.exports = { + siteMetadata: { + title: process.env.TITLE || `The Movie Database - LekoArts`, + description: process.env.DESC || `Source from The Movie Database (TMDb) API (v3) in Gatsby.`, + url: process.env.URL || `https://tmdb.lekoarts.de`, + logo: `/logo.png`, + }, + plugins: [ + { + resolve: `gatsby-source-tmdb`, + options: { + apiKey: process.env.GATSBY_API_KEY, + sessionID: process.env.SESSION_ID, + timezone: `Europe/Berlin`, + region: `DE`, + endpoints: [ + { + url: `account/:account_id/lists`, + extension: { + url: `list/:list_id`, + }, + }, + { + url: `account/:account_id/favorite/movies`, + extension: { + url: `movie/:movie_id`, + }, + }, + { + url: `account/:account_id/favorite/tv`, + extension: { + url: `tv/:tv_id`, + }, + }, + { + url: `account/:account_id/watchlist/movies`, + extension: { + url: `movie/:movie_id`, + }, + }, + { + url: `account/:account_id/watchlist/tv`, + extension: { + url: `tv/:tv_id`, + }, + }, + ], + }, + }, + `gatsby-plugin-react-helmet-async`, + `gatsby-plugin-gatsby-cloud`, + ], +} diff --git a/example/gatsby-node.js b/example/gatsby-node.js new file mode 100644 index 0000000..7324713 --- /dev/null +++ b/example/gatsby-node.js @@ -0,0 +1,15 @@ +const { VanillaExtractPlugin } = require(`@vanilla-extract/webpack-plugin`) + +exports.onCreateBabelConfig = ({ actions }) => { + actions.setBabelPlugin({ + name: require.resolve(`@vanilla-extract/babel-plugin`), + }) +} + +exports.onCreateWebpackConfig = ({ actions, stage }) => { + if (stage === `develop` || stage === `build-javascript`) { + actions.setWebpackConfig({ + plugins: [new VanillaExtractPlugin()], + }) + } +} diff --git a/example/package.json b/example/package.json new file mode 100644 index 0000000..9d7ce8b --- /dev/null +++ b/example/package.json @@ -0,0 +1,30 @@ +{ + "name": "example", + "version": "0.0.1", + "private": true, + "license": "MIT", + "starter-name": "gatsby-starter-tmdb", + "dependencies": { + "@vanilla-extract/babel-plugin": "^0.1.0", + "@vanilla-extract/css": "^0.1.0", + "@vanilla-extract/webpack-plugin": "^0.1.0", + "date-fns": "^2.19.0", + "gatsby": "^3.2.1", + "gatsby-plugin-gatsby-cloud": "^2.2.0", + "gatsby-plugin-react-helmet-async": "^1.2.0", + "gatsby-source-tmdb": "*", + "react": "^17.0.2", + "react-content-loader": "^6.0.2", + "react-dom": "^17.0.2", + "react-helmet-async": "^1.0.9", + "react-query": "^3.13.4", + "react-spring": "^9.0.0", + "react-tabs": "^3.2.1" + }, + "scripts": { + "develop": "gatsby develop", + "build": "gatsby build", + "serve": "gatsby serve", + "clean": "gatsby clean" + } +} diff --git a/example/src/api/fetch-tmdb.ts b/example/src/api/fetch-tmdb.ts new file mode 100644 index 0000000..365d27e --- /dev/null +++ b/example/src/api/fetch-tmdb.ts @@ -0,0 +1,13 @@ +const BASE_URL = `https://api.themoviedb.org/3/` + +export const fetchTmdb = async ({ type, id }) => { + const URL = `${BASE_URL}${type}/${id}?api_key=${process.env.GATSBY_API_KEY}&language=en-US&append_to_response=videos,similar,credits` + // @ts-ignore + const res = await fetch(URL) + + if (res.url !== URL) { + throw new Error(`Couldn't load the information. Please try again later.`) + } + + return res.json() +} diff --git a/example/src/api/provider.tsx b/example/src/api/provider.tsx new file mode 100644 index 0000000..89be3e4 --- /dev/null +++ b/example/src/api/provider.tsx @@ -0,0 +1,15 @@ +import * as React from "react" +import { QueryClient, QueryClientProvider } from "react-query" + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60 * 60, // 1 hour, + retry: 3, // Do 3 retries until failed + }, + }, +}) + +const Provider: React.FC = ({ children }) => {children} + +export default Provider diff --git a/example/src/components/back.css.ts b/example/src/components/back.css.ts new file mode 100644 index 0000000..f67f83c --- /dev/null +++ b/example/src/components/back.css.ts @@ -0,0 +1,18 @@ +import { style } from "@vanilla-extract/css" +import { themeVars } from "../styles.css" + +export const backStyle = style({ + color: themeVars.color.white, + position: `absolute`, + top: `1rem`, + left: `1rem`, + padding: `0.5rem 0.75rem`, + background: `rgba(0, 0, 0, 0.1)`, + borderRadius: themeVars.radii.default, + textDecoration: `none`, + transition: `all 0.3s ease-in-out`, + ":hover": { + background: themeVars.color.primary, + color: themeVars.color.black, + }, +}) diff --git a/example/src/components/back.tsx b/example/src/components/back.tsx new file mode 100644 index 0000000..8773186 --- /dev/null +++ b/example/src/components/back.tsx @@ -0,0 +1,11 @@ +import * as React from "react" +import { Link } from "gatsby" +import { backStyle } from "./back.css" + +const Back: React.FC = () => ( + + Back to Overview + +) + +export default Back diff --git a/example/src/components/card.css.ts b/example/src/components/card.css.ts new file mode 100644 index 0000000..37385e6 --- /dev/null +++ b/example/src/components/card.css.ts @@ -0,0 +1,113 @@ +import { style } from "@vanilla-extract/css" +import { breakpoints, themeVars } from "../styles.css" + +export const imageStyle = style({ + position: `absolute`, + top: `0`, + bottom: `0`, + left: `0`, + right: `0`, + zIndex: -1000, + height: `100% !important`, + width: `100% !important`, +}) + +export const wrapperStyle = style({ + display: `flex`, + flexDirection: `column`, + justifyContent: `flex-end`, + paddingBottom: `150%`, + boxShadow: `0 10px 30px -5px rgba(0, 0, 0, 0.3)`, + transition: `box-shadow 0.5s`, + willChange: `transform`, + width: `100%`, + borderRadius: themeVars.radii.default, + overflow: `hidden`, + position: `relative`, + ":after": { + content: `''`, + position: `absolute`, + display: `block`, + width: `102%`, + height: `102%`, + top: `0`, + left: `-3px`, + right: `0`, + bottom: `0`, + background: `linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 100%)`, + zIndex: -10, + borderRadius: themeVars.radii.default, + transition: `opacity 0.5s ease-in-out`, + opacity: 1, + }, + "@media": { + [`screen and (min-width: ${breakpoints.sm})`]: { + ":after": { + opacity: 0, + }, + }, + }, + selectors: { + "&:hover:after": { + opacity: 1, + }, + }, +}) + +export const contentStyle = style({ + padding: `0.75rem`, + position: `absolute`, + bottom: `0`, + left: `0`, + right: `0`, + opacity: 1, + transition: `opacity 0.5s ease-in-out`, + selectors: { + [`${wrapperStyle}:hover &`]: { + opacity: 1, + }, + }, + "@media": { + [`screen and (min-width: ${breakpoints.sm})`]: { + padding: `1rem`, + opacity: 0, + }, + }, +}) + +export const titleStyle = style({ + fontSize: `1.15rem`, +}) + +export const itemStyle = style({ + marginRight: `0.5rem`, + display: `flex`, + alignItems: `center`, + flexDirection: `column`, + justifyContent: `space-between`, + "@media": { + [`screen and (min-width: ${breakpoints.sm})`]: { + marginRight: `1rem`, + }, + }, +}) + +export const itemIconStyle = style({ + fill: themeVars.color.primary, + marginBottom: `0.25rem`, +}) + +export const itemTextStyle = style({ + fontSize: `0.75rem`, + textAlign: `center`, +}) + +export const linkStyle = style({ + position: `absolute`, + top: `0`, + right: `0`, + bottom: `0`, + left: `0`, + color: themeVars.color.white, + ":hover": { color: themeVars.color.white }, +}) diff --git a/example/src/components/card.tsx b/example/src/components/card.tsx new file mode 100644 index 0000000..49a9713 --- /dev/null +++ b/example/src/components/card.tsx @@ -0,0 +1,110 @@ +import * as React from "react" +import { useSpring, animated } from "react-spring" +import { Link } from "gatsby" +import { format, parseISO } from "date-fns" +import { Icon } from "./icon" +import { + wrapperStyle, + linkStyle, + imageStyle, + contentStyle, + titleStyle, + itemStyle, + itemIconStyle, + itemTextStyle, +} from "./card.css" + +type CardProps = { + name: string + link: string + cover: string + next?: string + rating: number + status?: "Returning Series" | "Ended" | "Canceled" + release: string + episodes?: number + seasons?: number +} + +const trans = (x, y, s) => `perspective(600px) rotateX(${x}deg) rotateY(${y}deg) scale(${s})` + +const Card: React.FC = ({ name, link, cover, next, rating, status, release, episodes, seasons }) => { + const ref = React.useRef() + const [animatedProps, api] = useSpring(() => ({ + xys: [0, 0, 1], + config: { mass: 10, tension: 400, friction: 30, precision: 0.00001 }, + })) + + return ( + { + const x = + // @ts-ignore + clientX - (ref.current.offsetLeft - (window.scrollX || window.pageXOffset || document.body.scrollLeft)) + // @ts-ignore + const y = clientY - (ref.current.offsetTop - (window.scrollY || window.pageYOffset || document.body.scrollTop)) + const dampen = 80 // Higher number => less rotation + const xys = [ + // @ts-ignore + -(y - ref.current.clientHeight / 2) / dampen, // rotateX + // @ts-ignore + (x - ref.current.clientWidth / 2) / dampen, // rotateY + 1.05, // Scale + ] + api.start({ xys }) + }} + onMouseLeave={() => { + api.start({ xys: [0, 0, 1] }) + }} + style={{ transform: animatedProps.xys.to(trans) }} + > + + +
+

+ {name} + {` `} + {status && + (status === `Returning Series` ? ( + + ) : ( + + ))} +

+
+
+
{rating}
+
+
+ + {` `} +
{format(parseISO(release), `yyyy`)}
+
+ {next && ( +
+ + {` `} +
{format(parseISO(next), `dd.MM.yy`)}
+
+ )} + {episodes && ( +
+
{episodes}
+
+ )} + {seasons && ( +
+
{seasons}
+
+ )} +
+
+ +
+ ) +} + +export default Card diff --git a/example/src/components/detail-view.css.ts b/example/src/components/detail-view.css.ts new file mode 100644 index 0000000..4a1f9d5 --- /dev/null +++ b/example/src/components/detail-view.css.ts @@ -0,0 +1,219 @@ +import { style, globalStyle } from "@vanilla-extract/css" +import { breakpoints, themeVars } from "../styles.css" + +export const detailViewWrapperStyle = style({ + padding: `5rem 0`, + "@media": { + [`screen and (min-width: ${breakpoints.sm})`]: { + padding: `6rem 0`, + }, + }, +}) + +export const informationStyle = style({ + display: `flex`, + flexDirection: `column`, + "@media": { + [`screen and (min-width: ${breakpoints.sm})`]: { + flexDirection: `row`, + }, + }, +}) + +export const posterWrapperStyle = style({ + position: `relative`, + "@media": { + [`screen and (max-width: ${breakpoints.sm})`]: { + marginBottom: `2rem`, + }, + }, +}) + +export const posterImageStyle = style({ + borderRadius: themeVars.radii.big, + maxWidth: `100%`, + boxShadow: `0 12px 40px -5px rgba(0, 0, 0, 0.4)`, +}) + +export const mainWrapper = style({ + marginLeft: 0, + maxWidth: `100%`, + "@media": { + [`screen and (min-width: ${breakpoints.sm})`]: { + marginLeft: `3rem`, + maxWidth: `50%`, + }, + }, +}) + +export const h1Style = style({ + marginTop: `0`, + marginBottom: `0.75rem`, + textShadow: `0 6px 18px rgba(0, 0, 0, 0.25)`, +}) + +export const h2Style = style({ + fontSize: `1.4rem`, + textShadow: `0 6px 18px rgba(0, 0, 0, 0.25)`, + color: themeVars.color.white, +}) + +export const originalNameStyle = style({ + fontStyle: `italic`, + fontWeight: 400, + color: themeVars.color.grey, +}) + +export const statistics1Style = style({ + fontSize: `1.5rem`, + lineHeight: `2rem`, + margin: `1rem 0`, + color: themeVars.color.greyLight, +}) + +globalStyle(`${statistics1Style} svg`, { + verticalAlign: `bottom`, + width: `2rem`, + height: `2rem`, +}) + +export const primaryFillStyle = style({ + fill: themeVars.color.primary, +}) + +export const whiteFillStyle = style({ + fill: themeVars.color.white, +}) + +export const totalRuntimeStyle = style({ + marginTop: `1rem`, + fontSize: `0.9rem`, + color: themeVars.color.grey, +}) + +export const statistics2Style = style({ + marginTop: `2rem`, + display: `flex`, + alignItems: `center`, + color: themeVars.color.greyLight, +}) + +globalStyle(`${statistics2Style} div`, { + marginRight: `1.25rem`, + fontSize: `1.15rem`, +}) + +globalStyle(`${statistics2Style} svg`, { + verticalAlign: `baseline`, +}) + +export const genresStyle = style({ + marginTop: `1rem`, + display: `flex`, + flexWrap: `wrap`, +}) + +export const genreStyle = style({ + fontSize: `0.8rem`, + marginRight: `0.5rem`, + padding: `0.35rem 0.65rem`, + background: `rgba(0, 0, 0, 0.1)`, + borderRadius: themeVars.radii.default, +}) + +export const overviewStyle = style({ + marginTop: `2rem`, + color: themeVars.color.greyLight, +}) + +export const paragraphStyle = style({ + letterSpacing: `-0.003em`, + lineHeight: 1.58, + fontSize: `1rem`, +}) + +export const secondaryInformationStyle = style({ + marginTop: `4rem`, +}) + +export const castOverviewStyle = style({ + display: `flex`, + flexWrap: `wrap`, + flexDirection: `row`, + justifyContent: `flex-start`, +}) + +export const castStyle = style({ + display: `flex`, + flexDirection: `column`, + flexWrap: `nowrap`, + width: `185px`, + marginBottom: `2rem`, + marginRight: `2rem`, +}) + +export const castImageWrapperStyle = style({ + maxWidth: `185px`, + height: `225px`, +}) + +export const castImageStyle = style({ + width: `100%`, + height: `100%`, + objectFit: `cover`, + borderRadius: themeVars.radii.default, + boxShadow: `0 6px 16px rgba(0, 0, 0, 0.3)`, +}) + +export const castNamesStyle = style({ + marginTop: `1rem`, + display: `flex`, + flexDirection: `column`, + flexWrap: `nowrap`, + fontSize: `1rem`, + color: themeVars.color.grey, +}) + +export const castNamesDetailStyle = style({ + fontWeight: 700, + color: themeVars.color.greyLight, +}) + +export const trailerStyle = style({ + position: `relative`, + overflow: `hidden`, + paddingTop: `56.25%`, + boxShadow: `0 10px 30px rgba(0, 0, 0, 0.3)`, + marginBottom: `3rem`, +}) + +export const iframeStyle = style({ + position: `absolute`, + top: `0`, + left: `0`, + width: `100%`, + height: `100%`, + border: `0`, +}) + +export const similarStyle = style({ + display: `flex`, + flexDirection: `row`, + flexWrap: `wrap`, + marginBottom: `2rem`, +}) + +export const similarLinkStyle = style({ + marginRight: `0.5rem`, + marginBottom: `0.5rem`, + padding: `0.4rem 0.75rem`, + background: `rgba(0, 0, 0, 0.2)`, + borderRadius: themeVars.radii.default, + color: themeVars.color.greyLight, + textDecoration: `none`, + transition: `all 0.3s ease-in-out`, + ":hover": { + color: themeVars.color.black, + background: themeVars.color.primary, + }, +}) diff --git a/example/src/components/detail-view.tsx b/example/src/components/detail-view.tsx new file mode 100644 index 0000000..9979179 --- /dev/null +++ b/example/src/components/detail-view.tsx @@ -0,0 +1,219 @@ +import * as React from "react" +import { useQuery } from "react-query" +import ContentLoader from "react-content-loader" +import { format, parseISO } from "date-fns" +import { fetchTmdb } from "../api/fetch-tmdb" +import { Icon } from "./icon" +import { + detailViewWrapperStyle, + informationStyle, + posterWrapperStyle, + posterImageStyle, + mainWrapper, + h1Style, + originalNameStyle, + h2Style, + statistics1Style, + primaryFillStyle, + totalRuntimeStyle, + statistics2Style, + whiteFillStyle, + genresStyle, + genreStyle, + overviewStyle, + paragraphStyle, + secondaryInformationStyle, + castOverviewStyle, + castStyle, + castImageWrapperStyle, + castImageStyle, + castNamesStyle, + castNamesDetailStyle, + trailerStyle, + iframeStyle, + similarStyle, + similarLinkStyle, +} from "./detail-view.css" + +const IMAGE_URL = `https://image.tmdb.org/t/p/` + +const convertMinsToHrsMins = (mins: number) => { + const h = Math.floor(mins / 60) + const m = mins % 60 + + if (m === 0) { + return `${h} Hours` + } + + return `${h} Hours ${m} Minutes` +} + +const calculateTime = ({ episodes, runTime }: { episodes: number; runTime: number | number[] }) => { + let time + if (Array.isArray(runTime)) { + // eslint-disable-next-line prefer-destructuring + time = runTime[0] + } else { + time = runTime + } + return convertMinsToHrsMins(episodes * time) +} + +const DetailView: React.FC<{ id: string; type: "tv" | "movie" }> = ({ id, type }) => { + const { status, data, error } = useQuery([`${type}-detail`, id], async () => fetchTmdb({ id, type })) + + if (status === `loading`) { + return ( +
+ + + + + + + + + + + + + + + + + + +
+ ) + } + + if (status === `error`) { + // @ts-ignore + return
Error: {error.message}
+ } + + return ( +
+
+
+ +
+
+

+ {data.title || data.name} ({format(parseISO(data.release_date || data.first_air_date), `yyyy`)}) +

+ {(data.title !== data.original_title || data.name !== data.original_name) && ( +
Original: {data.original_title || data.original_name}
+ )} +
+ {data.vote_average} + {data.status && + type === `tv` && + (data.status === `Returning Series` ? ( + + {data.status} + + ) : ( + + {data.status} + + ))} +
+
+ {data.number_of_episodes && ( +
+ {data.number_of_episodes} +
+ )} + {data.number_of_seasons && ( +
+ {data.number_of_seasons} +
+ )} + {data.next_episode_to_air && ( +
+ + {` `} + {format(parseISO(data.next_episode_to_air.air_date), `dd.MM.yy`)} +
+ )} +
+ {data.runtime &&
Runtime: {data.runtime} Minutes
} + {data.episode_run_time && ( +
+ Total Runtime:{` `} + {calculateTime({ + episodes: data.number_of_episodes, + runTime: data.episode_run_time, + })} +
+ )} +
+ {data.genres.map((genre) => ( +
+ {genre.name} +
+ ))} +
+
+

Overview

+

{data.overview}

+
+
+
+
+ {data.credits.cast.length > 0 && ( + <> +

Top Billed Cast

+
+ {data.credits.cast.slice(0, 10).map((member) => ( +
+
+ +
+
+ {member.name} + {member.character} +
+
+ ))} +
+ + )} + {data.videos.results.length > 0 && ( + <> +

Trailer

+
+