Compare commits

...

36 Commits

Author SHA1 Message Date
fallenbagel
0da85ca0d1 build(dockerfile): ignore scripts to not run husky install when devdependencies are pruned 2024-06-24 00:32:49 +05:00
fallenbagel
d5e37e4f3f build(dockerfile): install node-gyp through npm 2024-06-24 00:14:21 +05:00
fallenbagel
f5a50914c8 build(dockerfile): add node-gyp back in 2024-06-24 00:10:30 +05:00
fallenbagel
e30a3ea74f build: migrate pnpm 8 to 9 2024-06-24 00:06:52 +05:00
fallenbagel
cc80bf2c56 build(dockerfile): remove unnecessary && on apk installation steps 2024-06-23 23:50:16 +05:00
fallenbagel
2c52dbcea3 build: install pnpm for all platforms 2024-06-23 23:47:52 +05:00
fallenbagel
1168c77cad build(dockerfile): copy the proper pnpm lockfile 2024-06-23 23:38:02 +05:00
fallenbagel
182aeaa636 build(dockerfile): migrate to pnpm from yarn in docker builds 2024-06-23 23:35:01 +05:00
fallenbagel
a58811e675 ci: use sh shell to get pnpm store directory 2024-06-23 23:29:43 +05:00
fallenbagel
37ba36f2df ci: pnpm cache to reduce install time 2024-06-23 23:27:29 +05:00
fallenbagel
e50df22cbf ci(cypress): setup nodejs v20 in cypress workflow 2024-06-23 23:14:30 +05:00
fallenbagel
878afb91df style: ran prettier on pnpm-lock 2024-06-23 23:10:21 +05:00
fallenbagel
ffb20ebe93 test(cypress): use pnpm instead of yarn 2024-06-23 23:09:45 +05:00
fallenbagel
cb7e2f073e ci: fix typo in pnpm action-setup for cypress workflow 2024-06-23 23:04:30 +05:00
fallenbagel
588b1e36dd ci: specify the pnpm version to use in workflow actions 2024-06-23 23:01:58 +05:00
fallenbagel
99ee19c714 build: migrate yarn to pnpm and restrict engine to node@^20.0.0 2024-06-23 22:57:44 +05:00
gauthier-th
fac81f75c9 fix: resolve various UI issues 2024-06-23 18:12:58 +02:00
gauthier-th
ea43e3ba1e refactor: update Node.js to v20 2024-06-23 13:33:03 +02:00
gauthier-th
f3e180afb1 fix: resize avatar images 2024-06-23 11:48:22 +02:00
gauthier-th
b5738b49d6 chore: temporarily remove builds for ARMv7 2024-06-22 23:51:32 +02:00
gauthier-th
966a721c54 fix: resolve GitHub CodeQL alert 2024-06-21 19:11:45 +02:00
gauthier-th
43f8260675 fix: change extract script for i18n to a custom script 2024-06-21 18:34:32 +02:00
fallenbagel
50700002e2 chore: added sharp for production image optimisation 2024-06-20 20:15:04 +05:00
gauthier-th
f7de2418e5 fix: break word on long path to avoid text overflow 2024-06-20 16:58:24 +02:00
gauthier-th
5c212ae2a8 fix: resize logo in sidebar 2024-06-20 11:28:51 +02:00
gauthier-th
6b248d97a7 refactor: switch compiler from Babel to SWC 2024-06-20 00:08:02 +02:00
gauthier-th
5efa1d7a46 fix: resolve webpack cache issue with country-flag-icons 2024-06-19 21:12:00 +02:00
gauthier-th
cb8cadae71 chore: merge origin/develop 2024-06-19 16:59:26 +02:00
fallenbagel
06e465d052 build: fixes an issue where dev env could lead to javascript heap out of memory 2024-06-19 04:03:04 +05:00
gauthier-th
5fb1c687fc fix: temporary allow all domains for image optimization 2024-06-18 23:57:53 +02:00
gauthier-th
65239a922f fix: adjust full-size for next/image components 2024-06-17 15:06:57 +02:00
Gauthier
48d178c1e9 fix: add proper size to next/image components 2024-06-16 00:20:01 +02:00
Gauthier
5d6e7f09a2 fix: remove old intl polyfill 2024-06-16 00:17:45 +02:00
Gauthier
c680202008 refactor: update ESLint rules and fix warnings/errors 2024-06-15 16:32:24 +02:00
Gauthier
63d8f550c4 refactor: update Next.js images 2024-06-15 13:45:42 +02:00
Gauthier
9ab5fa5972 refactor: update Next.js and React.js 2024-06-14 00:42:51 +02:00
172 changed files with 28129 additions and 15467 deletions

View File

@@ -5,9 +5,7 @@ module.exports = {
'eslint:recommended', 'eslint:recommended',
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
'plugin:jsx-a11y/recommended', 'plugin:jsx-a11y/recommended',
'plugin:react/recommended', 'plugin:@next/next/recommended',
'plugin:react-hooks/recommended',
'plugin:react/jsx-runtime',
'prettier', 'prettier',
], ],
parserOptions: { parserOptions: {

View File

@@ -13,20 +13,35 @@ jobs:
name: Lint & Test Build name: Lint & Test Build
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
container: node:18.18-alpine container: node:20-alpine
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Pnpm Setup
uses: pnpm/action-setup@v4
with:
version: 9
- name: Get pnpm store directory
shell: sh
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies - name: Install dependencies
env: env:
HUSKY: 0 HUSKY: 0
run: yarn run: pnpm install
- name: Lint - name: Lint
run: yarn lint run: pnpm lint
- name: Formatting - name: Formatting
run: yarn format:check run: pnpm format:check
- name: Build - name: Build
run: yarn build run: pnpm build
build_and_push: build_and_push:
name: Build & Publish Docker Images name: Build & Publish Docker Images
@@ -60,7 +75,7 @@ jobs:
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64
push: true push: true
build-args: | build-args: |
COMMIT_TAG=${{ github.sha }} COMMIT_TAG=${{ github.sha }}

View File

@@ -14,11 +14,19 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Pnpm Setup
uses: pnpm/action-setup@v4
with:
version: 9
- name: Cypress run - name: Cypress run
uses: cypress-io/github-action@v6 uses: cypress-io/github-action@v6
with: with:
build: yarn cypress:build build: pnpm cypress:build
start: yarn start start: pnpm start
wait-on: 'http://localhost:5055' wait-on: 'http://localhost:5055'
record: true record: true
env: env:

View File

@@ -29,7 +29,7 @@ jobs:
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64
push: true push: true
build-args: | build-args: |
COMMIT_TAG=${{ github.sha }} COMMIT_TAG=${{ github.sha }}

View File

@@ -16,7 +16,7 @@ jobs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 20
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
@@ -26,8 +26,23 @@ jobs:
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }} password: ${{ secrets.DOCKER_TOKEN }}
- name: Pnpm Setup
uses: pnpm/action-setup@v4
with:
version: 9
- name: Get pnpm store directory
shell: sh
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies - name: Install dependencies
run: yarn run: pnpm install
- name: Release - name: Release
env: env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
@@ -45,7 +60,6 @@ jobs:
# architecture: # architecture:
# - amd64 # - amd64
# - arm64 # - arm64
# - armhf
# steps: # steps:
# - name: Checkout Code # - name: Checkout Code
# uses: actions/checkout@v4 # uses: actions/checkout@v4

View File

@@ -30,7 +30,6 @@ jobs:
architecture: architecture:
- amd64 - amd64
- arm64 - arm64
- armhf
steps: steps:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v4 uses: actions/checkout@v4

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict=true

View File

@@ -1,4 +1,4 @@
FROM node:18.18-alpine AS BUILD_IMAGE FROM node:20-alpine AS BUILD_IMAGE
WORKDIR /app WORKDIR /app
@@ -10,22 +10,24 @@ RUN \
'linux/arm64' | 'linux/arm/v7') \ 'linux/arm64' | 'linux/arm/v7') \
apk update && \ apk update && \
apk add --no-cache python3 make g++ gcc libc6-compat bash && \ apk add --no-cache python3 make g++ gcc libc6-compat bash && \
yarn global add node-gyp \ npm install --global node-gyp \
;; \ ;; \
esac esac
COPY package.json yarn.lock ./ Run npm install --global pnpm
RUN CYPRESS_INSTALL_BINARY=0 yarn install --frozen-lockfile --network-timeout 1000000
COPY package.json pnpm-lock.yaml ./
RUN CYPRESS_INSTALL_BINARY=0 pnpm install --frozen-lockfile
COPY . ./ COPY . ./
ARG COMMIT_TAG ARG COMMIT_TAG
ENV COMMIT_TAG=${COMMIT_TAG} ENV COMMIT_TAG=${COMMIT_TAG}
RUN yarn build RUN pnpm build
# remove development dependencies # remove development dependencies
RUN yarn install --production --ignore-scripts --prefer-offline RUN pnpm prune --prod --ignore-scripts
RUN rm -rf src server .next/cache RUN rm -rf src server .next/cache
@@ -34,7 +36,7 @@ RUN touch config/DOCKER
RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
FROM node:18.18-alpine FROM node:20-alpine
# Metadata for Github Package Registry # Metadata for Github Package Registry
LABEL org.opencontainers.image.source="https://github.com/Fallenbagel/jellyseerr" LABEL org.opencontainers.image.source="https://github.com/Fallenbagel/jellyseerr"
@@ -47,6 +49,6 @@ RUN apk add --no-cache tzdata tini && rm -rf /tmp/*
COPY --from=BUILD_IMAGE /app ./ COPY --from=BUILD_IMAGE /app ./
ENTRYPOINT [ "/sbin/tini", "--" ] ENTRYPOINT [ "/sbin/tini", "--" ]
CMD [ "yarn", "start" ] CMD [ "pnpm", "start" ]
EXPOSE 5055 EXPOSE 5055

View File

@@ -1,4 +1,4 @@
FROM node:18.18-alpine FROM node:20-alpine
COPY . /app COPY . /app
WORKDIR /app WORKDIR /app

View File

@@ -53,7 +53,7 @@ https://hub.docker.com/r/fallenbagel/jellyseerr
Pre-requisites: Pre-requisites:
- Nodejs [v18](https://nodejs.org/download/release/v18.18.2) - Nodejs [v20](https://nodejs.org/en/download)
- [Yarn](https://classic.yarnpkg.com/lang/en/docs/install) - [Yarn](https://classic.yarnpkg.com/lang/en/docs/install)
- Download/git clone the source code from the github (Either develop branch or main for stable) - Download/git clone the source code from the github (Either develop branch or main for stable)
@@ -73,7 +73,7 @@ _To set env variables such as `JELLYFIN_TYPE=emby` create a file called `.env` i
**Pre-requisites:** **Pre-requisites:**
- Nodejs [v18](https://nodejs.org/en/download/package-manager) - Nodejs [v20](https://nodejs.org/en/download)
- [Yarn](https://classic.yarnpkg.com/lang/en/docs/install) (on Debian based distros, the package manager provided `yarn` is different and is a package called cmdlet. You can remove that using `apt-remove cmdlet` then install yarn using `npm install -g yarn`) - [Yarn](https://classic.yarnpkg.com/lang/en/docs/install) (on Debian based distros, the package manager provided `yarn` is different and is a package called cmdlet. You can remove that using `apt-remove cmdlet` then install yarn using `npm install -g yarn`)
- Git - Git

View File

@@ -1,25 +0,0 @@
module.exports = function (api) {
api.cache(true);
return {
presets: [
[
'next/babel',
{
'preset-env': {
useBuiltIns: 'entry',
corejs: '3',
},
},
],
],
plugins: [
[
'react-intl-auto',
{
removePrefix: 'src/',
},
],
],
};
};

View File

@@ -2,6 +2,6 @@ import './commands';
before(() => { before(() => {
if (Cypress.env('SEED_DATABASE')) { if (Cypress.env('SEED_DATABASE')) {
cy.exec('yarn cypress:prepare'); cy.exec('pnpm cypress:prepare');
} }
}); });

View File

@@ -10,7 +10,11 @@ module.exports = {
JELLYFIN_TYPE: process.env.JELLYFIN_TYPE, JELLYFIN_TYPE: process.env.JELLYFIN_TYPE,
}, },
images: { images: {
domains: ['image.tmdb.org'], remotePatterns: [
{ hostname: 'gravatar.com' },
{ hostname: 'image.tmdb.org' },
{ hostname: '*', protocol: 'https' },
],
}, },
webpack(config) { webpack(config) {
config.module.rules.push({ config.module.rules.push({

View File

@@ -3,26 +3,27 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "nodemon -e ts --watch server --watch overseerr-api.yml -e .json,.ts,.yml -x ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/index.ts", "dev": "nodemon -e ts --watch server --watch overseerr-api.yml -e .json,.ts,.yml -x ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/index.ts",
"build:server": "tsc --project server/tsconfig.json && copyfiles -u 2 server/templates/**/*.{html,pug} dist/templates && tsc-alias -p server/tsconfig.json", "build:server": "tsc --project server/tsconfig.json && copyfiles -u 2 server/templates/**/*.{html,pug} dist/templates && tsc-alias -p server/tsconfig.json",
"build:next": "next build", "build:next": "next build",
"build": "yarn build:next && yarn build:server", "build": "pnpm build:next && pnpm build:server",
"lint": "eslint \"./server/**/*.{ts,tsx}\" \"./src/**/*.{ts,tsx}\" --cache", "lint": "eslint \"./server/**/*.{ts,tsx}\" \"./src/**/*.{ts,tsx}\" --cache",
"lintfix": "eslint \"./server/**/*.{ts,tsx}\" \"./src/**/*.{ts,tsx}\" --fix", "lintfix": "eslint \"./server/**/*.{ts,tsx}\" \"./src/**/*.{ts,tsx}\" --fix",
"start": "NODE_ENV=production node dist/index.js", "start": "NODE_ENV=production node dist/index.js",
"i18n:extract": "extract-messages -l=en -o src/i18n/locale -d en --flat true --overwriteDefault true \"./src/**/!(*.test).{ts,tsx}\"", "i18n:extract": "ts-node --project server/tsconfig.json src/i18n/extractMessages.ts",
"migration:generate": "ts-node -r tsconfig-paths/register --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:generate -d server/datasource.ts", "migration:generate": "ts-node -r tsconfig-paths/register --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:generate -d server/datasource.ts",
"migration:create": "ts-node -r tsconfig-paths/register --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:create -d server/datasource.ts", "migration:create": "ts-node -r tsconfig-paths/register --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:create -d server/datasource.ts",
"migration:run": "ts-node -r tsconfig-paths/register --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:run -d server/datasource.ts", "migration:run": "ts-node -r tsconfig-paths/register --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:run -d server/datasource.ts",
"format": "prettier --loglevel warn --write --cache .", "format": "prettier --loglevel warn --write --cache .",
"format:check": "prettier --check --cache .", "format:check": "prettier --check --cache .",
"typecheck": "yarn typecheck:server && yarn typecheck:client", "typecheck": "pnpm typecheck:server && pnpm typecheck:client",
"typecheck:server": "tsc --project server/tsconfig.json --noEmit", "typecheck:server": "tsc --project server/tsconfig.json --noEmit",
"typecheck:client": "tsc --noEmit", "typecheck:client": "tsc --noEmit",
"prepare": "husky install", "prepare": "husky install",
"cypress:open": "cypress open", "cypress:open": "cypress open",
"cypress:prepare": "ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/scripts/prepareTestDb.ts", "cypress:prepare": "ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/scripts/prepareTestDb.ts",
"cypress:build": "yarn build && yarn cypress:prepare" "cypress:build": "pnpm build && pnpm cypress:prepare"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -34,6 +35,7 @@
"@formatjs/intl-locale": "3.1.1", "@formatjs/intl-locale": "3.1.1",
"@formatjs/intl-pluralrules": "5.1.10", "@formatjs/intl-pluralrules": "5.1.10",
"@formatjs/intl-utils": "3.8.4", "@formatjs/intl-utils": "3.8.4",
"@formatjs/swc-plugin-experimental": "^0.4.0",
"@headlessui/react": "1.7.12", "@headlessui/react": "1.7.12",
"@heroicons/react": "2.0.16", "@heroicons/react": "2.0.16",
"@supercharge/request-ip": "1.2.0", "@supercharge/request-ip": "1.2.0",
@@ -59,11 +61,10 @@
"express-openapi-validator": "4.13.8", "express-openapi-validator": "4.13.8",
"express-rate-limit": "6.7.0", "express-rate-limit": "6.7.0",
"express-session": "1.17.3", "express-session": "1.17.3",
"formik": "2.2.9", "formik": "^2.4.6",
"gravatar-url": "3.1.0", "gravatar-url": "3.1.0",
"intl": "1.2.5",
"lodash": "4.17.21", "lodash": "4.17.21",
"next": "12.3.4", "next": "^14.2.4",
"node-cache": "5.1.2", "node-cache": "5.1.2",
"node-gyp": "9.3.1", "node-gyp": "9.3.1",
"node-schedule": "2.1.1", "node-schedule": "2.1.1",
@@ -71,13 +72,13 @@
"openpgp": "5.7.0", "openpgp": "5.7.0",
"plex-api": "5.3.2", "plex-api": "5.3.2",
"pug": "3.0.2", "pug": "3.0.2",
"react": "18.2.0", "react": "^18.3.1",
"react-ace": "10.1.0", "react-ace": "10.1.0",
"react-animate-height": "2.1.2", "react-animate-height": "2.1.2",
"react-aria": "3.23.0", "react-aria": "3.23.0",
"react-dom": "18.2.0", "react-dom": "^18.3.1",
"react-intersection-observer": "9.4.3", "react-intersection-observer": "9.4.3",
"react-intl": "6.2.10", "react-intl": "^6.6.8",
"react-markdown": "8.0.5", "react-markdown": "8.0.5",
"react-popper-tooltip": "4.4.2", "react-popper-tooltip": "4.4.2",
"react-select": "5.7.0", "react-select": "5.7.0",
@@ -89,9 +90,10 @@
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"secure-random-password": "0.2.3", "secure-random-password": "0.2.3",
"semver": "7.3.8", "semver": "7.3.8",
"sharp": "^0.33.4",
"sqlite3": "5.1.4", "sqlite3": "5.1.4",
"swagger-ui-express": "4.6.2", "swagger-ui-express": "4.6.2",
"swr": "2.0.4", "swr": "2.2.5",
"typeorm": "0.3.12", "typeorm": "0.3.12",
"web-push": "3.5.0", "web-push": "3.5.0",
"winston": "3.8.2", "winston": "3.8.2",
@@ -102,7 +104,6 @@
"zod": "3.20.6" "zod": "3.20.6"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "7.21.0",
"@commitlint/cli": "17.4.4", "@commitlint/cli": "17.4.4",
"@commitlint/config-conventional": "17.4.4", "@commitlint/config-conventional": "17.4.4",
"@semantic-release/changelog": "6.0.2", "@semantic-release/changelog": "6.0.2",
@@ -123,8 +124,8 @@
"@types/node": "17.0.36", "@types/node": "17.0.36",
"@types/node-schedule": "2.1.0", "@types/node-schedule": "2.1.0",
"@types/nodemailer": "6.4.7", "@types/nodemailer": "6.4.7",
"@types/react": "18.0.28", "@types/react": "^18.3.3",
"@types/react-dom": "18.0.11", "@types/react-dom": "^18.3.0",
"@types/react-transition-group": "4.4.5", "@types/react-transition-group": "4.4.5",
"@types/secure-random-password": "0.2.1", "@types/secure-random-password": "0.2.1",
"@types/semver": "7.3.13", "@types/semver": "7.3.13",
@@ -136,15 +137,13 @@
"@typescript-eslint/eslint-plugin": "5.54.0", "@typescript-eslint/eslint-plugin": "5.54.0",
"@typescript-eslint/parser": "5.54.0", "@typescript-eslint/parser": "5.54.0",
"autoprefixer": "10.4.13", "autoprefixer": "10.4.13",
"babel-plugin-react-intl": "8.2.25",
"babel-plugin-react-intl-auto": "3.3.0",
"commitizen": "4.3.0", "commitizen": "4.3.0",
"copyfiles": "2.4.1", "copyfiles": "2.4.1",
"cy-mobile-commands": "0.3.0", "cy-mobile-commands": "0.3.0",
"cypress": "12.7.0", "cypress": "12.7.0",
"cz-conventional-changelog": "3.3.0", "cz-conventional-changelog": "3.3.0",
"eslint": "8.35.0", "eslint": "8.35.0",
"eslint-config-next": "12.3.4", "eslint-config-next": "^14.2.4",
"eslint-config-prettier": "8.6.0", "eslint-config-prettier": "8.6.0",
"eslint-plugin-formatjs": "4.9.0", "eslint-plugin-formatjs": "4.9.0",
"eslint-plugin-jsx-a11y": "6.7.1", "eslint-plugin-jsx-a11y": "6.7.1",
@@ -152,7 +151,6 @@
"eslint-plugin-prettier": "4.2.1", "eslint-plugin-prettier": "4.2.1",
"eslint-plugin-react": "7.32.2", "eslint-plugin-react": "7.32.2",
"eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-react-hooks": "4.6.0",
"extract-react-intl-messages": "4.1.1",
"husky": "8.0.3", "husky": "8.0.3",
"lint-staged": "13.1.2", "lint-staged": "13.1.2",
"nodemon": "2.0.20", "nodemon": "2.0.20",
@@ -168,10 +166,12 @@
"tsconfig-paths": "4.1.2", "tsconfig-paths": "4.1.2",
"typescript": "4.9.5" "typescript": "4.9.5"
}, },
"resolutions": { "engines": {
"node": "^20.0.0",
"pnpm": "^9.0.0"
},
"overrides": {
"sqlite3/node-gyp": "8.4.1", "sqlite3/node-gyp": "8.4.1",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"@types/express-session": "1.17.6" "@types/express-session": "1.17.6"
}, },
"config": { "config": {
@@ -237,8 +237,7 @@
], ],
"platforms": [ "platforms": [
"linux/amd64", "linux/amd64",
"linux/arm64", "linux/arm64"
"linux/arm/v7"
] ]
}, },
"@semantic-release/github" "@semantic-release/github"

26267
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -146,7 +146,7 @@ app
try { try {
const descriptor = Object.getOwnPropertyDescriptor(req, 'ip'); const descriptor = Object.getOwnPropertyDescriptor(req, 'ip');
if (descriptor?.writable === true) { if (descriptor?.writable === true) {
req.ip = getClientIp(req) ?? ''; (req as any).ip = getClientIp(req) ?? '';
} }
} catch (e) { } catch (e) {
logger.error('Failed to attach the ip to the request', { logger.error('Failed to attach the ip to the request', {

View File

@@ -5,6 +5,7 @@
"module": "commonjs", "module": "commonjs",
"outDir": "../dist", "outDir": "../dist",
"noEmit": false, "noEmit": false,
"incremental": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@server/*": ["*"] "@server/*": ["*"]

View File

@@ -12,7 +12,7 @@ confinement: strict
architectures: architectures:
- build-on: amd64 - build-on: amd64
- build-on: arm64 - build-on: arm64
- build-on: armhf # - build-on: armhf
parts: parts:
jellyseerr: jellyseerr:
@@ -27,12 +27,12 @@ parts:
- automake - automake
- python-gi - python-gi
- python-gi-dev - python-gi-dev
- on armhf: # - on armhf:
- libatomic1 # - libatomic1
- build-essential # - build-essential
- automake # - automake
- python-gi # - python-gi
- python-gi-dev # - python-gi-dev
source: . source: .
override-pull: | override-pull: |
snapcraftctl pull snapcraftctl pull
@@ -75,7 +75,7 @@ parts:
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
# Set Node.js version # Set Node.js version
NODE_MAJOR=18 NODE_MAJOR=20
# Add Node.js repository to sources list # Add Node.js repository to sources list
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
@@ -97,9 +97,9 @@ parts:
cp -R $SNAPCRAFT_PART_BUILD/node_modules $SNAPCRAFT_PART_INSTALL/ cp -R $SNAPCRAFT_PART_BUILD/node_modules $SNAPCRAFT_PART_INSTALL/
# Remove .github and gitbook as it will fail snap lint # Remove .github and gitbook as it will fail snap lint
rm -rf $SNAPCRAFT_PART_INSTALL/.github rm -rf $SNAPCRAFT_PART_INSTALL/.github
stage-packages: # stage-packages:
- on armhf: # - on armhf:
- libatomic1 # - libatomic1
stage: [.next, ./*] stage: [.next, ./*]
prime: [.next, ./*] prime: [.next, ./*]

View File

@@ -1,7 +1,8 @@
import Badge from '@app/components/Common/Badge'; import Badge from '@app/components/Common/Badge';
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl'; import defineMessages from '@app/utils/defineMessages';
import { FormattedRelativeTime, useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.AirDateBadge', {
airedrelative: 'Aired {relativeTime}', airedrelative: 'Aired {relativeTime}',
airsrelative: 'Airing {relativeTime}', airsrelative: 'Airing {relativeTime}',
}); });

View File

@@ -1,8 +1,9 @@
import Alert from '@app/components/Common/Alert'; import Alert from '@app/components/Common/Alert';
import { defineMessages, useIntl } from 'react-intl'; import defineMessages from '@app/utils/defineMessages';
import { useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.AppDataWarning', {
dockerVolumeMissingDescription: dockerVolumeMissingDescription:
'The <code>{appDataPath}</code> volume mount was not configured properly. All data will be cleared when the container is stopped or restarted.', 'The <code>{appDataPath}</code> volume mount was not configured properly. All data will be cleared when the container is stopped or restarted.',
}); });

View File

@@ -10,6 +10,7 @@ import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper'; import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'; import { ArrowDownTrayIcon } from '@heroicons/react/24/outline';
import { MediaStatus } from '@server/constants/media'; import { MediaStatus } from '@server/constants/media';
@@ -18,10 +19,10 @@ import { uniq } from 'lodash';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.CollectionDetails', {
overview: 'Overview', overview: 'Overview',
numberofmovies: '{count} Movies', numberofmovies: '{count} Movies',
requestcollection: 'Request Collection', requestcollection: 'Request Collection',
@@ -166,10 +167,9 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
<Link <Link
href={`/discover/movies/genre/${genreId}`} href={`/discover/movies/genre/${genreId}`}
key={`genre-${genreId}`} key={`genre-${genreId}`}
className="hover:underline"
> >
<a className="hover:underline"> {genres.find((g) => g.id === genreId)?.name}
{genres.find((g) => g.id === genreId)?.name}
</a>
</Link> </Link>
)) ))
.reduce((prev, curr) => ( .reduce((prev, curr) => (
@@ -195,8 +195,8 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
<CachedImage <CachedImage
alt="" alt=""
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`} src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`}
layout="fill" style={{ width: '100%', height: '100%', objectFit: 'cover' }}
objectFit="cover" fill
priority priority
/> />
<div <div
@@ -229,7 +229,8 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
: '/images/overseerr_poster_not_found.png' : '/images/overseerr_poster_not_found.png'
} }
alt="" alt=""
layout="responsive" sizes="100vw"
style={{ width: '100%', height: 'auto' }}
width={600} width={600}
height={900} height={900}
priority priority

View File

@@ -1,4 +1,3 @@
import type * as React from 'react';
import { useState } from 'react'; import { useState } from 'react';
import AnimateHeight from 'react-animate-height'; import AnimateHeight from 'react-animate-height';

View File

@@ -93,13 +93,12 @@ const Badge = (
); );
} else if (href) { } else if (href) {
return ( return (
<Link href={href}> <Link
<a href={href}
className={badgeStyle.join(' ')} className={badgeStyle.join(' ')}
ref={ref as React.Ref<HTMLAnchorElement>} ref={ref as React.Ref<HTMLAnchorElement>}
> >
{children} {children}
</a>
</Link> </Link>
); );
} else { } else {

View File

@@ -64,8 +64,8 @@ const ImageFader: ForwardRefRenderFunction<HTMLDivElement, ImageFaderProps> = (
className="absolute inset-0 h-full w-full" className="absolute inset-0 h-full w-full"
alt="" alt=""
src={imageUrl} src={imageUrl}
layout="fill" style={{ width: '100%', height: '100%', objectFit: 'cover' }}
objectFit="cover" fill
{...overrides} {...overrides}
/> />
<div <div

View File

@@ -125,8 +125,8 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
<CachedImage <CachedImage
alt="" alt=""
src={backdrop} src={backdrop}
layout="fill" style={{ width: '100%', height: '100%', objectFit: 'cover' }}
objectFit="cover" fill
priority priority
/> />
<div <div

View File

@@ -55,15 +55,14 @@ const SettingsLink = ({
} }
return ( return (
<Link href={route}> <Link
<a href={route}
className={`${linkClasses} ${ className={`${linkClasses} ${
currentPath.match(regex) ? activeLinkColor : inactiveLinkColor currentPath.match(regex) ? activeLinkColor : inactiveLinkColor
}`} }`}
aria-current="page" aria-current="page"
> >
{children} {children}
</a>
</Link> </Link>
); );
}; };

View File

@@ -12,40 +12,39 @@ const CompanyCard = ({ image, url, name }: CompanyCardProps) => {
const [isHovered, setHovered] = useState(false); const [isHovered, setHovered] = useState(false);
return ( return (
<Link href={url}> <Link
<a href={url}
className={`relative flex h-32 w-56 transform-gpu cursor-pointer items-center justify-center p-8 shadow ring-1 transition duration-300 ease-in-out sm:h-36 sm:w-72 ${ className={`relative flex h-32 w-56 transform-gpu cursor-pointer items-center justify-center p-8 shadow ring-1 transition duration-300 ease-in-out sm:h-36 sm:w-72 ${
isHovered isHovered
? 'scale-105 bg-gray-700 ring-gray-500' ? 'scale-105 bg-gray-700 ring-gray-500'
: 'scale-100 bg-gray-800 ring-gray-700' : 'scale-100 bg-gray-800 ring-gray-700'
} rounded-xl`} } rounded-xl`}
onMouseEnter={() => { onMouseEnter={() => {
setHovered(true);
}}
onMouseLeave={() => setHovered(false)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
setHovered(true); setHovered(true);
}} }
onMouseLeave={() => setHovered(false)} }}
onKeyDown={(e) => { role="link"
if (e.key === 'Enter') { tabIndex={0}
setHovered(true); >
} <div className="relative h-full w-full">
}} <CachedImage
role="link" src={image}
tabIndex={0} alt={name}
> className="relative z-40 h-full w-full"
<div className="relative h-full w-full"> style={{ width: '100%', height: '100%', objectFit: 'contain' }}
<CachedImage fill
src={image}
alt={name}
className="relative z-40 h-full w-full"
layout="fill"
objectFit="contain"
/>
</div>
<div
className={`absolute bottom-0 left-0 right-0 z-0 h-12 rounded-b-xl bg-gradient-to-t ${
isHovered ? 'from-gray-800' : 'from-gray-900'
}`}
/> />
</a> </div>
<div
className={`absolute bottom-0 left-0 right-0 z-0 h-12 rounded-b-xl bg-gradient-to-t ${
isHovered ? 'from-gray-800' : 'from-gray-900'
}`}
/>
</Link> </Link>
); );
}; };

View File

@@ -4,6 +4,7 @@ import { sliderTitles } from '@app/components/Discover/constants';
import MediaSlider from '@app/components/MediaSlider'; import MediaSlider from '@app/components/MediaSlider';
import { WatchProviderSelector } from '@app/components/Selector'; import { WatchProviderSelector } from '@app/components/Selector';
import { encodeURIExtraParams } from '@app/hooks/useDiscover'; import { encodeURIExtraParams } from '@app/hooks/useDiscover';
import defineMessages from '@app/utils/defineMessages';
import type { import type {
TmdbCompanySearchResponse, TmdbCompanySearchResponse,
TmdbGenre, TmdbGenre,
@@ -16,12 +17,12 @@ import type { Keyword, ProductionCompany } from '@server/models/common';
import axios from 'axios'; import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import AsyncSelect from 'react-select/async'; import AsyncSelect from 'react-select/async';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import * as Yup from 'yup'; import * as Yup from 'yup';
const messages = defineMessages({ const messages = defineMessages('components.Discover.CreateSlider', {
addSlider: 'Add Slider', addSlider: 'Add Slider',
editSlider: 'Edit Slider', editSlider: 'Edit Slider',
slidernameplaceholder: 'Slider Name', slidernameplaceholder: 'Slider Name',

View File

@@ -4,11 +4,12 @@ import PageTitle from '@app/components/Common/PageTitle';
import useDiscover from '@app/hooks/useDiscover'; import useDiscover from '@app/hooks/useDiscover';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import type { MovieResult } from '@server/models/Search'; import type { MovieResult } from '@server/models/Search';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.Discover.DiscoverMovieGenre', {
genreMovies: '{genre} Movies', genreMovies: '{genre} Movies',
}); });

View File

@@ -4,12 +4,13 @@ import PageTitle from '@app/components/Common/PageTitle';
import useDiscover, { encodeURIExtraParams } from '@app/hooks/useDiscover'; import useDiscover, { encodeURIExtraParams } from '@app/hooks/useDiscover';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import type { TmdbKeyword } from '@server/api/themoviedb/interfaces'; import type { TmdbKeyword } from '@server/api/themoviedb/interfaces';
import type { MovieResult } from '@server/models/Search'; import type { MovieResult } from '@server/models/Search';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.Discover.DiscoverMovieKeyword', {
keywordMovies: '{keywordTitle} Movies', keywordMovies: '{keywordTitle} Movies',
}); });

View File

@@ -4,11 +4,12 @@ import PageTitle from '@app/components/Common/PageTitle';
import useDiscover from '@app/hooks/useDiscover'; import useDiscover from '@app/hooks/useDiscover';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import type { MovieResult } from '@server/models/Search'; import type { MovieResult } from '@server/models/Search';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.Discover.DiscoverMovieLanguage', {
languageMovies: '{language} Movies', languageMovies: '{language} Movies',
}); });

View File

@@ -11,14 +11,15 @@ import FilterSlideover from '@app/components/Discover/FilterSlideover';
import useDiscover from '@app/hooks/useDiscover'; import useDiscover from '@app/hooks/useDiscover';
import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams'; import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import { BarsArrowDownIcon, FunnelIcon } from '@heroicons/react/24/solid'; import { BarsArrowDownIcon, FunnelIcon } from '@heroicons/react/24/solid';
import type { SortOptions as TMDBSortOptions } from '@server/api/themoviedb'; import type { SortOptions as TMDBSortOptions } from '@server/api/themoviedb';
import type { MovieResult } from '@server/models/Search'; import type { MovieResult } from '@server/models/Search';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useState } from 'react'; import { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.Discover.DiscoverMovies', {
discovermovies: 'Movies', discovermovies: 'Movies',
activefilters: activefilters:
'{count, plural, one {# Active Filter} other {# Active Filters}}', '{count, plural, one {# Active Filter} other {# Active Filters}}',

View File

@@ -4,12 +4,14 @@ import PageTitle from '@app/components/Common/PageTitle';
import useDiscover from '@app/hooks/useDiscover'; import useDiscover from '@app/hooks/useDiscover';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import type { TvNetwork } from '@server/models/common'; import type { TvNetwork } from '@server/models/common';
import type { TvResult } from '@server/models/Search'; import type { TvResult } from '@server/models/Search';
import Image from 'next/image';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.Discover.DiscoverNetwork', {
networkSeries: '{network} Series', networkSeries: '{network} Series',
}); });
@@ -47,10 +49,11 @@ const DiscoverTvNetwork = () => {
<Header> <Header>
{firstResultData?.network.logoPath ? ( {firstResultData?.network.logoPath ? (
<div className="mb-6 flex justify-center"> <div className="mb-6 flex justify-center">
<img <Image
src={`//image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)${firstResultData.network.logoPath}`} src={`//image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)${firstResultData.network.logoPath}`}
alt={firstResultData.network.name} alt={firstResultData.network.name}
className="max-h-24 sm:max-h-32" className="max-h-24 sm:max-h-32"
fill
/> />
</div> </div>
) : ( ) : (

View File

@@ -8,6 +8,7 @@ import CreateSlider from '@app/components/Discover/CreateSlider';
import GenreTag from '@app/components/GenreTag'; import GenreTag from '@app/components/GenreTag';
import KeywordTag from '@app/components/KeywordTag'; import KeywordTag from '@app/components/KeywordTag';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { import {
ArrowUturnLeftIcon, ArrowUturnLeftIcon,
@@ -22,10 +23,10 @@ import type DiscoverSlider from '@server/entity/DiscoverSlider';
import axios from 'axios'; import axios from 'axios';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { useDrag, useDrop } from 'react-aria'; import { useDrag, useDrop } from 'react-aria';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
const messages = defineMessages({ const messages = defineMessages('components.Discover.DiscoverSliderEdit', {
deletesuccess: 'Sucessfully deleted slider.', deletesuccess: 'Sucessfully deleted slider.',
deletefail: 'Failed to delete slider.', deletefail: 'Failed to delete slider.',
remove: 'Remove', remove: 'Remove',

View File

@@ -4,12 +4,14 @@ import PageTitle from '@app/components/Common/PageTitle';
import useDiscover from '@app/hooks/useDiscover'; import useDiscover from '@app/hooks/useDiscover';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import type { ProductionCompany } from '@server/models/common'; import type { ProductionCompany } from '@server/models/common';
import type { MovieResult } from '@server/models/Search'; import type { MovieResult } from '@server/models/Search';
import Image from 'next/image';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.Discover.DiscoverStudio', {
studioMovies: '{studio} Movies', studioMovies: '{studio} Movies',
}); });
@@ -47,10 +49,11 @@ const DiscoverMovieStudio = () => {
<Header> <Header>
{firstResultData?.studio.logoPath ? ( {firstResultData?.studio.logoPath ? (
<div className="mb-6 flex justify-center"> <div className="mb-6 flex justify-center">
<img <Image
src={`//image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)${firstResultData.studio.logoPath}`} src={`//image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)${firstResultData.studio.logoPath}`}
alt={firstResultData.studio.name} alt={firstResultData.studio.name}
className="max-h-24 sm:max-h-32" className="max-h-24 sm:max-h-32"
fill
/> />
</div> </div>
) : ( ) : (

View File

@@ -11,14 +11,15 @@ import FilterSlideover from '@app/components/Discover/FilterSlideover';
import useDiscover from '@app/hooks/useDiscover'; import useDiscover from '@app/hooks/useDiscover';
import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams'; import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import { BarsArrowDownIcon, FunnelIcon } from '@heroicons/react/24/solid'; import { BarsArrowDownIcon, FunnelIcon } from '@heroicons/react/24/solid';
import type { SortOptions as TMDBSortOptions } from '@server/api/themoviedb'; import type { SortOptions as TMDBSortOptions } from '@server/api/themoviedb';
import type { TvResult } from '@server/models/Search'; import type { TvResult } from '@server/models/Search';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useState } from 'react'; import { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.Discover.DiscoverTv', {
discovertv: 'Series', discovertv: 'Series',
activefilters: activefilters:
'{count, plural, one {# Active Filter} other {# Active Filters}}', '{count, plural, one {# Active Filter} other {# Active Filters}}',

View File

@@ -4,11 +4,12 @@ import PageTitle from '@app/components/Common/PageTitle';
import useDiscover from '@app/hooks/useDiscover'; import useDiscover from '@app/hooks/useDiscover';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import type { TvResult } from '@server/models/Search'; import type { TvResult } from '@server/models/Search';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.Discover.DiscoverTvGenre', {
genreSeries: '{genre} Series', genreSeries: '{genre} Series',
}); });

View File

@@ -4,12 +4,13 @@ import PageTitle from '@app/components/Common/PageTitle';
import useDiscover, { encodeURIExtraParams } from '@app/hooks/useDiscover'; import useDiscover, { encodeURIExtraParams } from '@app/hooks/useDiscover';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import type { TmdbKeyword } from '@server/api/themoviedb/interfaces'; import type { TmdbKeyword } from '@server/api/themoviedb/interfaces';
import type { TvResult } from '@server/models/Search'; import type { TvResult } from '@server/models/Search';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.Discover.DiscoverTvKeyword', {
keywordSeries: '{keywordTitle} Series', keywordSeries: '{keywordTitle} Series',
}); });

View File

@@ -4,11 +4,12 @@ import PageTitle from '@app/components/Common/PageTitle';
import useDiscover from '@app/hooks/useDiscover'; import useDiscover from '@app/hooks/useDiscover';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import type { TvResult } from '@server/models/Search'; import type { TvResult } from '@server/models/Search';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.Discover.DiscoverTvLanguage', {
languageSeries: '{language} Series', languageSeries: '{language} Series',
}); });

View File

@@ -3,12 +3,11 @@ import ListView from '@app/components/Common/ListView';
import PageTitle from '@app/components/Common/PageTitle'; import PageTitle from '@app/components/Common/PageTitle';
import useDiscover from '@app/hooks/useDiscover'; import useDiscover from '@app/hooks/useDiscover';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import type { TvResult } from '@server/models/Search'; import type { TvResult } from '@server/models/Search';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.DiscoverTvUpcoming', {});
upcomingtv: 'Upcoming Series',
});
const DiscoverTvUpcoming = () => { const DiscoverTvUpcoming = () => {
const intl = useIntl(); const intl = useIntl();

View File

@@ -4,12 +4,13 @@ import PageTitle from '@app/components/Common/PageTitle';
import useDiscover from '@app/hooks/useDiscover'; import useDiscover from '@app/hooks/useDiscover';
import { useUser } from '@app/hooks/useUser'; import { useUser } from '@app/hooks/useUser';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces'; import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.Discover.DiscoverWatchlist', {
discoverwatchlist: 'Your Watchlist', discoverwatchlist: 'Your Watchlist',
watchlist: 'Plex Watchlist', watchlist: 'Plex Watchlist',
}); });
@@ -58,8 +59,8 @@ const DiscoverWatchlist = () => {
<Header <Header
subtext={ subtext={
router.query.userId ? ( router.query.userId ? (
<Link href={`/users/${user?.id}`}> <Link href={`/users/${user?.id}`} className="hover:underline">
<a className="hover:underline">{user?.displayName}</a> {user?.displayName}
</Link> </Link>
) : ( ) : (
'' ''

View File

@@ -15,11 +15,12 @@ import {
useBatchUpdateQueryParams, useBatchUpdateQueryParams,
useUpdateQueryParams, useUpdateQueryParams,
} from '@app/hooks/useUpdateQueryParams'; } from '@app/hooks/useUpdateQueryParams';
import defineMessages from '@app/utils/defineMessages';
import { XCircleIcon } from '@heroicons/react/24/outline'; import { XCircleIcon } from '@heroicons/react/24/outline';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import Datepicker from 'react-tailwindcss-datepicker-sct'; import Datepicker from 'react-tailwindcss-datepicker-sct';
const messages = defineMessages({ const messages = defineMessages('components.Discover.FilterSlideover', {
filters: 'Filters', filters: 'Filters',
activefilters: activefilters:
'{count, plural, one {# Active Filter} other {# Active Filters}}', '{count, plural, one {# Active Filter} other {# Active Filters}}',

View File

@@ -4,11 +4,12 @@ import PageTitle from '@app/components/Common/PageTitle';
import { genreColorMap } from '@app/components/Discover/constants'; import { genreColorMap } from '@app/components/Discover/constants';
import GenreCard from '@app/components/GenreCard'; import GenreCard from '@app/components/GenreCard';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces'; import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.Discover.MovieGenreList', {
moviegenres: 'Movie Genres', moviegenres: 'Movie Genres',
}); });

View File

@@ -1,14 +1,15 @@
import { genreColorMap } from '@app/components/Discover/constants'; import { genreColorMap } from '@app/components/Discover/constants';
import GenreCard from '@app/components/GenreCard'; import GenreCard from '@app/components/GenreCard';
import Slider from '@app/components/Slider'; import Slider from '@app/components/Slider';
import defineMessages from '@app/utils/defineMessages';
import { ArrowRightCircleIcon } from '@heroicons/react/24/outline'; import { ArrowRightCircleIcon } from '@heroicons/react/24/outline';
import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces'; import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
import Link from 'next/link'; import Link from 'next/link';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.Discover.MovieGenreSlider', {
moviegenres: 'Movie Genres', moviegenres: 'Movie Genres',
}); });
@@ -25,11 +26,9 @@ const MovieGenreSlider = () => {
return ( return (
<> <>
<div className="slider-header"> <div className="slider-header">
<Link href="/discover/movies/genres"> <Link href="/discover/movies/genres" className="slider-title">
<a className="slider-title"> <span>{intl.formatMessage(messages.moviegenres)}</span>
<span>{intl.formatMessage(messages.moviegenres)}</span> <ArrowRightCircleIcon />
<ArrowRightCircleIcon />
</a>
</Link> </Link>
</div> </div>
<Slider <Slider

View File

@@ -1,8 +1,9 @@
import CompanyCard from '@app/components/CompanyCard'; import CompanyCard from '@app/components/CompanyCard';
import Slider from '@app/components/Slider'; import Slider from '@app/components/Slider';
import { defineMessages, useIntl } from 'react-intl'; import defineMessages from '@app/utils/defineMessages';
import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.Discover.NetworkSlider', {
networks: 'Networks', networks: 'Networks',
}); });

View File

@@ -1,13 +1,14 @@
import Slider from '@app/components/Slider'; import Slider from '@app/components/Slider';
import TmdbTitleCard from '@app/components/TitleCard/TmdbTitleCard'; import TmdbTitleCard from '@app/components/TitleCard/TmdbTitleCard';
import { useUser } from '@app/hooks/useUser'; import { useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages';
import { ArrowRightCircleIcon } from '@heroicons/react/24/outline'; import { ArrowRightCircleIcon } from '@heroicons/react/24/outline';
import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces'; import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces';
import Link from 'next/link'; import Link from 'next/link';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.Discover.PlexWatchlistSlider', {
plexwatchlist: 'Your Watchlist', plexwatchlist: 'Your Watchlist',
emptywatchlist: emptywatchlist:
'Media added to your <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> will appear here.', 'Media added to your <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> will appear here.',
@@ -39,11 +40,9 @@ const PlexWatchlistSlider = () => {
return ( return (
<> <>
<div className="slider-header"> <div className="slider-header">
<Link href="/discover/watchlist"> <Link href="/discover/watchlist" className="slider-title">
<a className="slider-title"> <span>{intl.formatMessage(messages.plexwatchlist)}</span>
<span>{intl.formatMessage(messages.plexwatchlist)}</span> <ArrowRightCircleIcon />
<ArrowRightCircleIcon />
</a>
</Link> </Link>
</div> </div>
<Slider <Slider

View File

@@ -24,11 +24,9 @@ const RecentRequestsSlider = () => {
return ( return (
<> <>
<div className="slider-header"> <div className="slider-header">
<Link href="/requests?filter=all"> <Link href="/requests?filter=all" className="slider-title">
<a className="slider-title"> <span>{intl.formatMessage(sliderTitles.recentrequests)}</span>
<span>{intl.formatMessage(sliderTitles.recentrequests)}</span> <ArrowRightCircleIcon />
<ArrowRightCircleIcon />
</a>
</Link> </Link>
</div> </div>
<Slider <Slider

View File

@@ -1,11 +1,12 @@
import Slider from '@app/components/Slider'; import Slider from '@app/components/Slider';
import TmdbTitleCard from '@app/components/TitleCard/TmdbTitleCard'; import TmdbTitleCard from '@app/components/TitleCard/TmdbTitleCard';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages';
import type { MediaResultsResponse } from '@server/interfaces/api/mediaInterfaces'; import type { MediaResultsResponse } from '@server/interfaces/api/mediaInterfaces';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.Discover.RecentlyAddedSlider', {
recentlyAdded: 'Recently Added', recentlyAdded: 'Recently Added',
}); });

View File

@@ -1,8 +1,9 @@
import CompanyCard from '@app/components/CompanyCard'; import CompanyCard from '@app/components/CompanyCard';
import Slider from '@app/components/Slider'; import Slider from '@app/components/Slider';
import { defineMessages, useIntl } from 'react-intl'; import defineMessages from '@app/utils/defineMessages';
import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.Discover.StudioSlider', {
studios: 'Studios', studios: 'Studios',
}); });

View File

@@ -3,14 +3,15 @@ import ListView from '@app/components/Common/ListView';
import PageTitle from '@app/components/Common/PageTitle'; import PageTitle from '@app/components/Common/PageTitle';
import useDiscover from '@app/hooks/useDiscover'; import useDiscover from '@app/hooks/useDiscover';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import type { import type {
MovieResult, MovieResult,
PersonResult, PersonResult,
TvResult, TvResult,
} from '@server/models/Search'; } from '@server/models/Search';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.Discover', {
trending: 'Trending', trending: 'Trending',
}); });

View File

@@ -4,11 +4,12 @@ import PageTitle from '@app/components/Common/PageTitle';
import { genreColorMap } from '@app/components/Discover/constants'; import { genreColorMap } from '@app/components/Discover/constants';
import GenreCard from '@app/components/GenreCard'; import GenreCard from '@app/components/GenreCard';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces'; import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.Discover.TvGenreList', {
seriesgenres: 'Series Genres', seriesgenres: 'Series Genres',
}); });

View File

@@ -1,14 +1,15 @@
import { genreColorMap } from '@app/components/Discover/constants'; import { genreColorMap } from '@app/components/Discover/constants';
import GenreCard from '@app/components/GenreCard'; import GenreCard from '@app/components/GenreCard';
import Slider from '@app/components/Slider'; import Slider from '@app/components/Slider';
import defineMessages from '@app/utils/defineMessages';
import { ArrowRightCircleIcon } from '@heroicons/react/24/outline'; import { ArrowRightCircleIcon } from '@heroicons/react/24/outline';
import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces'; import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
import Link from 'next/link'; import Link from 'next/link';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.Discover.TvGenreSlider', {
tvgenres: 'Series Genres', tvgenres: 'Series Genres',
}); });
@@ -25,11 +26,9 @@ const TvGenreSlider = () => {
return ( return (
<> <>
<div className="slider-header"> <div className="slider-header">
<Link href="/discover/tv/genres"> <Link href="/discover/tv/genres" className="slider-title">
<a className="slider-title"> <span>{intl.formatMessage(messages.tvgenres)}</span>
<span>{intl.formatMessage(messages.tvgenres)}</span> <ArrowRightCircleIcon />
<ArrowRightCircleIcon />
</a>
</Link> </Link>
</div> </div>
<Slider <Slider

View File

@@ -3,10 +3,11 @@ import ListView from '@app/components/Common/ListView';
import PageTitle from '@app/components/Common/PageTitle'; import PageTitle from '@app/components/Common/PageTitle';
import useDiscover from '@app/hooks/useDiscover'; import useDiscover from '@app/hooks/useDiscover';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import type { MovieResult } from '@server/models/Search'; import type { MovieResult } from '@server/models/Search';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.Discover', {
upcomingmovies: 'Upcoming Movies', upcomingmovies: 'Upcoming Movies',
}); });

View File

@@ -1,5 +1,5 @@
import defineMessages from '@app/utils/defineMessages';
import type { ParsedUrlQuery } from 'querystring'; import type { ParsedUrlQuery } from 'querystring';
import { defineMessages } from 'react-intl';
import { z } from 'zod'; import { z } from 'zod';
type AvailableColors = type AvailableColors =
@@ -66,7 +66,7 @@ export const genreColorMap: Record<number, [string, string]> = {
10768: colorTones.darkred, // War & Politics 10768: colorTones.darkred, // War & Politics
}; };
export const sliderTitles = defineMessages({ export const sliderTitles = defineMessages('components.Discover', {
recentrequests: 'Recent Requests', recentrequests: 'Recent Requests',
popularmovies: 'Popular Movies', popularmovies: 'Popular Movies',
populartv: 'Popular Series', populartv: 'Popular Series',

View File

@@ -17,6 +17,7 @@ import MediaSlider from '@app/components/MediaSlider';
import { encodeURIExtraParams } from '@app/hooks/useDiscover'; import { encodeURIExtraParams } from '@app/hooks/useDiscover';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react'; import { Transition } from '@headlessui/react';
import { import {
ArrowDownOnSquareIcon, ArrowDownOnSquareIcon,
@@ -29,11 +30,11 @@ import { DiscoverSliderType } from '@server/constants/discover';
import type DiscoverSlider from '@server/entity/DiscoverSlider'; import type DiscoverSlider from '@server/entity/DiscoverSlider';
import axios from 'axios'; import axios from 'axios';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.Discover', {
discover: 'Discover', discover: 'Discover',
emptywatchlist: emptywatchlist:
'Media added to your <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> will appear here.', 'Media added to your <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> will appear here.',

View File

@@ -1,9 +1,10 @@
import Badge from '@app/components/Common/Badge'; import Badge from '@app/components/Common/Badge';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages';
import type { DownloadingItem } from '@server/lib/downloadtracker'; import type { DownloadingItem } from '@server/lib/downloadtracker';
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl'; import { FormattedRelativeTime, useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.DownloadBlock', {
estimatedtime: 'Estimated {time}', estimatedtime: 'Estimated {time}',
formattedTitle: '{title}: Season {seasonNumber} Episode {episodeNumber}', formattedTitle: '{title}: Season {seasonNumber} Episode {episodeNumber}',
}); });

View File

@@ -14,37 +14,41 @@ const GenreCard = ({ image, url, name, canExpand = false }: GenreCardProps) => {
const [isHovered, setHovered] = useState(false); const [isHovered, setHovered] = useState(false);
return ( return (
<Link href={url}> <Link
<a href={url}
className={`relative flex h-32 items-center justify-center sm:h-36 ${ className={`relative flex h-32 items-center justify-center sm:h-36 ${
canExpand ? 'w-full' : 'w-56 sm:w-72' canExpand ? 'w-full' : 'w-56 sm:w-72'
} transform-gpu cursor-pointer p-8 shadow ring-1 transition duration-300 ease-in-out ${ } transform-gpu cursor-pointer p-8 shadow ring-1 transition duration-300 ease-in-out ${
isHovered isHovered
? 'scale-105 bg-gray-700 bg-opacity-100 ring-gray-500' ? 'scale-105 bg-gray-700 bg-opacity-100 ring-gray-500'
: 'scale-100 bg-gray-800 bg-opacity-80 ring-gray-700' : 'scale-100 bg-gray-800 bg-opacity-80 ring-gray-700'
} overflow-hidden rounded-xl bg-cover bg-center`} } overflow-hidden rounded-xl bg-cover bg-center`}
onMouseEnter={() => { onMouseEnter={() => {
setHovered(true);
}}
onMouseLeave={() => setHovered(false)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
setHovered(true); setHovered(true);
}} }
onMouseLeave={() => setHovered(false)} }}
onKeyDown={(e) => { role="link"
if (e.key === 'Enter') { tabIndex={0}
setHovered(true); >
} <CachedImage
}} src={image}
role="link" alt=""
tabIndex={0} style={{ width: '100%', height: '100%', objectFit: 'cover' }}
> fill
<CachedImage src={image} alt="" layout="fill" objectFit="cover" /> />
<div <div
className={`absolute inset-0 z-10 h-full w-full bg-gray-800 transition duration-300 ${ className={`absolute inset-0 z-10 h-full w-full bg-gray-800 transition duration-300 ${
isHovered ? 'bg-opacity-10' : 'bg-opacity-30' isHovered ? 'bg-opacity-10' : 'bg-opacity-30'
}`} }`}
/> />
<div className="relative z-20 w-full truncate whitespace-normal text-center text-2xl font-bold text-white sm:text-3xl"> <div className="relative z-20 w-full truncate whitespace-normal text-center text-2xl font-bold text-white sm:text-3xl">
{name} {name}
</div> </div>
</a>
</Link> </Link>
); );
}; };

View File

@@ -45,10 +45,9 @@ const IssueBlock = ({ issue }: IssueBlockProps) => {
? '/profile' ? '/profile'
: `/users/${issue.createdBy.id}` : `/users/${issue.createdBy.id}`
} }
className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline"
> >
<a className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline"> {issue.createdBy.displayName}
{issue.createdBy.displayName}
</a>
</Link> </Link>
</span> </span>
</div> </div>
@@ -64,7 +63,7 @@ const IssueBlock = ({ issue }: IssueBlockProps) => {
</div> </div>
</div> </div>
<div className="ml-2 flex flex-shrink-0 flex-wrap"> <div className="ml-2 flex flex-shrink-0 flex-wrap">
<Link href={`/issues/${issue.id}`} passHref> <Link href={`/issues/${issue.id}`} passHref legacyBehavior>
<Button buttonType="primary" as="a"> <Button buttonType="primary" as="a">
<EyeIcon /> <EyeIcon />
</Button> </Button>

View File

@@ -1,18 +1,20 @@
import Button from '@app/components/Common/Button'; import Button from '@app/components/Common/Button';
import Modal from '@app/components/Common/Modal'; import Modal from '@app/components/Common/Modal';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages';
import { Menu, Transition } from '@headlessui/react'; import { Menu, Transition } from '@headlessui/react';
import { EllipsisVerticalIcon } from '@heroicons/react/24/solid'; import { EllipsisVerticalIcon } from '@heroicons/react/24/solid';
import type { default as IssueCommentType } from '@server/entity/IssueComment'; import type { default as IssueCommentType } from '@server/entity/IssueComment';
import axios from 'axios'; import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { Fragment, useState } from 'react'; import { Fragment, useState } from 'react';
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl'; import { FormattedRelativeTime, useIntl } from 'react-intl';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import * as Yup from 'yup'; import * as Yup from 'yup';
const messages = defineMessages({ const messages = defineMessages('components.IssueDetails.IssueComment', {
postedby: 'Posted {relativeTime} by {username}', postedby: 'Posted {relativeTime} by {username}',
postedbyedited: 'Posted {relativeTime} by {username} (Edited)', postedbyedited: 'Posted {relativeTime} by {username} (Edited)',
delete: 'Delete Comment', delete: 'Delete Comment',
@@ -84,13 +86,13 @@ const IssueComment = ({
</Modal> </Modal>
</Transition> </Transition>
<Link href={isActiveUser ? '/profile' : `/users/${comment.user.id}`}> <Link href={isActiveUser ? '/profile' : `/users/${comment.user.id}`}>
<a> <Image
<img src={comment.user.avatar}
src={comment.user.avatar} alt=""
alt="" className="h-10 w-10 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
className="h-10 w-10 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105" width={40}
/> height={40}
</a> />
</Link> </Link>
<div className="relative flex-1"> <div className="relative flex-1">
<div className="w-full rounded-md shadow ring-1 ring-gray-500"> <div className="w-full rounded-md shadow ring-1 ring-gray-500">
@@ -242,10 +244,9 @@ const IssueComment = ({
href={ href={
isActiveUser ? '/profile' : `/users/${comment.user.id}` isActiveUser ? '/profile' : `/users/${comment.user.id}`
} }
className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline"
> >
<a className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline"> {comment.user.displayName}
{comment.user.displayName}
</a>
</Link> </Link>
), ),
relativeTime: ( relativeTime: (

View File

@@ -1,14 +1,15 @@
import Button from '@app/components/Common/Button'; import Button from '@app/components/Common/Button';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { Menu, Transition } from '@headlessui/react'; import { Menu, Transition } from '@headlessui/react';
import { EllipsisVerticalIcon } from '@heroicons/react/24/solid'; import { EllipsisVerticalIcon } from '@heroicons/react/24/solid';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import { useState } from 'react'; import { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
const messages = defineMessages({ const messages = defineMessages('components.IssueDetails.IssueDescription', {
description: 'Description', description: 'Description',
edit: 'Edit Description', edit: 'Edit Description',
deleteissue: 'Delete Issue', deleteissue: 'Delete Issue',

View File

@@ -12,6 +12,7 @@ import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react'; import { Transition } from '@headlessui/react';
import { import {
ChatBubbleOvalLeftEllipsisIcon, ChatBubbleOvalLeftEllipsisIcon,
@@ -29,15 +30,16 @@ import type { TvDetails } from '@server/models/Tv';
import axios from 'axios'; import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import getConfig from 'next/config'; import getConfig from 'next/config';
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useState } from 'react'; import { useState } from 'react';
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl'; import { FormattedRelativeTime, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr'; import useSWR from 'swr';
import * as Yup from 'yup'; import * as Yup from 'yup';
const messages = defineMessages({ const messages = defineMessages('components.IssueDetails', {
openedby: '#{issueId} opened {relativeTime} by {username}', openedby: '#{issueId} opened {relativeTime} by {username}',
closeissue: 'Close Issue', closeissue: 'Close Issue',
closeissueandcomment: 'Close with Comment', closeissueandcomment: 'Close with Comment',
@@ -210,8 +212,8 @@ const IssueDetails = () => {
<CachedImage <CachedImage
alt="" alt=""
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`} src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`}
layout="fill" style={{ width: '100%', height: '100%', objectFit: 'cover' }}
objectFit="cover" fill
priority priority
/> />
<div <div
@@ -232,7 +234,8 @@ const IssueDetails = () => {
: '/images/overseerr_poster_not_found.png' : '/images/overseerr_poster_not_found.png'
} }
alt="" alt=""
layout="responsive" sizes="100vw"
style={{ width: '100%', height: 'auto' }}
width={600} width={600}
height={900} height={900}
priority priority
@@ -256,8 +259,9 @@ const IssueDetails = () => {
href={`/${ href={`/${
issueData.media.mediaType === MediaType.MOVIE ? 'movie' : 'tv' issueData.media.mediaType === MediaType.MOVIE ? 'movie' : 'tv'
}/${data.id}`} }/${data.id}`}
className="hover:underline"
> >
<a className="hover:underline">{title}</a> {title}
</Link>{' '} </Link>{' '}
{releaseYear && ( {releaseYear && (
<span className="media-year">({releaseYear.slice(0, 4)})</span> <span className="media-year">({releaseYear.slice(0, 4)})</span>
@@ -273,17 +277,18 @@ const IssueDetails = () => {
? '/profile' ? '/profile'
: `/users/${issueData.createdBy.id}` : `/users/${issueData.createdBy.id}`
} }
className="group ml-1 inline-flex h-full items-center xl:ml-1.5"
> >
<a className="group ml-1 inline-flex h-full items-center xl:ml-1.5"> <Image
<img className="mr-0.5 h-5 w-5 scale-100 transform-gpu rounded-full object-cover transition duration-300 group-hover:scale-105 xl:mr-1 xl:h-6 xl:w-6"
className="mr-0.5 h-5 w-5 scale-100 transform-gpu rounded-full object-cover transition duration-300 group-hover:scale-105 xl:mr-1 xl:h-6 xl:w-6" src={issueData.createdBy.avatar}
src={issueData.createdBy.avatar} alt=""
alt="" width={20}
/> height={20}
<span className="font-semibold text-gray-100 transition duration-300 group-hover:text-white group-hover:underline"> />
{issueData.createdBy.displayName} <span className="font-semibold text-gray-100 transition duration-300 group-hover:text-white group-hover:underline">
</span> {issueData.createdBy.displayName}
</a> </span>
</Link> </Link>
), ),
relativeTime: ( relativeTime: (

View File

@@ -4,18 +4,20 @@ import CachedImage from '@app/components/Common/CachedImage';
import { issueOptions } from '@app/components/IssueModal/constants'; import { issueOptions } from '@app/components/IssueModal/constants';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { EyeIcon } from '@heroicons/react/24/solid'; import { EyeIcon } from '@heroicons/react/24/solid';
import { IssueStatus } from '@server/constants/issue'; import { IssueStatus } from '@server/constants/issue';
import { MediaType } from '@server/constants/media'; import { MediaType } from '@server/constants/media';
import type Issue from '@server/entity/Issue'; import type Issue from '@server/entity/Issue';
import type { MovieDetails } from '@server/models/Movie'; import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv'; import type { TvDetails } from '@server/models/Tv';
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useInView } from 'react-intersection-observer'; import { useInView } from 'react-intersection-observer';
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl'; import { FormattedRelativeTime, useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.IssueList.IssueItem', {
openeduserdate: '{date} by {user}', openeduserdate: '{date} by {user}',
seasons: '{seasonCount, plural, one {Season} other {Seasons}}', seasons: '{seasonCount, plural, one {Season} other {Seasons}}',
episodes: '{episodeCount, plural, one {Episode} other {Episodes}}', episodes: '{episodeCount, plural, one {Episode} other {Episodes}}',
@@ -113,8 +115,8 @@ const IssueItem = ({ issue }: IssueItemProps) => {
<CachedImage <CachedImage
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`} src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
alt="" alt=""
layout="fill" style={{ width: '100%', height: '100%', objectFit: 'cover' }}
objectFit="cover" fill
/> />
<div <div
className="absolute inset-0" className="absolute inset-0"
@@ -133,21 +135,20 @@ const IssueItem = ({ issue }: IssueItemProps) => {
? `/movie/${issue.media.tmdbId}` ? `/movie/${issue.media.tmdbId}`
: `/tv/${issue.media.tmdbId}` : `/tv/${issue.media.tmdbId}`
} }
className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105"
> >
<a className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105"> <CachedImage
<CachedImage src={
src={ title.posterPath
title.posterPath ? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}` : '/images/overseerr_poster_not_found.png'
: '/images/overseerr_poster_not_found.png' }
} alt=""
alt="" sizes="100vw"
layout="responsive" style={{ width: '100%', height: 'auto', objectFit: 'cover' }}
width={600} width={600}
height={900} height={900}
objectFit="cover" />
/>
</a>
</Link> </Link>
<div className="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4"> <div className="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
<div className="pt-0.5 text-xs text-white sm:pt-1"> <div className="pt-0.5 text-xs text-white sm:pt-1">
@@ -162,10 +163,9 @@ const IssueItem = ({ issue }: IssueItemProps) => {
? `/movie/${issue.media.tmdbId}` ? `/movie/${issue.media.tmdbId}`
: `/tv/${issue.media.tmdbId}` : `/tv/${issue.media.tmdbId}`
} }
className="mr-2 min-w-0 truncate text-lg font-bold text-white hover:underline xl:text-xl"
> >
<a className="mr-2 min-w-0 truncate text-lg font-bold text-white hover:underline xl:text-xl"> {isMovie(title) ? title.title : title.name}
{isMovie(title) ? title.title : title.name}
</a>
</Link> </Link>
{problemSeasonEpisodeLine.length > 0 && ( {problemSeasonEpisodeLine.length > 0 && (
<div className="card-field"> <div className="card-field">
@@ -222,17 +222,20 @@ const IssueItem = ({ issue }: IssueItemProps) => {
/> />
), ),
user: ( user: (
<Link href={`/users/${issue.createdBy.id}`}> <Link
<a className="group flex items-center truncate"> href={`/users/${issue.createdBy.id}`}
<img className="group flex items-center truncate"
src={issue.createdBy.avatar} >
alt="" <Image
className="avatar-sm ml-1.5 object-cover" src={issue.createdBy.avatar}
/> alt=""
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline"> className="avatar-sm ml-1.5 object-cover"
{issue.createdBy.displayName} width={20}
</span> height={20}
</a> />
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
{issue.createdBy.displayName}
</span>
</Link> </Link>
), ),
})} })}
@@ -259,7 +262,7 @@ const IssueItem = ({ issue }: IssueItemProps) => {
</div> </div>
<div className="z-10 mt-4 flex w-full flex-col justify-center pl-4 pr-4 xl:mt-0 xl:w-96 xl:items-end xl:pl-0"> <div className="z-10 mt-4 flex w-full flex-col justify-center pl-4 pr-4 xl:mt-0 xl:w-96 xl:items-end xl:pl-0">
<span className="w-full"> <span className="w-full">
<Link href={`/issues/${issue.id}`} passHref> <Link href={`/issues/${issue.id}`} passHref legacyBehavior>
<Button as="a" className="w-full" buttonType="primary"> <Button as="a" className="w-full" buttonType="primary">
<EyeIcon /> <EyeIcon />
<span>{intl.formatMessage(messages.viewissue)}</span> <span>{intl.formatMessage(messages.viewissue)}</span>

View File

@@ -5,6 +5,7 @@ import PageTitle from '@app/components/Common/PageTitle';
import IssueItem from '@app/components/IssueList/IssueItem'; import IssueItem from '@app/components/IssueList/IssueItem';
import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams'; import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { import {
BarsArrowDownIcon, BarsArrowDownIcon,
ChevronLeftIcon, ChevronLeftIcon,
@@ -14,10 +15,10 @@ import {
import type { IssueResultsResponse } from '@server/interfaces/api/issueInterfaces'; import type { IssueResultsResponse } from '@server/interfaces/api/issueInterfaces';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.IssueList', {
issues: 'Issues', issues: 'Issues',
sortAdded: 'Most Recent', sortAdded: 'Most Recent',
sortModified: 'Last Modified', sortModified: 'Last Modified',

View File

@@ -4,6 +4,7 @@ import { issueOptions } from '@app/components/IssueModal/constants';
import useSettings from '@app/hooks/useSettings'; import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { RadioGroup } from '@headlessui/react'; import { RadioGroup } from '@headlessui/react';
import { ArrowRightCircleIcon } from '@heroicons/react/24/solid'; import { ArrowRightCircleIcon } from '@heroicons/react/24/solid';
import { MediaStatus } from '@server/constants/media'; import { MediaStatus } from '@server/constants/media';
@@ -13,12 +14,12 @@ import type { TvDetails } from '@server/models/Tv';
import axios from 'axios'; import axios from 'axios';
import { Field, Formik } from 'formik'; import { Field, Formik } from 'formik';
import Link from 'next/link'; import Link from 'next/link';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr'; import useSWR from 'swr';
import * as Yup from 'yup'; import * as Yup from 'yup';
const messages = defineMessages({ const messages = defineMessages('components.IssueModal.CreateIssueModal', {
validationMessageRequired: 'You must provide a description', validationMessageRequired: 'You must provide a description',
whatswrong: "What's wrong?", whatswrong: "What's wrong?",
providedetail: providedetail:
@@ -118,7 +119,7 @@ const CreateIssueModal = ({
strong: (msg: React.ReactNode) => <strong>{msg}</strong>, strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})} })}
</div> </div>
<Link href={`/issues/${newIssue.data.id}`}> <Link href={`/issues/${newIssue.data.id}`} legacyBehavior>
<Button as="a" className="mt-4"> <Button as="a" className="mt-4">
<span>{intl.formatMessage(messages.toastviewissue)}</span> <span>{intl.formatMessage(messages.toastviewissue)}</span>
<ArrowRightCircleIcon /> <ArrowRightCircleIcon />

View File

@@ -1,8 +1,8 @@
import defineMessages from '@app/utils/defineMessages';
import { IssueType } from '@server/constants/issue'; import { IssueType } from '@server/constants/issue';
import type { MessageDescriptor } from 'react-intl'; import type { MessageDescriptor } from 'react-intl';
import { defineMessages } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.IssueModal', {
issueAudio: 'Audio', issueAudio: 'Audio',
issueVideo: 'Video', issueVideo: 'Video',
issueSubtitles: 'Subtitle', issueSubtitles: 'Subtitle',

View File

@@ -1,13 +1,14 @@
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import type { Language } from '@server/lib/settings'; import type { Language } from '@server/lib/settings';
import { sortBy } from 'lodash'; import { sortBy } from 'lodash';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import type { CSSObjectWithLabel } from 'react-select'; import type { CSSObjectWithLabel } from 'react-select';
import Select from 'react-select'; import Select from 'react-select';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.LanguageSelector', {
originalLanguageDefault: 'All Languages', originalLanguageDefault: 'All Languages',
languageServerDefault: 'Default ({language})', languageServerDefault: 'Default ({language})',
}); });

View File

@@ -2,12 +2,13 @@ import type { AvailableLocale } from '@app/context/LanguageContext';
import { availableLanguages } from '@app/context/LanguageContext'; import { availableLanguages } from '@app/context/LanguageContext';
import useClickOutside from '@app/hooks/useClickOutside'; import useClickOutside from '@app/hooks/useClickOutside';
import useLocale from '@app/hooks/useLocale'; import useLocale from '@app/hooks/useLocale';
import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react'; import { Transition } from '@headlessui/react';
import { LanguageIcon } from '@heroicons/react/24/solid'; import { LanguageIcon } from '@heroicons/react/24/solid';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.Layout.LanguagePicker', {
displaylanguage: 'Display Language', displaylanguage: 'Display Language',
}); });

View File

@@ -142,25 +142,25 @@ const MobileMenu = () => {
{filteredLinks.map((link) => { {filteredLinks.map((link) => {
const isActive = router.pathname.match(link.activeRegExp); const isActive = router.pathname.match(link.activeRegExp);
return ( return (
<Link key={`mobile-menu-link-${link.href}`} href={link.href}> <Link
<a key={`mobile-menu-link-${link.href}`}
className={`flex items-center space-x-2 ${ href={link.href}
isActive ? 'text-indigo-500' : '' className={`flex items-center space-x-2 ${
}`} isActive ? 'text-indigo-500' : ''
onKeyDown={(e) => { }`}
if (e.key === 'Enter') { onKeyDown={(e) => {
setIsOpen(false); if (e.key === 'Enter') {
} setIsOpen(false);
}} }
onClick={() => setIsOpen(false)} }}
role="button" onClick={() => setIsOpen(false)}
tabIndex={0} role="button"
> tabIndex={0}
{cloneElement(isActive ? link.svgIconSelected : link.svgIcon, { >
className: 'h-5 w-5', {cloneElement(isActive ? link.svgIconSelected : link.svgIcon, {
})} className: 'h-5 w-5',
<span>{link.content}</span> })}
</a> <span>{link.content}</span>
</Link> </Link>
); );
})} })}
@@ -173,19 +173,19 @@ const MobileMenu = () => {
const isActive = const isActive =
router.pathname.match(link.activeRegExp) && !isOpen; router.pathname.match(link.activeRegExp) && !isOpen;
return ( return (
<Link key={`mobile-menu-link-${link.href}`} href={link.href}> <Link
<a key={`mobile-menu-link-${link.href}`}
className={`flex flex-col items-center space-y-1 ${ href={link.href}
isActive ? 'text-indigo-500' : '' className={`flex flex-col items-center space-y-1 ${
}`} isActive ? 'text-indigo-500' : ''
> }`}
{cloneElement( >
isActive ? link.svgIconSelected : link.svgIcon, {cloneElement(
{ isActive ? link.svgIconSelected : link.svgIcon,
className: 'h-6 w-6', {
} className: 'h-6 w-6',
)} }
</a> )}
</Link> </Link>
); );
})} })}

View File

@@ -1,9 +1,10 @@
import useSearchInput from '@app/hooks/useSearchInput'; import useSearchInput from '@app/hooks/useSearchInput';
import defineMessages from '@app/utils/defineMessages';
import { XCircleIcon } from '@heroicons/react/24/outline'; import { XCircleIcon } from '@heroicons/react/24/outline';
import { MagnifyingGlassIcon } from '@heroicons/react/24/solid'; import { MagnifyingGlassIcon } from '@heroicons/react/24/solid';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.Layout.SearchInput', {
searchPlaceholder: 'Search Movies & TV', searchPlaceholder: 'Search Movies & TV',
}); });

View File

@@ -2,6 +2,7 @@ import UserWarnings from '@app/components/Layout/UserWarnings';
import VersionStatus from '@app/components/Layout/VersionStatus'; import VersionStatus from '@app/components/Layout/VersionStatus';
import useClickOutside from '@app/hooks/useClickOutside'; import useClickOutside from '@app/hooks/useClickOutside';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react'; import { Transition } from '@headlessui/react';
import { import {
ClockIcon, ClockIcon,
@@ -13,12 +14,13 @@ import {
UsersIcon, UsersIcon,
XMarkIcon, XMarkIcon,
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { Fragment, useRef } from 'react'; import { Fragment, useRef } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
export const menuMessages = defineMessages({ export const menuMessages = defineMessages('components.Layout.Sidebar', {
dashboard: 'Discover', dashboard: 'Discover',
browsemovies: 'Movies', browsemovies: 'Movies',
browsetv: 'Series', browsetv: 'Series',
@@ -146,16 +148,16 @@ const Sidebar = ({ open, setClosed }: SidebarProps) => {
</div> </div>
<div <div
ref={navRef} ref={navRef}
className="flex flex-1 flex-col overflow-y-auto pt-8 pb-8 sm:pb-4" className="flex flex-1 flex-col overflow-y-auto pt-4 pb-8 sm:pb-4"
> >
<div className="flex flex-shrink-0 items-center px-2"> <div className="flex flex-shrink-0 items-center px-2">
<span className="px-4 text-xl text-gray-50"> <span className="w-full px-4 text-xl text-gray-50">
<a href="/"> <Link href="/" className="relative block h-24 w-64">
<img src="/logo_full.svg" alt="Logo" /> <Image src="/logo_full.svg" alt="Logo" fill />
</a> </Link>
</span> </span>
</div> </div>
<nav className="mt-16 flex-1 space-y-4 px-4"> <nav className="mt-10 flex-1 space-y-4 px-4">
{SidebarLinks.filter((link) => {SidebarLinks.filter((link) =>
link.requiredPermission link.requiredPermission
? hasPermission(link.requiredPermission, { ? hasPermission(link.requiredPermission, {
@@ -168,32 +170,27 @@ const Sidebar = ({ open, setClosed }: SidebarProps) => {
key={`mobile-${sidebarLink.messagesKey}`} key={`mobile-${sidebarLink.messagesKey}`}
href={sidebarLink.href} href={sidebarLink.href}
as={sidebarLink.as} as={sidebarLink.as}
onClick={() => setClosed()}
onKeyDown={(e) => {
if (e.key === 'Enter') {
setClosed();
}
}}
role="button"
tabIndex={0}
className={`flex items-center rounded-md px-2 py-2 text-base font-medium leading-6 text-white transition duration-150 ease-in-out focus:outline-none
${
router.pathname.match(sidebarLink.activeRegExp)
? 'bg-gradient-to-br from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500'
: 'hover:bg-gray-700 focus:bg-gray-700'
}
`}
data-testid={`${sidebarLink.dataTestId}-mobile`}
> >
<a {sidebarLink.svgIcon}
onClick={() => setClosed()} {intl.formatMessage(
onKeyDown={(e) => { menuMessages[sidebarLink.messagesKey]
if (e.key === 'Enter') { )}
setClosed();
}
}}
role="button"
tabIndex={0}
className={`flex items-center rounded-md px-2 py-2 text-base font-medium leading-6 text-white transition duration-150 ease-in-out focus:outline-none
${
router.pathname.match(
sidebarLink.activeRegExp
)
? 'bg-gradient-to-br from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500'
: 'hover:bg-gray-700 focus:bg-gray-700'
}
`}
data-testid={`${sidebarLink.dataTestId}-mobile`}
>
{sidebarLink.svgIcon}
{intl.formatMessage(
menuMessages[sidebarLink.messagesKey]
)}
</a>
</Link> </Link>
); );
})} })}
@@ -221,15 +218,15 @@ const Sidebar = ({ open, setClosed }: SidebarProps) => {
<div className="fixed top-0 bottom-0 left-0 z-30 hidden lg:flex lg:flex-shrink-0"> <div className="fixed top-0 bottom-0 left-0 z-30 hidden lg:flex lg:flex-shrink-0">
<div className="sidebar flex w-64 flex-col"> <div className="sidebar flex w-64 flex-col">
<div className="flex h-0 flex-1 flex-col"> <div className="flex h-0 flex-1 flex-col">
<div className="flex flex-1 flex-col overflow-y-auto pt-8 pb-4"> <div className="flex flex-1 flex-col overflow-y-auto pb-4">
<div className="flex flex-shrink-0 items-center"> <div className="flex flex-shrink-0 items-center">
<span className="px-4 text-2xl text-gray-50"> <span className="w-full px-4 py-2 text-2xl text-gray-50">
<a href="/"> <Link href="/" className="relative block h-24">
<img src="/logo_full.svg" alt="Logo" /> <Image src="/logo_full.svg" alt="Logo" fill />
</a> </Link>
</span> </span>
</div> </div>
<nav className="mt-16 flex-1 space-y-4 px-4"> <nav className="mt-8 flex-1 space-y-4 px-4">
{SidebarLinks.filter((link) => {SidebarLinks.filter((link) =>
link.requiredPermission link.requiredPermission
? hasPermission(link.requiredPermission, { ? hasPermission(link.requiredPermission, {
@@ -242,24 +239,19 @@ const Sidebar = ({ open, setClosed }: SidebarProps) => {
key={`desktop-${sidebarLink.messagesKey}`} key={`desktop-${sidebarLink.messagesKey}`}
href={sidebarLink.href} href={sidebarLink.href}
as={sidebarLink.as} as={sidebarLink.as}
className={`group flex items-center rounded-md px-2 py-2 text-lg font-medium leading-6 text-white transition duration-150 ease-in-out focus:outline-none
${
router.pathname.match(sidebarLink.activeRegExp)
? 'bg-gradient-to-br from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500'
: 'hover:bg-gray-700 focus:bg-gray-700'
}
`}
data-testid={sidebarLink.dataTestId}
> >
<a {sidebarLink.svgIcon}
className={`group flex items-center rounded-md px-2 py-2 text-lg font-medium leading-6 text-white transition duration-150 ease-in-out focus:outline-none {intl.formatMessage(
${ menuMessages[sidebarLink.messagesKey]
router.pathname.match( )}
sidebarLink.activeRegExp
)
? 'bg-gradient-to-br from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500'
: 'hover:bg-gray-700 focus:bg-gray-700'
}
`}
data-testid={sidebarLink.dataTestId}
>
{sidebarLink.svgIcon}
{intl.formatMessage(
menuMessages[sidebarLink.messagesKey]
)}
</a>
</Link> </Link>
); );
})} })}

View File

@@ -1,14 +1,18 @@
import Infinity from '@app/assets/infinity.svg'; import Infinity from '@app/assets/infinity.svg';
import { SmallLoadingSpinner } from '@app/components/Common/LoadingSpinner'; import { SmallLoadingSpinner } from '@app/components/Common/LoadingSpinner';
import ProgressCircle from '@app/components/Common/ProgressCircle'; import ProgressCircle from '@app/components/Common/ProgressCircle';
import defineMessages from '@app/utils/defineMessages';
import type { QuotaResponse } from '@server/interfaces/api/userInterfaces'; import type { QuotaResponse } from '@server/interfaces/api/userInterfaces';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages(
movierequests: 'Movie Requests', 'components.Layout.UserDropdown.MiniQuotaDisplay',
seriesrequests: 'Series Requests', {
}); movierequests: 'Movie Requests',
seriesrequests: 'Series Requests',
}
);
type MiniQuotaDisplayProps = { type MiniQuotaDisplayProps = {
userId: number; userId: number;

View File

@@ -1,5 +1,6 @@
import MiniQuotaDisplay from '@app/components/Layout/UserDropdown/MiniQuotaDisplay'; import MiniQuotaDisplay from '@app/components/Layout/UserDropdown/MiniQuotaDisplay';
import { useUser } from '@app/hooks/useUser'; import { useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages';
import { Menu, Transition } from '@headlessui/react'; import { Menu, Transition } from '@headlessui/react';
import { import {
ArrowRightOnRectangleIcon, ArrowRightOnRectangleIcon,
@@ -7,12 +8,13 @@ import {
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import { CogIcon, UserIcon } from '@heroicons/react/24/solid'; import { CogIcon, UserIcon } from '@heroicons/react/24/solid';
import axios from 'axios'; import axios from 'axios';
import Image from 'next/image';
import type { LinkProps } from 'next/link'; import type { LinkProps } from 'next/link';
import Link from 'next/link'; import Link from 'next/link';
import { forwardRef, Fragment } from 'react'; import { forwardRef, Fragment } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.Layout.UserDropdown', {
myprofile: 'Profile', myprofile: 'Profile',
settings: 'Settings', settings: 'Settings',
requests: 'Requests', requests: 'Requests',
@@ -24,10 +26,8 @@ const ForwardedLink = forwardRef<
LinkProps & React.ComponentPropsWithoutRef<'a'> LinkProps & React.ComponentPropsWithoutRef<'a'>
>(({ href, children, ...rest }, ref) => { >(({ href, children, ...rest }, ref) => {
return ( return (
<Link href={href}> <Link href={href} ref={ref} {...rest}>
<a ref={ref} {...rest}> {children}
{children}
</a>
</Link> </Link>
); );
}); });
@@ -53,10 +53,12 @@ const UserDropdown = () => {
className="flex max-w-xs items-center rounded-full text-sm ring-1 ring-gray-700 hover:ring-gray-500 focus:outline-none focus:ring-gray-500" className="flex max-w-xs items-center rounded-full text-sm ring-1 ring-gray-700 hover:ring-gray-500 focus:outline-none focus:ring-gray-500"
data-testid="user-menu" data-testid="user-menu"
> >
<img <Image
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10" className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
src={user?.avatar} src={user?.avatar || ''}
alt="" alt=""
width={40}
height={40}
/> />
</Menu.Button> </Menu.Button>
</div> </div>
@@ -74,10 +76,12 @@ const UserDropdown = () => {
<div className="divide-y divide-gray-700 rounded-md bg-gray-800 bg-opacity-80 ring-1 ring-gray-700 backdrop-blur"> <div className="divide-y divide-gray-700 rounded-md bg-gray-800 bg-opacity-80 ring-1 ring-gray-700 backdrop-blur">
<div className="flex flex-col space-y-4 px-4 py-4"> <div className="flex flex-col space-y-4 px-4 py-4">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<img <Image
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10" className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
src={user?.avatar} src={user?.avatar || ''}
alt="" alt=""
width={40}
height={40}
/> />
<div className="flex min-w-0 flex-col"> <div className="flex min-w-0 flex-col">
<span className="truncate text-xl font-semibold text-gray-200"> <span className="truncate text-xl font-semibold text-gray-200">

View File

@@ -1,10 +1,10 @@
import { useUser } from '@app/hooks/useUser'; import { useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages';
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'; import { ExclamationTriangleIcon } from '@heroicons/react/24/outline';
import Link from 'next/link'; import Link from 'next/link';
import type React from 'react'; import { useIntl } from 'react-intl';
import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.Layout.UserWarnings', {
emailRequired: 'An email address is required.', emailRequired: 'An email address is required.',
emailInvalid: 'Email address is invalid.', emailInvalid: 'Email address is invalid.',
passwordRequired: 'A password is required.', passwordRequired: 'A password is required.',
@@ -37,24 +37,23 @@ const UserWarnings: React.FC<UserWarningsProps> = ({ onClick }) => {
} }
res = ( res = (
<Link href={link}> <Link
<a href={link}
onClick={onClick} onClick={onClick}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter' && onClick) { if (e.key === 'Enter' && onClick) {
onClick(); onClick();
} }
}} }}
role="button" role="button"
tabIndex={0} tabIndex={0}
className="mx-2 mb-2 flex items-center rounded-lg bg-yellow-500 p-2 text-xs text-white ring-1 ring-gray-700 transition duration-300 hover:bg-yellow-400" className="mx-2 mb-2 flex items-center rounded-lg bg-yellow-500 p-2 text-xs text-white ring-1 ring-gray-700 transition duration-300 hover:bg-yellow-400"
> >
<ExclamationTriangleIcon className="h-6 w-6" /> <ExclamationTriangleIcon className="h-6 w-6" />
<div className="flex min-w-0 flex-1 flex-col truncate px-2 last:pr-0"> <div className="flex min-w-0 flex-1 flex-col truncate px-2 last:pr-0">
<span className="font-bold">{warningTitle}</span> <span className="font-bold">{warningTitle}</span>
<span className="truncate">{warningText}</span> <span className="truncate">{warningText}</span>
</div> </div>
</a>
</Link> </Link>
); );
}); });

View File

@@ -1,3 +1,4 @@
import defineMessages from '@app/utils/defineMessages';
import { import {
ArrowUpCircleIcon, ArrowUpCircleIcon,
BeakerIcon, BeakerIcon,
@@ -6,10 +7,10 @@ import {
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import type { StatusResponse } from '@server/interfaces/api/settingsInterfaces'; import type { StatusResponse } from '@server/interfaces/api/settingsInterfaces';
import Link from 'next/link'; import Link from 'next/link';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.Layout.VersionStatus', {
streamdevelop: 'Jellyseerr Develop', streamdevelop: 'Jellyseerr Develop',
streamstable: 'Jellyseerr Stable', streamstable: 'Jellyseerr Stable',
outofdate: 'Out of Date', outofdate: 'Out of Date',
@@ -39,49 +40,48 @@ const VersionStatus = ({ onClick }: VersionStatusProps) => {
: intl.formatMessage(messages.streamstable); : intl.formatMessage(messages.streamstable);
return ( return (
<Link href="/settings/about"> <Link
<a href="/settings/about"
onClick={onClick} onClick={onClick}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter' && onClick) { if (e.key === 'Enter' && onClick) {
onClick(); onClick();
} }
}} }}
role="button" role="button"
tabIndex={0} tabIndex={0}
className={`mx-2 flex items-center rounded-lg p-2 text-xs ring-1 ring-gray-700 transition duration-300 ${ className={`mx-2 flex items-center rounded-lg p-2 text-xs ring-1 ring-gray-700 transition duration-300 ${
data.updateAvailable data.updateAvailable
? 'bg-yellow-500 text-white hover:bg-yellow-400' ? 'bg-yellow-500 text-white hover:bg-yellow-400'
: 'bg-gray-900 text-gray-300 hover:bg-gray-800' : 'bg-gray-900 text-gray-300 hover:bg-gray-800'
}`} }`}
> >
{data.commitTag === 'local' ? ( {data.commitTag === 'local' ? (
<CodeBracketIcon className="h-6 w-6" /> <CodeBracketIcon className="h-6 w-6" />
) : data.version.startsWith('develop-') ? ( ) : data.version.startsWith('develop-') ? (
<BeakerIcon className="h-6 w-6" /> <BeakerIcon className="h-6 w-6" />
) : ( ) : (
<ServerIcon className="h-6 w-6" /> <ServerIcon className="h-6 w-6" />
)} )}
<div className="flex min-w-0 flex-1 flex-col truncate px-2 last:pr-0"> <div className="flex min-w-0 flex-1 flex-col truncate px-2 last:pr-0">
<span className="font-bold">{versionStream}</span> <span className="font-bold">{versionStream}</span>
<span className="truncate"> <span className="truncate">
{data.commitTag === 'local' ? ( {data.commitTag === 'local' ? (
'(⌐■_■)' '(⌐■_■)'
) : data.commitsBehind > 0 ? ( ) : data.commitsBehind > 0 ? (
intl.formatMessage(messages.commitsbehind, { intl.formatMessage(messages.commitsbehind, {
commitsBehind: data.commitsBehind, commitsBehind: data.commitsBehind,
}) })
) : data.commitsBehind === -1 ? ( ) : data.commitsBehind === -1 ? (
intl.formatMessage(messages.outofdate) intl.formatMessage(messages.outofdate)
) : ( ) : (
<code className="bg-transparent p-0"> <code className="bg-transparent p-0">
{data.version.replace('develop-', '')} {data.version.replace('develop-', '')}
</code> </code>
)} )}
</span> </span>
</div> </div>
{data.updateAvailable && <ArrowUpCircleIcon className="h-6 w-6" />} {data.updateAvailable && <ArrowUpCircleIcon className="h-6 w-6" />}
</a>
</Link> </Link>
); );
}; };

View File

@@ -1,13 +1,13 @@
import Modal from '@app/components/Common/Modal'; import Modal from '@app/components/Common/Modal';
import useSettings from '@app/hooks/useSettings'; import useSettings from '@app/hooks/useSettings';
import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react'; import { Transition } from '@headlessui/react';
import axios from 'axios'; import axios from 'axios';
import { Field, Formik } from 'formik'; import { Field, Formik } from 'formik';
import type React from 'react'; import { useIntl } from 'react-intl';
import { defineMessages, useIntl } from 'react-intl';
import * as Yup from 'yup'; import * as Yup from 'yup';
const messages = defineMessages({ const messages = defineMessages('components.Login', {
title: 'Add Email', title: 'Add Email',
description: description:
'Since this is your first time logging into {applicationName}, you are required to add a valid email address.', 'Since this is your first time logging into {applicationName}, you are required to add a valid email address.',

View File

@@ -1,17 +1,17 @@
import Button from '@app/components/Common/Button'; import Button from '@app/components/Common/Button';
import Tooltip from '@app/components/Common/Tooltip'; import Tooltip from '@app/components/Common/Tooltip';
import useSettings from '@app/hooks/useSettings'; import useSettings from '@app/hooks/useSettings';
import defineMessages from '@app/utils/defineMessages';
import { InformationCircleIcon } from '@heroicons/react/24/solid'; import { InformationCircleIcon } from '@heroicons/react/24/solid';
import { ApiErrorCode } from '@server/constants/error'; import { ApiErrorCode } from '@server/constants/error';
import axios from 'axios'; import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import getConfig from 'next/config'; import getConfig from 'next/config';
import type React from 'react'; import { useIntl } from 'react-intl';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import * as Yup from 'yup'; import * as Yup from 'yup';
const messages = defineMessages({ const messages = defineMessages('components.Login', {
username: 'Username', username: 'Username',
password: 'Password', password: 'Password',
hostname: '{mediaServerName} URL', hostname: '{mediaServerName} URL',

View File

@@ -1,6 +1,7 @@
import Button from '@app/components/Common/Button'; import Button from '@app/components/Common/Button';
import SensitiveInput from '@app/components/Common/SensitiveInput'; import SensitiveInput from '@app/components/Common/SensitiveInput';
import useSettings from '@app/hooks/useSettings'; import useSettings from '@app/hooks/useSettings';
import defineMessages from '@app/utils/defineMessages';
import { import {
ArrowLeftOnRectangleIcon, ArrowLeftOnRectangleIcon,
LifebuoyIcon, LifebuoyIcon,
@@ -9,10 +10,10 @@ import axios from 'axios';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import Link from 'next/link'; import Link from 'next/link';
import { useState } from 'react'; import { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import * as Yup from 'yup'; import * as Yup from 'yup';
const messages = defineMessages({ const messages = defineMessages('components.Login', {
username: 'Username', username: 'Username',
email: 'Email Address', email: 'Email Address',
password: 'Password', password: 'Password',
@@ -137,7 +138,7 @@ const LocalLogin = ({ revalidate }: LocalLoginProps) => {
</span> </span>
{passwordResetEnabled && ( {passwordResetEnabled && (
<span className="inline-flex rounded-md shadow-sm"> <span className="inline-flex rounded-md shadow-sm">
<Link href="/resetpassword" passHref> <Link href="/resetpassword" passHref legacyBehavior>
<Button as="a" buttonType="ghost"> <Button as="a" buttonType="ghost">
<LifebuoyIcon /> <LifebuoyIcon />
<span> <span>

View File

@@ -6,18 +6,20 @@ import LocalLogin from '@app/components/Login/LocalLogin';
import PlexLoginButton from '@app/components/PlexLoginButton'; import PlexLoginButton from '@app/components/PlexLoginButton';
import useSettings from '@app/hooks/useSettings'; import useSettings from '@app/hooks/useSettings';
import { useUser } from '@app/hooks/useUser'; import { useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react'; import { Transition } from '@headlessui/react';
import { XCircleIcon } from '@heroicons/react/24/solid'; import { XCircleIcon } from '@heroicons/react/24/solid';
import { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
import axios from 'axios'; import axios from 'axios';
import getConfig from 'next/config'; import getConfig from 'next/config';
import { useRouter } from 'next/dist/client/router'; import { useRouter } from 'next/dist/client/router';
import Image from 'next/image';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
import JellyfinLogin from './JellyfinLogin'; import JellyfinLogin from './JellyfinLogin';
const messages = defineMessages({ const messages = defineMessages('components.Login', {
signin: 'Sign In', signin: 'Sign In',
signinheader: 'Sign in to continue', signinheader: 'Sign in to continue',
signinwithplex: 'Use your Plex account', signinwithplex: 'Use your Plex account',
@@ -86,8 +88,10 @@ const Login = () => {
<LanguagePicker /> <LanguagePicker />
</div> </div>
<div className="relative z-40 mt-10 flex flex-col items-center px-4 sm:mx-auto sm:w-full sm:max-w-md"> <div className="relative z-40 mt-10 flex flex-col items-center px-4 sm:mx-auto sm:w-full sm:max-w-md">
<img src="/logo_stacked.svg" className="mb-10 max-w-full" alt="Logo" /> <div className="relative h-48 w-full max-w-full">
<h2 className="mt-2 text-center text-3xl font-extrabold leading-9 text-gray-100"> <Image src="/logo_stacked.svg" alt="Logo" fill />
</div>
<h2 className="mt-12 text-center text-3xl font-extrabold leading-9 text-gray-100">
{intl.formatMessage(messages.signinheader)} {intl.formatMessage(messages.signinheader)}
</h2> </h2>
</div> </div>

View File

@@ -8,6 +8,7 @@ import RequestBlock from '@app/components/RequestBlock';
import useSettings from '@app/hooks/useSettings'; import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { Bars4Icon, ServerIcon } from '@heroicons/react/24/outline'; import { Bars4Icon, ServerIcon } from '@heroicons/react/24/outline';
import { import {
CheckCircleIcon, CheckCircleIcon,
@@ -27,11 +28,12 @@ import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv'; import type { TvDetails } from '@server/models/Tv';
import axios from 'axios'; import axios from 'axios';
import getConfig from 'next/config'; import getConfig from 'next/config';
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.ManageSlideOver', {
manageModalTitle: 'Manage {mediaType}', manageModalTitle: 'Manage {mediaType}',
manageModalIssues: 'Open Issues', manageModalIssues: 'Open Issues',
manageModalRequests: 'Requests', manageModalRequests: 'Requests',
@@ -328,19 +330,20 @@ const ManageSlideOver = ({
: `/users/${user.id}` : `/users/${user.id}`
} }
key={`watch-user-${user.id}`} key={`watch-user-${user.id}`}
className="z-0 mb-1 -mr-2 shrink-0 hover:z-50"
> >
<a className="z-0 mb-1 -mr-2 shrink-0 hover:z-50"> <Tooltip
<Tooltip key={`watch-user-${user.id}`}
key={`watch-user-${user.id}`} content={user.displayName}
content={user.displayName} >
> <Image
<img src={user.avatar}
src={user.avatar} alt={user.displayName}
alt={user.displayName} className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105" width={32}
/> height={32}
</Tooltip> />
</a> </Tooltip>
</Link> </Link>
))} ))}
</span> </span>
@@ -488,19 +491,20 @@ const ManageSlideOver = ({
: `/users/${user.id}` : `/users/${user.id}`
} }
key={`watch-user-${user.id}`} key={`watch-user-${user.id}`}
className="z-0 mb-1 -mr-2 shrink-0 hover:z-50"
> >
<a className="z-0 mb-1 -mr-2 shrink-0 hover:z-50"> <Tooltip
<Tooltip key={`watch-user-${user.id}`}
key={`watch-user-${user.id}`} content={user.displayName}
content={user.displayName} >
> <Image
<img src={user.avatar}
src={user.avatar} alt={user.displayName}
alt={user.displayName} className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105" width={32}
/> height={32}
</Tooltip> />
</a> </Tooltip>
</Link> </Link>
))} ))}
</span> </span>

View File

@@ -1,11 +1,13 @@
import TitleCard from '@app/components/TitleCard'; import TitleCard from '@app/components/TitleCard';
import defineMessages from '@app/utils/defineMessages';
import { ArrowRightCircleIcon } from '@heroicons/react/24/solid'; import { ArrowRightCircleIcon } from '@heroicons/react/24/solid';
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useState } from 'react'; import { useState } from 'react';
import { useInView } from 'react-intersection-observer'; import { useInView } from 'react-intersection-observer';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.MediaSlider.ShowMoreCard', {
seemore: 'See More', seemore: 'See More',
}); });
@@ -30,79 +32,82 @@ const ShowMoreCard = ({ url, posters }: ShowMoreCardProps) => {
} }
return ( return (
<Link href={url}> <Link
<a href={url}
className={'w-36 sm:w-36 md:w-44'} className={'w-36 sm:w-36 md:w-44'}
onMouseEnter={() => { onMouseEnter={() => {
setHovered(true);
}}
onMouseLeave={() => setHovered(false)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
setHovered(true); setHovered(true);
}} }
onMouseLeave={() => setHovered(false)} }}
onKeyDown={(e) => { role="link"
if (e.key === 'Enter') { tabIndex={0}
setHovered(true); >
} <div
}} className={`relative w-36 transform-gpu cursor-pointer
role="link" overflow-hidden rounded-xl text-white shadow-lg ring-1 transition duration-150 ease-in-out sm:w-36 md:w-44 ${
tabIndex={0} isHovered
? 'scale-105 bg-gray-600 ring-gray-500'
: 'scale-100 bg-gray-800 ring-gray-700'
}`}
> >
<div <div style={{ paddingBottom: '150%' }}>
className={`relative w-36 transform-gpu cursor-pointer <div className="absolute inset-0 flex h-full w-full flex-col items-center p-2">
overflow-hidden rounded-xl text-white shadow-lg ring-1 transition duration-150 ease-in-out sm:w-36 md:w-44 ${ <div className="relative z-10 flex h-full flex-wrap items-center justify-center opacity-30">
isHovered {posters[0] && (
? 'scale-105 bg-gray-600 ring-gray-500' <div className="w-1/2 p-1">
: 'scale-100 bg-gray-800 ring-gray-700' <Image
}`} src={`//image.tmdb.org/t/p/w300_and_h450_face${posters[0]}`}
> alt=""
<div style={{ paddingBottom: '150%' }}> className="w-full rounded-md"
<div className="absolute inset-0 flex h-full w-full flex-col items-center p-2"> fill
<div className="relative z-10 flex h-full flex-wrap items-center justify-center opacity-30"> />
{posters[0] && (
<div className="w-1/2 p-1">
<img
src={`//image.tmdb.org/t/p/w300_and_h450_face${posters[0]}`}
alt=""
className="w-full rounded-md"
/>
</div>
)}
{posters[1] && (
<div className="w-1/2 p-1">
<img
src={`//image.tmdb.org/t/p/w300_and_h450_face${posters[1]}`}
alt=""
className="w-full rounded-md"
/>
</div>
)}
{posters[2] && (
<div className="w-1/2 p-1">
<img
src={`//image.tmdb.org/t/p/w300_and_h450_face${posters[2]}`}
alt=""
className="w-full rounded-md"
/>
</div>
)}
{posters[3] && (
<div className="w-1/2 p-1">
<img
src={`//image.tmdb.org/t/p/w300_and_h450_face${posters[3]}`}
alt=""
className="w-full rounded-md"
/>
</div>
)}
</div>
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center text-white">
<ArrowRightCircleIcon className="w-14" />
<div className="mt-2 font-extrabold">
{intl.formatMessage(messages.seemore)}
</div> </div>
)}
{posters[1] && (
<div className="w-1/2 p-1">
<Image
src={`//image.tmdb.org/t/p/w300_and_h450_face${posters[1]}`}
alt=""
className="w-full rounded-md"
fill
/>
</div>
)}
{posters[2] && (
<div className="w-1/2 p-1">
<Image
src={`//image.tmdb.org/t/p/w300_and_h450_face${posters[2]}`}
alt=""
className="w-full rounded-md"
fill
/>
</div>
)}
{posters[3] && (
<div className="w-1/2 p-1">
<Image
src={`//image.tmdb.org/t/p/w300_and_h450_face${posters[3]}`}
alt=""
className="w-full rounded-md"
fill
/>
</div>
)}
</div>
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center text-white">
<ArrowRightCircleIcon className="w-14" />
<div className="mt-2 font-extrabold">
{intl.formatMessage(messages.seemore)}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</a> </div>
</Link> </Link>
); );
}; };

View File

@@ -152,11 +152,9 @@ const MediaSlider = ({
<> <>
<div className="slider-header"> <div className="slider-header">
{linkUrl ? ( {linkUrl ? (
<Link href={linkUrl}> <Link href={linkUrl} className="slider-title min-w-0 pr-16">
<a className="slider-title min-w-0 pr-16"> <span className="truncate">{title}</span>
<span className="truncate">{title}</span> <ArrowRightCircleIcon />
<ArrowRightCircleIcon />
</a>
</Link> </Link>
) : ( ) : (
<div className="slider-title"> <div className="slider-title">

View File

@@ -3,13 +3,14 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import PageTitle from '@app/components/Common/PageTitle'; import PageTitle from '@app/components/Common/PageTitle';
import PersonCard from '@app/components/PersonCard'; import PersonCard from '@app/components/PersonCard';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import type { MovieDetails } from '@server/models/Movie'; import type { MovieDetails } from '@server/models/Movie';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.MovieDetails.MovieCast', {
fullcast: 'Full Cast', fullcast: 'Full Cast',
}); });
@@ -34,8 +35,8 @@ const MovieCast = () => {
<div className="mt-1 mb-5"> <div className="mt-1 mb-5">
<Header <Header
subtext={ subtext={
<Link href={`/movie/${data.id}`}> <Link href={`/movie/${data.id}`} className="hover:underline">
<a className="hover:underline">{data.title}</a> {data.title}
</Link> </Link>
} }
> >

View File

@@ -3,13 +3,14 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import PageTitle from '@app/components/Common/PageTitle'; import PageTitle from '@app/components/Common/PageTitle';
import PersonCard from '@app/components/PersonCard'; import PersonCard from '@app/components/PersonCard';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import type { MovieDetails } from '@server/models/Movie'; import type { MovieDetails } from '@server/models/Movie';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.MovieDetails.MovieCrew', {
fullcrew: 'Full Crew', fullcrew: 'Full Crew',
}); });
@@ -34,8 +35,8 @@ const MovieCrew = () => {
<div className="mt-1 mb-5"> <div className="mt-1 mb-5">
<Header <Header
subtext={ subtext={
<Link href={`/movie/${data.id}`}> <Link href={`/movie/${data.id}`} className="hover:underline">
<a className="hover:underline">{data.title}</a> {data.title}
</Link> </Link>
} }
> >

View File

@@ -3,14 +3,15 @@ import ListView from '@app/components/Common/ListView';
import PageTitle from '@app/components/Common/PageTitle'; import PageTitle from '@app/components/Common/PageTitle';
import useDiscover from '@app/hooks/useDiscover'; import useDiscover from '@app/hooks/useDiscover';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import type { MovieDetails } from '@server/models/Movie'; import type { MovieDetails } from '@server/models/Movie';
import type { MovieResult } from '@server/models/Search'; import type { MovieResult } from '@server/models/Search';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.MovieDetails', {
recommendations: 'Recommendations', recommendations: 'Recommendations',
}); });
@@ -44,8 +45,8 @@ const MovieRecommendations = () => {
<div className="mt-1 mb-5"> <div className="mt-1 mb-5">
<Header <Header
subtext={ subtext={
<Link href={`/movie/${movieData?.id}`}> <Link href={`/movie/${movieData?.id}`} className="hover:underline">
<a className="hover:underline">{movieData?.title}</a> {movieData?.title}
</Link> </Link>
} }
> >

View File

@@ -3,14 +3,15 @@ import ListView from '@app/components/Common/ListView';
import PageTitle from '@app/components/Common/PageTitle'; import PageTitle from '@app/components/Common/PageTitle';
import useDiscover from '@app/hooks/useDiscover'; import useDiscover from '@app/hooks/useDiscover';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import type { MovieDetails } from '@server/models/Movie'; import type { MovieDetails } from '@server/models/Movie';
import type { MovieResult } from '@server/models/Search'; import type { MovieResult } from '@server/models/Search';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.MovieDetails', {
similar: 'Similar Titles', similar: 'Similar Titles',
}); });
@@ -42,8 +43,8 @@ const MovieSimilar = () => {
<div className="mt-1 mb-5"> <div className="mt-1 mb-5">
<Header <Header
subtext={ subtext={
<Link href={`/movie/${movieData?.id}`}> <Link href={`/movie/${movieData?.id}`} className="hover:underline">
<a className="hover:underline">{movieData?.title}</a> {movieData?.title}
</Link> </Link>
} }
> >

View File

@@ -27,6 +27,7 @@ import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import { sortCrewPriority } from '@app/utils/creditHelpers'; import { sortCrewPriority } from '@app/utils/creditHelpers';
import defineMessages from '@app/utils/defineMessages';
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper'; import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
import { import {
ArrowRightCircleIcon, ArrowRightCircleIcon,
@@ -46,17 +47,17 @@ import { IssueStatus } from '@server/constants/issue';
import { MediaStatus } from '@server/constants/media'; import { MediaStatus } from '@server/constants/media';
import { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
import type { MovieDetails as MovieDetailsType } from '@server/models/Movie'; import type { MovieDetails as MovieDetailsType } from '@server/models/Movie';
import { hasFlag } from 'country-flag-icons'; import { countries } from 'country-flag-icons';
import 'country-flag-icons/3x2/flags.css'; import 'country-flag-icons/3x2/flags.css';
import { uniqBy } from 'lodash'; import { uniqBy } from 'lodash';
import getConfig from 'next/config'; import getConfig from 'next/config';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.MovieDetails', {
originaltitle: 'Original Title', originaltitle: 'Original Title',
releasedate: releasedate:
'{releaseCount, plural, one {Release Date} other {Release Dates}}', '{releaseCount, plural, one {Release Date} other {Release Dates}}',
@@ -239,8 +240,12 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
movieAttributes.push( movieAttributes.push(
data.genres data.genres
.map((g) => ( .map((g) => (
<Link href={`/discover/movies?genre=${g.id}`} key={`genre-${g.id}`}> <Link
<a className="hover:underline">{g.name}</a> href={`/discover/movies?genre=${g.id}`}
key={`genre-${g.id}`}
className="hover:underline"
>
{g.name}
</Link> </Link>
)) ))
.reduce((prev, curr) => ( .reduce((prev, curr) => (
@@ -294,8 +299,8 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
<CachedImage <CachedImage
alt="" alt=""
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`} src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`}
layout="fill" style={{ width: '100%', height: '100%', objectFit: 'cover' }}
objectFit="cover" fill
priority priority
/> />
<div <div
@@ -336,7 +341,8 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
: '/images/overseerr_poster_not_found.png' : '/images/overseerr_poster_not_found.png'
} }
alt="" alt=""
layout="responsive" sizes="100vw"
style={{ width: '100%', height: 'auto' }}
width={600} width={600}
height={900} height={900}
priority priority
@@ -483,18 +489,19 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
{sortedCrew.slice(0, 6).map((person) => ( {sortedCrew.slice(0, 6).map((person) => (
<li key={`crew-${person.job}-${person.id}`}> <li key={`crew-${person.job}-${person.id}`}>
<span>{person.job}</span> <span>{person.job}</span>
<Link href={`/person/${person.id}`}> <Link href={`/person/${person.id}`} className="crew-name">
<a className="crew-name">{person.name}</a> {person.name}
</Link> </Link>
</li> </li>
))} ))}
</ul> </ul>
<div className="mt-4 flex justify-end"> <div className="mt-4 flex justify-end">
<Link href={`/movie/${data.id}/crew`}> <Link
<a className="flex items-center text-gray-400 transition duration-300 hover:text-gray-100"> href={`/movie/${data.id}/crew`}
<span>{intl.formatMessage(messages.viewfullcrew)}</span> className="flex items-center text-gray-400 transition duration-300 hover:text-gray-100"
<ArrowRightCircleIcon className="ml-1.5 inline-block h-5 w-5" /> >
</a> <span>{intl.formatMessage(messages.viewfullcrew)}</span>
<ArrowRightCircleIcon className="ml-1.5 inline-block h-5 w-5" />
</Link> </Link>
</div> </div>
</> </>
@@ -505,10 +512,9 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
<Link <Link
href={`/discover/movies?keywords=${keyword.id}`} href={`/discover/movies?keywords=${keyword.id}`}
key={`keyword-id-${keyword.id}`} key={`keyword-id-${keyword.id}`}
className="mb-2 mr-2 inline-flex last:mr-0"
> >
<a className="mb-2 mr-2 inline-flex last:mr-0"> <Tag>{keyword.name}</Tag>
<Tag>{keyword.name}</Tag>
</a>
</Link> </Link>
))} ))}
</div> </div>
@@ -518,31 +524,33 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
{data.collection && ( {data.collection && (
<div className="mb-6"> <div className="mb-6">
<Link href={`/collection/${data.collection.id}`}> <Link href={`/collection/${data.collection.id}`}>
<a> <div className="group relative z-0 scale-100 transform-gpu cursor-pointer overflow-hidden rounded-lg bg-gray-800 bg-cover bg-center shadow-md ring-1 ring-gray-700 transition duration-300 hover:scale-105 hover:ring-gray-500">
<div className="group relative z-0 scale-100 transform-gpu cursor-pointer overflow-hidden rounded-lg bg-gray-800 bg-cover bg-center shadow-md ring-1 ring-gray-700 transition duration-300 hover:scale-105 hover:ring-gray-500"> <div className="absolute inset-0 z-0">
<div className="absolute inset-0 z-0"> <CachedImage
<CachedImage src={`https://image.tmdb.org/t/p/w1440_and_h320_multi_faces/${data.collection.backdropPath}`}
src={`https://image.tmdb.org/t/p/w1440_and_h320_multi_faces/${data.collection.backdropPath}`} alt=""
alt="" style={{
layout="fill" width: '100%',
objectFit="cover" height: '100%',
/> objectFit: 'cover',
<div }}
className="absolute inset-0" fill
style={{ />
backgroundImage: <div
'linear-gradient(180deg, rgba(31, 41, 55, 0.47) 0%, rgba(31, 41, 55, 0.80) 100%)', className="absolute inset-0"
}} style={{
/> backgroundImage:
</div> 'linear-gradient(180deg, rgba(31, 41, 55, 0.47) 0%, rgba(31, 41, 55, 0.80) 100%)',
<div className="relative z-10 flex h-full items-center justify-between p-4 text-gray-200 transition duration-300 group-hover:text-white"> }}
<div>{data.collection.name}</div> />
<Button buttonSize="sm">
{intl.formatMessage(globalMessages.view)}
</Button>
</div>
</div> </div>
</a> <div className="relative z-10 flex h-full items-center justify-between p-4 text-gray-200 transition duration-300 group-hover:text-white">
<div>{data.collection.name}</div>
<Button buttonSize="sm">
{intl.formatMessage(globalMessages.view)}
</Button>
</div>
</div>
</Link> </Link>
</div> </div>
)} )}
@@ -739,15 +747,13 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
<Link <Link
href={`/discover/movies/language/${data.originalLanguage}`} href={`/discover/movies/language/${data.originalLanguage}`}
> >
<a> {intl.formatDisplayName(data.originalLanguage, {
{intl.formatDisplayName(data.originalLanguage, { type: 'language',
type: 'language', fallback: 'none',
fallback: 'none', }) ??
}) ?? data.spokenLanguages.find(
data.spokenLanguages.find( (lng) => lng.iso_639_1 === data.originalLanguage
(lng) => lng.iso_639_1 === data.originalLanguage )?.name}
)?.name}
</a>
</Link> </Link>
</span> </span>
</div> </div>
@@ -766,7 +772,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
className="flex items-center justify-end" className="flex items-center justify-end"
key={`prodcountry-${c.iso_3166_1}`} key={`prodcountry-${c.iso_3166_1}`}
> >
{hasFlag(c.iso_3166_1) && ( {countries.includes(c.iso_3166_1) && (
<span <span
className={`mr-1.5 text-xs leading-5 flag:${c.iso_3166_1}`} className={`mr-1.5 text-xs leading-5 flag:${c.iso_3166_1}`}
/> />
@@ -803,8 +809,9 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
<Link <Link
href={`/discover/movies/studio/${s.id}`} href={`/discover/movies/studio/${s.id}`}
key={`studio-${s.id}`} key={`studio-${s.id}`}
className="block"
> >
<a className="block">{s.name}</a> {s.name}
</Link> </Link>
); );
})} })}
@@ -864,11 +871,13 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
{data.credits.cast.length > 0 && ( {data.credits.cast.length > 0 && (
<> <>
<div className="slider-header"> <div className="slider-header">
<Link href="/movie/[movieId]/cast" as={`/movie/${data.id}/cast`}> <Link
<a className="slider-title"> href="/movie/[movieId]/cast"
<span>{intl.formatMessage(messages.cast)}</span> as={`/movie/${data.id}/cast`}
<ArrowRightCircleIcon /> className="slider-title"
</a> >
<span>{intl.formatMessage(messages.cast)}</span>
<ArrowRightCircleIcon />
</Link> </Link>
</div> </div>
<Slider <Slider

View File

@@ -2,11 +2,12 @@ import NotificationType from '@app/components/NotificationTypeSelector/Notificat
import useSettings from '@app/hooks/useSettings'; import useSettings from '@app/hooks/useSettings';
import type { User } from '@app/hooks/useUser'; import type { User } from '@app/hooks/useUser';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages';
import { sortBy } from 'lodash'; import { sortBy } from 'lodash';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.NotificationTypeSelector', {
notificationTypes: 'Notification Types', notificationTypes: 'Notification Types',
mediarequested: 'Request Pending Approval', mediarequested: 'Request Pending Approval',
mediarequestedDescription: mediarequestedDescription:

View File

@@ -3,10 +3,11 @@ import PermissionOption from '@app/components/PermissionOption';
import useSettings from '@app/hooks/useSettings'; import useSettings from '@app/hooks/useSettings';
import type { User } from '@app/hooks/useUser'; import type { User } from '@app/hooks/useUser';
import { Permission } from '@app/hooks/useUser'; import { Permission } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages';
import { MediaServerType } from '@server/constants/server'; import { MediaServerType } from '@server/constants/server';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
export const messages = defineMessages({ export const messages = defineMessages('components.PermissionEdit', {
admin: 'Admin', admin: 'Admin',
adminDescription: adminDescription:
'Full administrator access. Bypasses all other permission checks.', 'Full administrator access. Bypasses all other permission checks.',

View File

@@ -21,71 +21,72 @@ const PersonCard = ({
const [isHovered, setHovered] = useState(false); const [isHovered, setHovered] = useState(false);
return ( return (
<Link href={`/person/${personId}`}> <Link
<a href={`/person/${personId}`}
className={canExpand ? 'w-full' : 'w-36 sm:w-36 md:w-44'} className={canExpand ? 'w-full' : 'w-36 sm:w-36 md:w-44'}
onMouseEnter={() => { onMouseEnter={() => {
setHovered(true);
}}
onMouseLeave={() => setHovered(false)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
setHovered(true); setHovered(true);
}} }
onMouseLeave={() => setHovered(false)} }}
onKeyDown={(e) => { role="link"
if (e.key === 'Enter') { tabIndex={0}
setHovered(true); >
} <div
}} className={`relative ${
role="link" canExpand ? 'w-full' : 'w-36 sm:w-36 md:w-44'
tabIndex={0} } transform-gpu cursor-pointer rounded-xl text-white shadow ring-1 transition duration-150 ease-in-out ${
isHovered
? 'scale-105 bg-gray-700 ring-gray-500'
: 'scale-100 bg-gray-800 ring-gray-700'
}`}
> >
<div <div style={{ paddingBottom: '150%' }}>
className={`relative ${ <div className="absolute inset-0 flex h-full w-full flex-col items-center p-2">
canExpand ? 'w-full' : 'w-36 sm:w-36 md:w-44' <div className="relative mt-2 mb-4 flex h-1/2 w-full justify-center">
} transform-gpu cursor-pointer rounded-xl text-white shadow ring-1 transition duration-150 ease-in-out ${ {profilePath ? (
isHovered <div className="relative h-full w-3/4 overflow-hidden rounded-full ring-1 ring-gray-700">
? 'scale-105 bg-gray-700 ring-gray-500' <CachedImage
: 'scale-100 bg-gray-800 ring-gray-700' src={`https://image.tmdb.org/t/p/w600_and_h900_bestv2${profilePath}`}
}`} alt=""
> style={{
<div style={{ paddingBottom: '150%' }}> width: '100%',
<div className="absolute inset-0 flex h-full w-full flex-col items-center p-2"> height: '100%',
<div className="relative mt-2 mb-4 flex h-1/2 w-full justify-center"> objectFit: 'cover',
{profilePath ? ( }}
<div className="relative h-full w-3/4 overflow-hidden rounded-full ring-1 ring-gray-700"> fill
<CachedImage />
src={`https://image.tmdb.org/t/p/w600_and_h900_bestv2${profilePath}`}
alt=""
layout="fill"
objectFit="cover"
/>
</div>
) : (
<UserCircleIcon className="h-full" />
)}
</div>
<div className="w-full truncate text-center font-bold">
{name}
</div>
{subName && (
<div
className="overflow-hidden whitespace-normal text-center text-sm text-gray-300"
style={{
WebkitLineClamp: 2,
display: '-webkit-box',
overflow: 'hidden',
WebkitBoxOrient: 'vertical',
}}
>
{subName}
</div> </div>
) : (
<UserCircleIcon className="h-full" />
)} )}
<div
className={`absolute bottom-0 left-0 right-0 h-12 rounded-b-xl bg-gradient-to-t ${
isHovered ? 'from-gray-800' : 'from-gray-900'
}`}
/>
</div> </div>
<div className="w-full truncate text-center font-bold">{name}</div>
{subName && (
<div
className="overflow-hidden whitespace-normal text-center text-sm text-gray-300"
style={{
WebkitLineClamp: 2,
display: '-webkit-box',
overflow: 'hidden',
WebkitBoxOrient: 'vertical',
}}
>
{subName}
</div>
)}
<div
className={`absolute bottom-0 left-0 right-0 h-12 rounded-b-xl bg-gradient-to-t ${
isHovered ? 'from-gray-800' : 'from-gray-900'
}`}
/>
</div> </div>
</div> </div>
</a> </div>
</Link> </Link>
); );
}; };

View File

@@ -6,16 +6,17 @@ import PageTitle from '@app/components/Common/PageTitle';
import TitleCard from '@app/components/TitleCard'; import TitleCard from '@app/components/TitleCard';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error'; import Error from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages';
import type { PersonCombinedCreditsResponse } from '@server/interfaces/api/personInterfaces'; import type { PersonCombinedCreditsResponse } from '@server/interfaces/api/personInterfaces';
import type { PersonDetails as PersonDetailsType } from '@server/models/Person'; import type { PersonDetails as PersonDetailsType } from '@server/models/Person';
import { groupBy } from 'lodash'; import { groupBy } from 'lodash';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import TruncateMarkup from 'react-truncate-markup'; import TruncateMarkup from 'react-truncate-markup';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.PersonDetails', {
birthdate: 'Born {birthdate}', birthdate: 'Born {birthdate}',
lifespan: '{birthdate} {deathdate}', lifespan: '{birthdate} {deathdate}',
alsoknownas: 'Also Known As: {names}', alsoknownas: 'Also Known As: {names}',
@@ -228,8 +229,8 @@ const PersonDetails = () => {
<CachedImage <CachedImage
src={`https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.profilePath}`} src={`https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.profilePath}`}
alt="" alt=""
layout="fill" style={{ width: '100%', height: '100%', objectFit: 'cover' }}
objectFit="cover" fill
/> />
</div> </div>
)} )}

View File

@@ -1,10 +1,11 @@
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import PlexOAuth from '@app/utils/plex'; import PlexOAuth from '@app/utils/plex';
import { ArrowLeftOnRectangleIcon } from '@heroicons/react/24/outline'; import { ArrowLeftOnRectangleIcon } from '@heroicons/react/24/outline';
import { useState } from 'react'; import { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.PlexLoginButton', {
signinwithplex: 'Sign In', signinwithplex: 'Sign In',
signingin: 'Signing In…', signingin: 'Signing In…',
}); });

View File

@@ -1,7 +1,8 @@
import defineMessages from '@app/utils/defineMessages';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.QuotaSelector', {
movieRequests: movieRequests:
'{quotaLimit} <quotaUnits>{movies} per {quotaDays} {days}</quotaUnits>', '{quotaLimit} <quotaUnits>{movies} per {quotaDays} {days}</quotaUnits>',
tvRequests: tvRequests:

View File

@@ -1,15 +1,16 @@
import useSettings from '@app/hooks/useSettings'; import useSettings from '@app/hooks/useSettings';
import defineMessages from '@app/utils/defineMessages';
import { Listbox, Transition } from '@headlessui/react'; import { Listbox, Transition } from '@headlessui/react';
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/24/solid'; import { CheckIcon, ChevronDownIcon } from '@heroicons/react/24/solid';
import type { Region } from '@server/lib/settings'; import type { Region } from '@server/lib/settings';
import { hasFlag } from 'country-flag-icons'; import { countries } from 'country-flag-icons';
import 'country-flag-icons/3x2/flags.css'; import 'country-flag-icons/3x2/flags.css';
import { sortBy } from 'lodash'; import { sortBy } from 'lodash';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.RegionSelector', {
regionDefault: 'All Regions', regionDefault: 'All Regions',
regionServerDefault: 'Default ({region})', regionServerDefault: 'Default ({region})',
}); });
@@ -92,11 +93,12 @@ const RegionSelector = ({
<div className="relative"> <div className="relative">
<span className="inline-block w-full rounded-md shadow-sm"> <span className="inline-block w-full rounded-md shadow-sm">
<Listbox.Button className="focus:shadow-outline-blue relative flex w-full cursor-default items-center rounded-md border border-gray-500 bg-gray-700 py-2 pl-3 pr-10 text-left text-white transition duration-150 ease-in-out focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5"> <Listbox.Button className="focus:shadow-outline-blue relative flex w-full cursor-default items-center rounded-md border border-gray-500 bg-gray-700 py-2 pl-3 pr-10 text-left text-white transition duration-150 ease-in-out focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5">
{((selectedRegion && hasFlag(selectedRegion?.iso_3166_1)) || {((selectedRegion &&
countries.includes(selectedRegion?.iso_3166_1)) ||
(isUserSetting && (isUserSetting &&
!selectedRegion && !selectedRegion &&
currentSettings.region && currentSettings.region &&
hasFlag(currentSettings.region))) && ( countries.includes(currentSettings.region))) && (
<span className="mr-2 h-4 overflow-hidden text-base leading-4"> <span className="mr-2 h-4 overflow-hidden text-base leading-4">
<span <span
className={`flag:${ className={`flag:${
@@ -146,7 +148,7 @@ const RegionSelector = ({
<span className="mr-2 text-base"> <span className="mr-2 text-base">
<span <span
className={ className={
hasFlag(currentSettings.region) countries.includes(currentSettings.region)
? `flag:${currentSettings.region}` ? `flag:${currentSettings.region}`
: 'pr-6' : 'pr-6'
} }
@@ -215,7 +217,7 @@ const RegionSelector = ({
<span className="mr-2 text-base"> <span className="mr-2 text-base">
<span <span
className={ className={
hasFlag(region.iso_3166_1) countries.includes(region.iso_3166_1)
? `flag:${region.iso_3166_1}` ? `flag:${region.iso_3166_1}`
: 'pr-6' : 'pr-6'
} }

View File

@@ -5,6 +5,7 @@ import RequestModal from '@app/components/RequestModal';
import useRequestOverride from '@app/hooks/useRequestOverride'; import useRequestOverride from '@app/hooks/useRequestOverride';
import { useUser } from '@app/hooks/useUser'; import { useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { import {
CalendarIcon, CalendarIcon,
CheckIcon, CheckIcon,
@@ -19,9 +20,9 @@ import type { MediaRequest } from '@server/entity/MediaRequest';
import axios from 'axios'; import axios from 'axios';
import Link from 'next/link'; import Link from 'next/link';
import { useState } from 'react'; import { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.RequestBlock', {
seasons: '{seasonCount, plural, one {Season} other {Seasons}}', seasons: '{seasonCount, plural, one {Season} other {Seasons}}',
requestoverrides: 'Request Overrides', requestoverrides: 'Request Overrides',
server: 'Destination Server', server: 'Destination Server',
@@ -101,10 +102,9 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
? '/profile' ? '/profile'
: `/users/${request.requestedBy.id}` : `/users/${request.requestedBy.id}`
} }
className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline"
> >
<a className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline"> {request.requestedBy.displayName}
{request.requestedBy.displayName}
</a>
</Link> </Link>
</span> </span>
</div> </div>
@@ -120,10 +120,9 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
? '/profile' ? '/profile'
: `/users/${request.modifiedBy.id}` : `/users/${request.modifiedBy.id}`
} }
className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline"
> >
<a className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline"> {request.modifiedBy.displayName}
{request.modifiedBy.displayName}
</a>
</Link> </Link>
</span> </span>
</div> </div>

View File

@@ -3,6 +3,7 @@ import RequestModal from '@app/components/RequestModal';
import useSettings from '@app/hooks/useSettings'; import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'; import { ArrowDownTrayIcon } from '@heroicons/react/24/outline';
import { import {
CheckIcon, CheckIcon,
@@ -14,9 +15,9 @@ import type Media from '@server/entity/Media';
import type { MediaRequest } from '@server/entity/MediaRequest'; import type { MediaRequest } from '@server/entity/MediaRequest';
import axios from 'axios'; import axios from 'axios';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages('components.RequestButton', {
viewrequest: 'View Request', viewrequest: 'View Request',
viewrequest4k: 'View 4K Request', viewrequest4k: 'View 4K Request',
requestmore: 'Request More', requestmore: 'Request More',

View File

@@ -7,6 +7,7 @@ import StatusBadge from '@app/components/StatusBadge';
import useDeepLinks from '@app/hooks/useDeepLinks'; import useDeepLinks from '@app/hooks/useDeepLinks';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper'; import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
import { withProperties } from '@app/utils/typeHelpers'; import { withProperties } from '@app/utils/typeHelpers';
import { import {
@@ -21,14 +22,15 @@ import type { MediaRequest } from '@server/entity/MediaRequest';
import type { MovieDetails } from '@server/models/Movie'; import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv'; import type { TvDetails } from '@server/models/Tv';
import axios from 'axios'; import axios from 'axios';
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useInView } from 'react-intersection-observer'; import { useInView } from 'react-intersection-observer';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import useSWR, { mutate } from 'swr'; import useSWR, { mutate } from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.RequestCard', {
seasons: '{seasonCount, plural, one {Season} other {Seasons}}', seasons: '{seasonCount, plural, one {Season} other {Seasons}}',
failedretry: 'Something went wrong while retrying the request.', failedretry: 'Something went wrong while retrying the request.',
mediaerror: '{mediaType} Not Found', mediaerror: '{mediaType} Not Found',
@@ -106,17 +108,22 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
{ type: 'or' } { type: 'or' }
) && ( ) && (
<div className="card-field !hidden sm:!block"> <div className="card-field !hidden sm:!block">
<Link href={`/users/${requestData.requestedBy.id}`}> <Link
<a className="group flex items-center"> href={`/users/${requestData.requestedBy.id}`}
<img className="group flex items-center"
>
<span className="avatar-sm">
<Image
src={requestData.requestedBy.avatar} src={requestData.requestedBy.avatar}
alt="" alt=""
className="avatar-sm" className="avatar-sm object-cover"
width={20}
height={20}
/> />
<span className="truncate group-hover:underline"> </span>
{requestData.requestedBy.displayName} <span className="truncate group-hover:underline">
</span> {requestData.requestedBy.displayName}
</a> </span>
</Link> </Link>
</div> </div>
)} )}
@@ -324,8 +331,8 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
<CachedImage <CachedImage
alt="" alt=""
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`} src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
layout="fill" style={{ width: '100%', height: '100%', objectFit: 'cover' }}
objectFit="cover" fill
/> />
<div <div
className="absolute inset-0" className="absolute inset-0"
@@ -352,27 +359,31 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
? `/movie/${requestData.media.tmdbId}` ? `/movie/${requestData.media.tmdbId}`
: `/tv/${requestData.media.tmdbId}` : `/tv/${requestData.media.tmdbId}`
} }
className="overflow-hidden overflow-ellipsis whitespace-nowrap text-base font-bold text-white hover:underline sm:text-lg"
> >
<a className="overflow-hidden overflow-ellipsis whitespace-nowrap text-base font-bold text-white hover:underline sm:text-lg"> {isMovie(title) ? title.title : title.name}
{isMovie(title) ? title.title : title.name}
</a>
</Link> </Link>
{hasPermission( {hasPermission(
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW], [Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
{ type: 'or' } { type: 'or' }
) && ( ) && (
<div className="card-field"> <div className="card-field">
<Link href={`/users/${requestData.requestedBy.id}`}> <Link
<a className="group flex items-center"> href={`/users/${requestData.requestedBy.id}`}
<img className="group flex items-center"
>
<span className="avatar-sm">
<Image
src={requestData.requestedBy.avatar} src={requestData.requestedBy.avatar}
alt="" alt=""
className="avatar-sm object-cover" className="avatar-sm object-cover"
width={20}
height={20}
/> />
<span className="truncate font-semibold group-hover:text-white group-hover:underline"> </span>
{requestData.requestedBy.displayName} <span className="truncate font-semibold group-hover:text-white group-hover:underline">
</span> {requestData.requestedBy.displayName}
</a> </span>
</Link> </Link>
</div> </div>
)} )}
@@ -572,20 +583,20 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
? `/movie/${requestData.media.tmdbId}` ? `/movie/${requestData.media.tmdbId}`
: `/tv/${requestData.media.tmdbId}` : `/tv/${requestData.media.tmdbId}`
} }
className="w-20 flex-shrink-0 scale-100 transform-gpu cursor-pointer overflow-hidden rounded-md shadow-sm transition duration-300 hover:scale-105 hover:shadow-md sm:w-28"
> >
<a className="w-20 flex-shrink-0 scale-100 transform-gpu cursor-pointer overflow-hidden rounded-md shadow-sm transition duration-300 hover:scale-105 hover:shadow-md sm:w-28"> <CachedImage
<CachedImage src={
src={ title.posterPath
title.posterPath ? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}` : '/images/overseerr_poster_not_found.png'
: '/images/overseerr_poster_not_found.png' }
} alt=""
alt="" sizes="100vw"
layout="responsive" style={{ width: '100%', height: 'auto' }}
width={600} width={600}
height={900} height={900}
/> />
</a>
</Link> </Link>
</div> </div>
</> </>

View File

@@ -7,6 +7,7 @@ import StatusBadge from '@app/components/StatusBadge';
import useDeepLinks from '@app/hooks/useDeepLinks'; import useDeepLinks from '@app/hooks/useDeepLinks';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper'; import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
import { import {
ArrowPathIcon, ArrowPathIcon,
@@ -20,14 +21,15 @@ import type { MediaRequest } from '@server/entity/MediaRequest';
import type { MovieDetails } from '@server/models/Movie'; import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv'; import type { TvDetails } from '@server/models/Tv';
import axios from 'axios'; import axios from 'axios';
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useState } from 'react'; import { useState } from 'react';
import { useInView } from 'react-intersection-observer'; import { useInView } from 'react-intersection-observer';
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl'; import { FormattedRelativeTime, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.RequestList.RequestItem', {
seasons: '{seasonCount, plural, one {Season} other {Seasons}}', seasons: '{seasonCount, plural, one {Season} other {Seasons}}',
failedretry: 'Something went wrong while retrying the request.', failedretry: 'Something went wrong while retrying the request.',
requested: 'Requested', requested: 'Requested',
@@ -179,17 +181,22 @@ const RequestItemError = ({
/> />
), ),
user: ( user: (
<Link href={`/users/${requestData.requestedBy.id}`}> <Link
<a className="group flex items-center truncate"> href={`/users/${requestData.requestedBy.id}`}
<img className="group flex items-center truncate"
>
<span className="avatar-sm ml-1.5">
<Image
src={requestData.requestedBy.avatar} src={requestData.requestedBy.avatar}
alt="" alt=""
className="avatar-sm ml-1.5" className="avatar-sm object-cover"
width={20}
height={20}
/> />
<span className="truncate text-sm group-hover:underline"> </span>
{requestData.requestedBy.displayName} <span className="truncate text-sm group-hover:underline">
</span> {requestData.requestedBy.displayName}
</a> </span>
</Link> </Link>
), ),
})} })}
@@ -233,17 +240,22 @@ const RequestItemError = ({
/> />
), ),
user: ( user: (
<Link href={`/users/${requestData.modifiedBy.id}`}> <Link
<a className="group flex items-center truncate"> href={`/users/${requestData.modifiedBy.id}`}
<img className="group flex items-center truncate"
>
<span className="avatar-sm ml-1.5">
<Image
src={requestData.modifiedBy.avatar} src={requestData.modifiedBy.avatar}
alt="" alt=""
className="avatar-sm ml-1.5" className="avatar-sm object-cover"
width={20}
height={20}
/> />
<span className="truncate text-sm group-hover:underline"> </span>
{requestData.modifiedBy.displayName} <span className="truncate text-sm group-hover:underline">
</span> {requestData.modifiedBy.displayName}
</a> </span>
</Link> </Link>
), ),
})} })}
@@ -381,8 +393,8 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
<CachedImage <CachedImage
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`} src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
alt="" alt=""
layout="fill" style={{ width: '100%', height: '100%', objectFit: 'cover' }}
objectFit="cover" fill
/> />
<div <div
className="absolute inset-0" className="absolute inset-0"
@@ -401,21 +413,20 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
? `/movie/${requestData.media.tmdbId}` ? `/movie/${requestData.media.tmdbId}`
: `/tv/${requestData.media.tmdbId}` : `/tv/${requestData.media.tmdbId}`
} }
className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105"
> >
<a className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105"> <CachedImage
<CachedImage src={
src={ title.posterPath
title.posterPath ? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}` : '/images/overseerr_poster_not_found.png'
: '/images/overseerr_poster_not_found.png' }
} alt=""
alt="" sizes="100vw"
layout="responsive" style={{ width: '100%', height: 'auto', objectFit: 'cover' }}
width={600} width={600}
height={900} height={900}
objectFit="cover" />
/>
</a>
</Link> </Link>
<div className="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4"> <div className="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
<div className="pt-0.5 text-xs font-medium text-white sm:pt-1"> <div className="pt-0.5 text-xs font-medium text-white sm:pt-1">
@@ -430,10 +441,9 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
? `/movie/${requestData.media.tmdbId}` ? `/movie/${requestData.media.tmdbId}`
: `/tv/${requestData.media.tmdbId}` : `/tv/${requestData.media.tmdbId}`
} }
className="mr-2 min-w-0 truncate text-lg font-bold text-white hover:underline xl:text-xl"
> >
<a className="mr-2 min-w-0 truncate text-lg font-bold text-white hover:underline xl:text-xl"> {isMovie(title) ? title.title : title.name}
{isMovie(title) ? title.title : title.name}
</a>
</Link> </Link>
{!isMovie(title) && request.seasons.length > 0 && ( {!isMovie(title) && request.seasons.length > 0 && (
<div className="card-field"> <div className="card-field">
@@ -527,17 +537,22 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
/> />
), ),
user: ( user: (
<Link href={`/users/${requestData.requestedBy.id}`}> <Link
<a className="group flex items-center truncate"> href={`/users/${requestData.requestedBy.id}`}
<img className="group flex items-center truncate"
>
<span className="avatar-sm ml-1.5">
<Image
src={requestData.requestedBy.avatar} src={requestData.requestedBy.avatar}
alt="" alt=""
className="avatar-sm ml-1.5 object-cover" className="avatar-sm object-cover"
width={20}
height={20}
/> />
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline"> </span>
{requestData.requestedBy.displayName} <span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
</span> {requestData.requestedBy.displayName}
</a> </span>
</Link> </Link>
), ),
})} })}
@@ -581,17 +596,22 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
/> />
), ),
user: ( user: (
<Link href={`/users/${requestData.modifiedBy.id}`}> <Link
<a className="group flex items-center truncate"> href={`/users/${requestData.modifiedBy.id}`}
<img className="group flex items-center truncate"
src={requestData.modifiedBy.avatar} >
<span className="avatar-sm ml-1.5">
<Image
src={requestData.requestedBy.avatar}
alt="" alt=""
className="avatar-sm ml-1.5 object-cover" className="avatar-sm object-cover"
width={20}
height={20}
/> />
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline"> </span>
{requestData.modifiedBy.displayName} <span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
</span> {requestData.modifiedBy.displayName}
</a> </span>
</Link> </Link>
), ),
})} })}

View File

@@ -6,6 +6,7 @@ import RequestItem from '@app/components/RequestList/RequestItem';
import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams'; import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
import { useUser } from '@app/hooks/useUser'; import { useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { import {
BarsArrowDownIcon, BarsArrowDownIcon,
ChevronLeftIcon, ChevronLeftIcon,
@@ -16,10 +17,10 @@ import type { RequestResultsResponse } from '@server/interfaces/api/requestInter
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.RequestList', {
requests: 'Requests', requests: 'Requests',
showallrequests: 'Show All Requests', showallrequests: 'Show All Requests',
sortAdded: 'Most Recent', sortAdded: 'Most Recent',
@@ -122,12 +123,12 @@ const RequestList = () => {
<Header <Header
subtext={ subtext={
router.pathname.startsWith('/profile') ? ( router.pathname.startsWith('/profile') ? (
<Link href={`/profile`}> <Link href={`/profile`} className="hover:underline">
<a className="hover:underline">{currentUser?.displayName}</a> {currentUser?.displayName}
</Link> </Link>
) : router.query.userId ? ( ) : router.query.userId ? (
<Link href={`/users/${user?.id}`}> <Link href={`/users/${user?.id}`} className="hover:underline">
<a className="hover:underline">{user?.displayName}</a> {user?.displayName}
</Link> </Link>
) : ( ) : (
'' ''

View File

@@ -3,6 +3,7 @@ import { SmallLoadingSpinner } from '@app/components/Common/LoadingSpinner';
import type { User } from '@app/hooks/useUser'; import type { User } from '@app/hooks/useUser';
import { Permission, useUser } from '@app/hooks/useUser'; import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { formatBytes } from '@app/utils/numberHelpers'; import { formatBytes } from '@app/utils/numberHelpers';
import { Listbox, Transition } from '@headlessui/react'; import { Listbox, Transition } from '@headlessui/react';
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/24/solid'; import { CheckIcon, ChevronDownIcon } from '@heroicons/react/24/solid';
@@ -13,8 +14,9 @@ import type {
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces'; import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
import { hasPermission } from '@server/lib/permissions'; import { hasPermission } from '@server/lib/permissions';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import Image from 'next/image';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import Select from 'react-select'; import Select from 'react-select';
import useSWR from 'swr'; import useSWR from 'swr';
@@ -23,7 +25,7 @@ type OptionType = {
label: string; label: string;
}; };
const messages = defineMessages({ const messages = defineMessages('components.RequestModal.AdvancedRequester', {
advancedoptions: 'Advanced', advancedoptions: 'Advanced',
destinationserver: 'Destination Server', destinationserver: 'Destination Server',
qualityprofile: 'Quality Profile', qualityprofile: 'Quality Profile',
@@ -559,10 +561,12 @@ const AdvancedRequester = ({
<span className="inline-block w-full rounded-md shadow-sm"> <span className="inline-block w-full rounded-md shadow-sm">
<Listbox.Button className="focus:shadow-outline-blue relative w-full cursor-default rounded-md border border-gray-700 bg-gray-800 py-2 pl-3 pr-10 text-left text-white transition duration-150 ease-in-out focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5"> <Listbox.Button className="focus:shadow-outline-blue relative w-full cursor-default rounded-md border border-gray-700 bg-gray-800 py-2 pl-3 pr-10 text-left text-white transition duration-150 ease-in-out focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5">
<span className="flex items-center"> <span className="flex items-center">
<img <Image
src={selectedUser.avatar} src={selectedUser.avatar}
alt="" alt=""
className="h-6 w-6 flex-shrink-0 rounded-full object-cover" className="h-6 w-6 flex-shrink-0 rounded-full object-cover"
width={24}
height={24}
/> />
<span className="ml-3 block"> <span className="ml-3 block">
{selectedUser.displayName} {selectedUser.displayName}
@@ -609,10 +613,12 @@ const AdvancedRequester = ({
selected ? 'font-semibold' : 'font-normal' selected ? 'font-semibold' : 'font-normal'
} flex items-center`} } flex items-center`}
> >
<img <Image
src={user.avatar} src={user.avatar}
alt="" alt=""
className="h-6 w-6 flex-shrink-0 rounded-full object-cover" className="h-6 w-6 flex-shrink-0 rounded-full object-cover"
width={24}
height={24}
/> />
<span className="ml-3 block flex-shrink-0"> <span className="ml-3 block flex-shrink-0">
{user.displayName} {user.displayName}

View File

@@ -7,6 +7,7 @@ import AdvancedRequester from '@app/components/RequestModal/AdvancedRequester';
import QuotaDisplay from '@app/components/RequestModal/QuotaDisplay'; import QuotaDisplay from '@app/components/RequestModal/QuotaDisplay';
import { useUser } from '@app/hooks/useUser'; import { useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { MediaRequestStatus, MediaStatus } from '@server/constants/media'; import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
import type { MediaRequest } from '@server/entity/MediaRequest'; import type { MediaRequest } from '@server/entity/MediaRequest';
import type { QuotaResponse } from '@server/interfaces/api/userInterfaces'; import type { QuotaResponse } from '@server/interfaces/api/userInterfaces';
@@ -14,11 +15,11 @@ import { Permission } from '@server/lib/permissions';
import type { Collection } from '@server/models/Collection'; import type { Collection } from '@server/models/Collection';
import axios from 'axios'; import axios from 'axios';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages({ const messages = defineMessages('components.RequestModal', {
requestadmin: 'This request will be approved automatically.', requestadmin: 'This request will be approved automatically.',
requestSuccess: '<strong>{title}</strong> requested successfully!', requestSuccess: '<strong>{title}</strong> requested successfully!',
requestcollectiontitle: 'Request Collection', requestcollectiontitle: 'Request Collection',
@@ -402,10 +403,14 @@ const CollectionRequestModal = ({
: '/images/overseerr_poster_not_found.png' : '/images/overseerr_poster_not_found.png'
} }
alt="" alt=""
layout="responsive" sizes="100vw"
style={{
width: '100%',
height: 'auto',
objectFit: 'cover',
}}
width={600} width={600}
height={900} height={900}
objectFit="cover"
/> />
</div> </div>
<div className="flex flex-col justify-center pl-2"> <div className="flex flex-col justify-center pl-2">

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