chore: Use yarn workspaces & add Turborepo

- Fix eslint in UI project and fix identified issues
- Update prettier configs to new format
- Remove prettier eslint plugin as VSCode is configured to format on save
- Add GitHub workflow to check formatting
- Remove unused dependencies
- Remove yarn requirement in final image
- Use node v20 types & upgrade TS
- Add an example of a cross-project reference
This commit is contained in:
Ben Scobie
2024-11-10 01:00:46 +00:00
parent 7294f40486
commit ffee533b48
101 changed files with 2311 additions and 3856 deletions

View File

@@ -1,3 +1,7 @@
node_modules
.yarn
**/node_modules/
.git
data/
server/dist
ui/.next
Dockerfile
**/*.md

35
.github/workflows/quality.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Code quality checks
on:
pull_request:
branches: [main]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
formatting:
name: Formatting
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Activate corepack
run: |
corepack install
corepack enable
- name: Install dependencies
run: yarn --immutable
- name: Check formatting
run: yarn turbo format:check

View File

@@ -31,4 +31,4 @@ jobs:
run: yarn --immutable
- name: Run tests
run: yarn test
run: yarn turbo test

12
.gitignore vendored
View File

@@ -1,5 +1,11 @@
data/
docs-output/
node_modules
.yarn
.idea
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
.idea
.turbo

View File

@@ -29,11 +29,14 @@
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"typescript.preferences.importModuleSpecifier": "relative",
"files.associations": {
"globals.css": "tailwindcss"
},
}
}
}

934
.yarn/releases/yarn-4.5.1.cjs vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,5 @@
nodeLinker: node-modules
# yarnPath: .yarn/releases/yarn-4.0.2.cjs
enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.5.1.cjs

View File

@@ -7,7 +7,7 @@ All help is welcome and greatly appreciated! If you would like to contribute to
### Tools Required
- HTML/Typescript/Javascript editor
- [VSCode](https://code.visualstudio.com/) is recommended. Upon opening the project, a few extensions will be automatically recommended for install.
- [VSCode](https://code.visualstudio.com/) is recommended. Upon opening the project, a few extensions will be automatically recommended for install.
- [NodeJS](https://nodejs.org/en/download/) (Node 20.x or higher)
- [Git](https://git-scm.com/downloads)
@@ -43,35 +43,34 @@ All help is welcome and greatly appreciated! If you would like to contribute to
- `docs`
- `feature`
- `fix`
- `patch`
- `patch`
4. Activate the correct Yarn version. (*Note: In order to run `corepack enable`, you will need to be running cmd or Powershell as an Administrator.*)
4. Activate the correct Yarn version. (_Note: In order to run `corepack enable`, you will need to be running cmd or PowerShell as an Administrator._)
```bash
```bash
corepack install
corepack enable
```
5. Install dependencies
```bash
yarn
```bash
yarn
```
6. As of Maintainerr v2.0, the project looks to ensure you have read/write permissions on the `data` directory. This `data` directory does not exist when you first clone your fork. Before running the below commands, create a folder inside of your main Maintainerr directory named `data`, and ensure it has full permissions to the `Everyone` user.
```bash
example -> C:\Users\You\Documents\GitRepos\Maintainerr\data
```
7. Run the development commands (you will need two different cmd/Powershell terminals. One for each command.)
```bash
yarn dev:server
yarn dev:ui
example -> C:\Users\You\Documents\GitRepos\Maintainerr\data
```
- If the build fails with Powershell, try to use cmd instead.
7. Run the development command
```bash
yarn dev
```
- If the build fails with PowerShell, try to use cmd instead.
8. Make your code changes/improvements and test that they work as intended.
@@ -94,8 +93,8 @@ All help is welcome and greatly appreciated! If you would like to contribute to
- Always rebase your commit to the latest `main` branch. Do **not** merge `main` into your branch.
- It is your responsibility to keep your branch up-to-date. Your work will **not** be merged unless it is rebased off the latest `main` branch.
- You can create a "draft" pull request early to get feedback on your work.
- Your code **must** be formatted correctly, or the tests will fail.
- We use Prettier to format our code base. It should automatically run with a Git hook, but it is recommended to have the Prettier extension installed in your editor and format on save.
- Your code **must** be formatted correctly.
- We use Prettier to format our code base. It is recommended to have the Prettier extension installed in your editor and format on save.
- If you have questions or need help, you can reach out via [Discussions](https://github.com/jorenn92/Maintainerr/discussions) or our [Discord server](https://discord.gg/WP4ZW2QYwk).
### UI Text Style

View File

@@ -1,33 +1,15 @@
FROM node:20-alpine3.19 AS builder
FROM node:20-alpine3.19 AS base
LABEL Description="Contains the Maintainerr Docker image"
WORKDIR /opt/app/
FROM base AS builder
ARG TARGETPLATFORM
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
WORKDIR /app
COPY server/ ./server/
COPY ui/ ./ui/
COPY package.json ./package.json
COPY yarn.lock ./yarn.lock
COPY .yarnrc.yml ./.yarnrc.yml
# Enable correct yarn version
RUN corepack install && \
corepack enable
RUN apk --update --no-cache add python3 make g++ curl
RUN yarn --immutable --network-timeout 99999999
RUN \
case "${TARGETPLATFORM}" in ('linux/arm64' | 'linux/amd64') \
yarn add sharp \
;; \
esac
RUN yarn build:server
RUN yarn global add turbo@^2
COPY . .
RUN yarn install --network-timeout 99999999
RUN yarn cache clean
RUN <<EOF cat >> ./ui/.env
NEXT_PUBLIC_BASE_PATH=/__PATH_PREFIX__
@@ -35,36 +17,38 @@ EOF
RUN sed -i "s,basePath: '',basePath: '/__PATH_PREFIX__',g" ./ui/next.config.js
RUN yarn build:ui
RUN yarn turbo build
# copy standalone UI
RUN mv ./ui/.next/standalone/ui/ ./standalone-ui/ && \
mv ./ui/.next/standalone/ ./standalone-ui/ && \
mv ./ui/.next/static ./standalone-ui/.next/static && \
mv ./ui/public ./standalone-ui/public && \
rm -rf ./ui && \
mv ./standalone-ui ./ui
FROM base AS runner
WORKDIR /opt/app
# copy standalone UI
COPY --from=builder --chmod=777 --chown=node:node /app/ui/.next/standalone/ui ./ui
COPY --from=builder --chmod=777 --chown=node:node /app/ui/.next/static ./ui/.next/static
COPY --from=builder --chmod=777 --chown=node:node /app/ui/public ./ui/public
# Copy standalone server
RUN mv ./server/dist ./standalone-server && \
rm -rf ./server && \
mv ./standalone-server ./server
COPY --from=builder --chmod=777 --chown=node:node /app/server/dist ./server
COPY --from=builder --chmod=777 --chown=node:node /app/server/node_modules ./server/node_modules
RUN rm -rf node_modules .yarn
COPY docker/supervisord.conf /etc/supervisord.conf
COPY --chmod=777 --chown=node:node docker/start.sh /opt/app/start.sh
RUN yarn workspaces focus --production
RUN rm -rf .yarn && \
rm -rf /opt/yarn-* && \
chown -R node:node /opt/ && \
chmod -R 755 /opt/ && \
# Data dir
mkdir -m 777 /opt/data && \
# Create required directories
RUN mkdir -m 777 /opt/data && \
mkdir -m 777 /opt/data/logs && \
chown -R node:node /opt/data
# Final build
FROM node:20-alpine3.19
# This is required for docker user directive to work
RUN chmod 777 /opt/app/start.sh && \
chmod 777 /opt/app/ui && \
chmod 777 /opt/app/ui/public && \
chmod 777 /opt/app/ui/.next/static && \
mkdir -m 777 /opt/app/ui/.next/cache && \
chown -R node:node /opt/app/ui/.next/cache
RUN apk --update --no-cache add curl supervisor
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
@@ -85,6 +69,8 @@ ENV UI_HOSTNAME=${UI_HOSTNAME}
ARG GIT_SHA
ENV GIT_SHA=$GIT_SHA
ENV DATA_DIR=/opt/data
# container version type. develop, stable, edge,.. a release=stable
ARG VERSION_TAG=develop
ENV VERSION_TAG=$VERSION_TAG
@@ -92,39 +78,11 @@ ENV VERSION_TAG=$VERSION_TAG
ARG BASE_PATH
ENV BASE_PATH=${BASE_PATH}
# Set global yarn vars to a folder read/write able for all users
ENV YARN_INSTALL_STATE_PATH=/tmp/.yarn/install-state.gz
ENV YARN_GLOBAL_FOLDER=/tmp/.yarn/global
ENV YARN_CACHE_FOLDER=/tmp/.yarn/cache
# Temporary workaround for https://github.com/libuv/libuv/pull/4141
ENV UV_USE_IO_URING=0
COPY --from=builder --chown=node:node /opt /opt
WORKDIR /opt/app
COPY docker/supervisord.conf /etc/supervisord.conf
COPY docker/start.sh /opt/app/start.sh
# Enable correct yarn version, add supervisor & chown root /opt dir
RUN corepack install && \
corepack enable && \
apk add supervisor curl && \
chown node:node /opt && \
# This is required for docker user directive to work
chmod 777 /opt && \
chmod 777 /opt/app/start.sh && \
chmod 777 -R /opt/app/ui && \
mkdir -m 777 /.cache && \
mkdir -pm 777 /opt/app/ui/.next/cache && \
chown -R node:node /opt/app/ui/.next/cache
USER node
# Picked up for Node's .cache directory.
ENV HOME=/
EXPOSE 6246
VOLUME [ "/opt/data" ]

View File

@@ -4,7 +4,7 @@ logfile=/dev/null
pidfile=/dev/null
[program:server]
command=yarn node /opt/app/server/main.js
command=node /opt/app/server/main.js
autorestart=true
startretries=100
stdout_logfile=/dev/fd/1
@@ -13,7 +13,7 @@ redirect_stderr=true
[program:ui]
environment=PORT=%(ENV_UI_PORT)s,HOSTNAME=%(ENV_UI_HOSTNAME)s
command=yarn node /opt/app/ui/server.js
command=node /opt/app/ui/server.js
autorestart=true
startretries=100
stdout_logfile=/dev/fd/1

View File

@@ -2,133 +2,37 @@
"name": "maintainerr",
"version": "2.2.1",
"private": true,
"packageManager": "yarn@4.1.1",
"packageManager": "yarn@4.5.1",
"repository": {
"type": "git",
"url": "https://github.com/jorenn92/Maintainerr.git"
},
"license": "MIT",
"installConfig": {
"hoistingLimits": "workspaces"
},
"scripts": {
"prebuild:server": "cd server && rimraf dist",
"dev:ui": "cd ui && next dev",
"dev:server": "cd server && nest start --watch",
"build:ui": "cd ui && next build",
"build:server": "cd server && nest build",
"start:ui": "cd ui && next start -p 80",
"start:server": "cd server && node dist/main",
"lint:ui": "cd ui && next lint",
"lint:server": "cd server && eslint \"{src,test}/**/*.ts\" --fix",
"test": "jest",
"test:clear": "jest --clearCache",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"migration:run": "ts-node node_modules/typeorm/cli.js migration:run -d ./datasource-config.ts",
"migration:revert": "ts-node node_modules/typeorm/cli.js migration:revert -d ./datasource-config.ts",
"migration:generate": "ts-node node_modules/typeorm/cli.js migration:generate --dataSource ./datasource-config.ts -p"
"build": "turbo build",
"dev": "turbo dev",
"lint": "turbo lint",
"format": "turbo format",
"format:check": "turbo format:check",
"knip": "knip"
},
"dependencies": {
"@headlessui/react": "2.2.0",
"@heroicons/react": "^1.0.6",
"@monaco-editor/react": "^4.6.0",
"@nestjs/cli": "^10.4.7",
"@nestjs/common": "^10.4.9",
"@nestjs/core": "^10.4.7",
"@nestjs/platform-express": "^10.4.12",
"@nestjs/schedule": "^4.1.1",
"@nestjs/typeorm": "^10.0.2",
"@types/node": "^22.9.4",
"axios": "^1.7.8",
"bowser": "^2.11.0",
"chalk": "^4.1.2",
"cron-validator": "^1.3.1",
"crypto": "^1.0.1",
"http-server": "^14.1.1",
"lodash": "^4.17.21",
"nest-winston": "^1.10.0",
"next": "15.0.3",
"node-cache": "^5.1.2",
"path": "^0.12.7",
"plex-api": "^5.3.2",
"react": "18.2.0",
"react-dom": "18.3.1",
"react-select": "^5.8.0",
"react-toast-notifications": "^2.5.1",
"react-transition-group": "^4.4.5",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"sqlite3": "^5.1.6",
"typeorm": "^0.3.20",
"typescript": "^5.3.3",
"web-push": "^3.6.6",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0",
"xml2js": "^0.6.2",
"yaml": "^2.6.0"
"workspaces": [
"ui",
"server"
],
"engines": {
"node": ">=18"
},
"devDependencies": {
"@automock/jest": "^1.4.0",
"@babel/core": "^7.26.0",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.13.0",
"@nestjs/cli": "^10.4.7",
"@nestjs/schematics": "^10.2.3",
"@nestjs/testing": "^10.4.12",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^13.0.0",
"@semantic-release/exec": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@svgr/webpack": "^8.1.0",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/lodash": "^4.17.13",
"@types/react": "^18.2.73",
"@types/react-dom": "^18.3.0",
"@types/react-transition-group": "^4.4.11",
"@types/web-push": "^3.6.4",
"@types/xml2js": "^0.4.14",
"@typescript-eslint/eslint-plugin": "^8.17.0",
"@typescript-eslint/parser": "^8.17.0",
"autoprefixer": "10.4.20",
"eslint": "^9.15.0",
"eslint-config-next": "15.0.3",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"jest": "^29.7.0",
"monaco-editor": "0.52.0",
"postcss": "^8.4.49",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.9",
"@types/node": "^22.9.4",
"knip": "^5.36.5",
"semantic-release": "^24.2.0",
"source-map-support": "^0.5.21",
"tailwindcss": "^3.4.15",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "server/src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "./coverage",
"testEnvironment": "node"
"turbo": "^2.2.3",
"typescript": "^5.6.3"
},
"release": {
"plugins": [

View File

@@ -1,4 +0,0 @@
{
"singleQuote": true,
"trailingComma": "all"
}

View File

@@ -1,55 +1,58 @@
import typescriptEslintEslintPlugin from "@typescript-eslint/eslint-plugin";
import globals from "globals";
import tsParser from "@typescript-eslint/parser";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
import typescriptEslintEslintPlugin from '@typescript-eslint/eslint-plugin';
import globals from 'globals';
import tsParser from '@typescript-eslint/parser';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import js from '@eslint/js';
import { FlatCompat } from '@eslint/eslintrc';
import eslintConfigPrettier from 'eslint-config-prettier';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
export default [{
ignores: ["**/eslint.config.mjs"],
}, ...compat.extends("plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"), {
export default [
{
ignores: ['**/eslint.config.mjs'],
},
...compat.extends('plugin:@typescript-eslint/recommended'),
{
plugins: {
"@typescript-eslint": typescriptEslintEslintPlugin,
'@typescript-eslint': typescriptEslintEslintPlugin,
},
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
globals: {
...globals.node,
...globals.jest,
},
parser: tsParser,
ecmaVersion: 5,
sourceType: "module",
parser: tsParser,
ecmaVersion: 5,
sourceType: 'module',
parserOptions: {
project: "tsconfig.json",
tsconfigRootDir: ".",
},
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: '.',
},
},
rules: {
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
caughtErrors: "none"
}
],
"prettier/prettier": ["error", {
endOfLine: "auto",
}],
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
caughtErrors: 'none',
},
],
},
}];
},
eslintConfigPrettier,
];

88
server/package.json Normal file
View File

@@ -0,0 +1,88 @@
{
"name": "@maintainerr/server",
"private": true,
"exports": {
"./*": "./src/*.ts"
},
"installConfig": {
"hoistingLimits": "workspaces"
},
"scripts": {
"dev": "nest start --watch",
"build": "nest build",
"start": "node dist/main",
"lint": "eslint \"{src,test}/**/*.ts\" --fix",
"format": "prettier --write --ignore-path .gitignore .",
"format:check": "prettier --check --ignore-path .gitignore .",
"test": "jest",
"test:clear": "jest --clearCache",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"migration:run": "ts-node node_modules/typeorm/cli.js migration:run -d ./src/datasource-config.ts",
"migration:revert": "ts-node node_modules/typeorm/cli.js migration:revert -d ./src/datasource-config.ts",
"migration:generate": "ts-node node_modules/typeorm/cli.js migration:generate --dataSource ./src/datasource-config.ts -p"
},
"dependencies": {
"@nestjs/common": "^10.4.9",
"@nestjs/core": "^10.4.7",
"@nestjs/platform-express": "^10.4.12",
"@nestjs/schedule": "^4.1.1",
"@nestjs/typeorm": "^10.0.2",
"axios": "^1.7.8",
"chalk": "^4.1.2",
"cron-validator": "^1.3.1",
"lodash": "^4.17.21",
"nest-winston": "^1.10.0",
"node-cache": "^5.1.2",
"plex-api": "^5.3.2",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"sqlite3": "^5.1.6",
"typeorm": "^0.3.20",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0",
"xml2js": "^0.6.2",
"yaml": "^2.5.1"
},
"devDependencies": {
"@automock/jest": "^1.4.0",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.13.0",
"@nestjs/cli": "^10.4.7",
"@nestjs/schematics": "^10.2.3",
"@nestjs/testing": "^10.4.12",
"@types/jest": "^29.5.14",
"@types/lodash": "^4.17.13",
"@types/node": "^20",
"@types/xml2js": "^0.4.14",
"@typescript-eslint/eslint-plugin": "^8.17.0",
"@typescript-eslint/parser": "^8.17.0",
"eslint": "^9.15.0",
"eslint-config-prettier": "^9.1.0",
"jest": "^29.7.0",
"prettier": "^3.3.3",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.6.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "./coverage",
"testEnvironment": "node"
}
}

View File

@@ -0,0 +1,10 @@
/**
* @see https://prettier.io/docs/en/configuration.html
* @type {import("prettier").Config}
*/
const config = {
singleQuote: true,
trailingComma: 'all',
};
export default config;

View File

@@ -1,12 +1,6 @@
import { Injectable, Logger } from '@nestjs/common';
import { ExternalApiService } from '../modules/api/external-api/external-api.service';
interface VersionResponse {
status: 1 | 0;
version: string;
commitTag: string;
updateAvailable: boolean;
}
import { VersionResponse } from './dto/version-response.dto';
@Injectable()
export class AppService {

View File

@@ -15,4 +15,4 @@ const ormConfig: TypeOrmModuleOptions = {
autoLoadEntities: true,
migrationsRun: true,
};
export = ormConfig;
export default ormConfig;

View File

@@ -0,0 +1,6 @@
export interface VersionResponse {
status: 1 | 0;
version: string;
commitTag: string;
updateAvailable: boolean;
}

View File

@@ -1,12 +1,12 @@
import { DataSource } from "typeorm";
import { DataSource } from 'typeorm';
const datasource = new DataSource({
type: "sqlite",
database: "./data/maintainerr.sqlite",
entities: ["./server/dist/**/*.entities{.ts,.js}"],
type: 'sqlite',
database: './data/maintainerr.sqlite',
entities: ['./dist/**/*.entities{.ts,.js}'],
synchronize: false,
migrationsTableName: "migrations",
migrations: ["./server/dist/database/migrations/**/*{.js,.ts}"],
migrationsTableName: 'migrations',
migrations: ['./dist/database/migrations/**/*{.js,.ts}'],
});
datasource

View File

@@ -7,6 +7,11 @@ import chalk from 'chalk';
import path from 'path';
import * as fs from 'fs';
const dataDir =
process.env.NODE_ENV === 'production'
? '/opt/data'
: path.join(__dirname, '../../data');
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
// Winston logger config
@@ -49,10 +54,7 @@ async function bootstrap() {
transports: [
new winston.transports.Console(),
new DailyRotateFile({
filename: path.join(
__dirname,
`../../data/logs/maintainerr-%DATE%.log`,
),
filename: path.join(dataDir, 'logs/maintainerr-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
@@ -78,13 +80,10 @@ async function bootstrap() {
function createDataDirectoryStructure() {
try {
// Check if data directory has read and write permissions
fs.accessSync(
path.join(__dirname, `../../data`),
fs.constants.R_OK | fs.constants.W_OK,
);
fs.accessSync(dataDir, fs.constants.R_OK | fs.constants.W_OK);
// create logs dir
const dir = path.join(__dirname, `../../data/logs`);
const dir = path.join(dataDir, 'logs');
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, {
recursive: true,
@@ -93,7 +92,7 @@ function createDataDirectoryStructure() {
}
// if db already exists, check r/w permissions
const db = path.join(__dirname, `../../data/maintainerr.sqlite`);
const db = path.join(dataDir, 'maintainerr.sqlite');
if (fs.existsSync(db)) {
fs.accessSync(db, fs.constants.R_OK | fs.constants.W_OK);
}

View File

@@ -6,7 +6,6 @@ import {
RuleConstants,
} from '../constants/rules.constants';
import { EPlexDataType } from '../../api/plex-api/enums/plex-data-type-enum';
import _ from 'lodash';
import {
TautulliApiService,
TautulliHistoryRequestOptions,

View File

@@ -1,5 +1,4 @@
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { RadarrSettingDto } from "../dto's/radarr-setting.dto";
import { Collection } from '../../collections/entities/collection.entities';
@Entity()

View File

@@ -1,5 +1,4 @@
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { SonarrSettingDto } from "../dto's/sonarr-setting.dto";
import { Collection } from '../../collections/entities/collection.entities';
@Entity()

View File

@@ -1,7 +1,7 @@
import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { randomUUID } from 'crypto';
import { IsNull, Not, Repository } from 'typeorm';
import { Not, Repository } from 'typeorm';
import { isValidCron } from 'cron-validator';
import { BasicResponseDto } from '../api/external-api/dto/basic-response.dto';
import { OverseerrApiService } from '../api/overseerr-api/overseerr-api.service';

View File

@@ -6,7 +6,7 @@
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",

29
turbo.json Normal file
View File

@@ -0,0 +1,29 @@
{
"$schema": "https://turbo.build/schema.json",
"ui": "tui",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"lint": {
"dependsOn": ["^lint"]
},
"dev": {
"cache": false,
"persistent": true
},
"test": {},
"test:watch": {
"cache": false,
"persistent": true
},
"format": {
"dependsOn": ["^format"]
},
"format:check": {
"dependsOn": ["^format:check"]
}
}
}

View File

@@ -1,7 +0,0 @@
{
"extends": "next/core-web-vitals",
"rules": {
"@next/next/no-html-link-for-pages": "off",
"react-hooks/exhaustive-deps": "off"
}
}

28
ui/eslint.config.mjs Normal file
View File

@@ -0,0 +1,28 @@
import { FlatCompat } from '@eslint/eslintrc'
import path from 'path'
import { fileURLToPath } from 'url'
import eslintConfigPrettier from 'eslint-config-prettier'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname,
})
/** @type {import('eslint').Linter.Config[]} */
const configs = [
...compat.extends('next/core-web-vitals', 'next/typescript'),
{
rules: {
'@next/next/no-html-link-for-pages': 'off',
'react-hooks/exhaustive-deps': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
},
},
eslintConfigPrettier,
]
export default configs

55
ui/package.json Normal file
View File

@@ -0,0 +1,55 @@
{
"name": "@maintainerr/ui",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start -p 80",
"lint": "next lint",
"format": "prettier --write --ignore-path .gitignore .",
"format:check": "prettier --check --ignore-path .gitignore ."
},
"installConfig": {
"hoistingLimits": "workspaces"
},
"dependencies": {
"@heroicons/react": "^1.0.6",
"@maintainerr/server": "workspace:^",
"@monaco-editor/react": "^4.6.0",
"axios": "^1.7.8",
"bowser": "^2.11.0",
"cron-validator": "^1.3.1",
"lodash": "^4.17.21",
"next": "15.0.3",
"react": "18.2.0",
"react-dom": "18.3.1",
"react-select": "^5.8.0",
"react-toast-notifications": "^2.5.1",
"react-transition-group": "^4.4.5",
"yaml": "^2.6.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.13.0",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@types/lodash": "^4.17.13",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react-transition-group": "^4.4.11",
"@typescript-eslint/eslint-plugin": "^8.17.0",
"@typescript-eslint/parser": "^8.17.0",
"autoprefixer": "10.4.20",
"eslint": "^9.15.0",
"eslint-config-next": "15.0.3",
"eslint-config-prettier": "^9.1.0",
"monaco-editor": "0.52.0",
"postcss": "^8.4.49",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.9",
"tailwindcss": "^3.4.15",
"typescript": "^5.6.3"
}
}

View File

@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

9
ui/postcss.config.mjs Normal file
View File

@@ -0,0 +1,9 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
export default config

View File

@@ -1,7 +0,0 @@
module.exports = {
arrowParens: 'always',
singleQuote: true,
tabWidth: 2,
semi: false,
tailwindConfig: './tailwind.config.js',
}

14
ui/prettier.config.mjs Normal file
View File

@@ -0,0 +1,14 @@
/**
* @see https://prettier.io/docs/en/configuration.html
* @type {import("prettier").Config}
*/
const config = {
arrowParens: 'always',
singleQuote: true,
tabWidth: 2,
semi: false,
tailwindConfig: './tailwind.config.js',
plugins: ['prettier-plugin-tailwindcss'],
}
export default config

View File

@@ -174,44 +174,44 @@ const AddModal = (props: IAddModal) => {
useEffect(() => {
setLoading(true)
props.type === 2
? selectedEpisodes !== -1
? GetApiHandler(`/collections?typeId=4`).then((resp) => {
// get collections for episodes
setCollectionOptions([...origCollectionOptions, ...resp])
setLoading(false)
})
: selectedSeasons !== -1
? GetApiHandler(`/collections?typeId=3`).then((resp) => {
// get collections for episodes and seasons
GetApiHandler(`/collections?typeId=4`).then((resp2) => {
setCollectionOptions([
...origCollectionOptions,
...resp,
...resp2,
])
setLoading(false)
})
})
: GetApiHandler(`/collections?typeId=2`).then((resp) => {
// get collections for episodes, seasons and shows
GetApiHandler(`/collections?typeId=3`).then((resp2) => {
GetApiHandler(`/collections?typeId=4`).then((resp3) => {
setCollectionOptions([
...origCollectionOptions,
...resp,
...resp2,
...resp3,
])
setLoading(false)
})
})
})
: GetApiHandler(`/collections?typeId=1`).then((resp) => {
// get collections for movies
if (props.type === 2) {
if (selectedEpisodes !== -1) {
GetApiHandler(`/collections?typeId=4`).then((resp) => {
// get collections for episodes
setCollectionOptions([...origCollectionOptions, ...resp])
setLoading(false)
})
} else if (selectedSeasons !== -1) {
GetApiHandler(`/collections?typeId=3`).then((resp) => {
// get collections for episodes and seasons
GetApiHandler(`/collections?typeId=4`).then((resp2) => {
setCollectionOptions([...origCollectionOptions, ...resp, ...resp2])
setLoading(false)
})
})
} else {
GetApiHandler(`/collections?typeId=2`).then((resp) => {
// get collections for episodes, seasons and shows
GetApiHandler(`/collections?typeId=3`).then((resp2) => {
GetApiHandler(`/collections?typeId=4`).then((resp3) => {
setCollectionOptions([
...origCollectionOptions,
...resp,
...resp2,
...resp3,
])
setLoading(false)
})
})
})
}
} else {
GetApiHandler(`/collections?typeId=1`).then((resp) => {
// get collections for movies
setCollectionOptions([...origCollectionOptions, ...resp])
setLoading(false)
})
}
}, [selectedSeasons, selectedEpisodes])
return (

View File

@@ -180,7 +180,7 @@ const CollectionInfo = (props: ICollectionInfo) => {
</li>
</ul>
<div className="text-zinc-300 font-bold heading mt-5">
<div className="heading mt-5 font-bold text-zinc-300">
<h2>{'Logs'}</h2>
</div>
@@ -188,7 +188,7 @@ const CollectionInfo = (props: ICollectionInfo) => {
{/* full container */}
<div className="mb-2 flex flex-grow flex-col sm:flex-grow-0 sm:flex-row sm:justify-end">
{/* search */}
<div className="mt-4 flex flex-grow mr-2 sm:w-1/2 w-full">
<div className="mr-2 mt-4 flex w-full flex-grow sm:w-1/2">
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-zinc-800 px-3 text-sm text-gray-100">
<SearchIcon className="h-6 w-6" />
</span>
@@ -203,7 +203,7 @@ const CollectionInfo = (props: ICollectionInfo) => {
{/* sort/filter container */}
<div className="mb-2 flex flex-1 flex-row justify-between sm:mb-0 sm:flex-none">
{/* sort */}
<div className="mt-4 mr-2 flex flex-grow sm:w-1/2">
<div className="mr-2 mt-4 flex flex-grow sm:w-1/2">
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-zinc-800 px-3 text-sm text-gray-100">
{currentSort === 'DESC' ? (
<SortDescendingIcon className="h-6 w-6" />
@@ -311,7 +311,7 @@ const CollectionInfo = (props: ICollectionInfo) => {
{loadingExtraRef.current ? (
<tr>
<Table.TD colSpan={2} noPadding>
<SmallLoadingSpinner className="w-8 m-auto mt-2 mb-2" />
<SmallLoadingSpinner className="m-auto mb-2 mt-2 w-8" />
</Table.TD>
</tr>
) : undefined}

View File

@@ -43,7 +43,7 @@ const RemoveFromCollectionBtn = (props: IRemoveFromCollectionBtn) => {
<Button
buttonType="primary"
buttonSize="md"
className="mt-2 mb-1 h-6 w-full text-zinc-200 shadow-md"
className="mb-1 mt-2 h-6 w-full text-zinc-200 shadow-md"
onClick={() => setSure(true)}
>
{<TrashIcon className="m-auto ml-3 h-3" />}{' '}
@@ -53,7 +53,7 @@ const RemoveFromCollectionBtn = (props: IRemoveFromCollectionBtn) => {
<Button
buttonType="primary"
buttonSize="md"
className="mt-2 mb-1 h-6 w-full text-zinc-200 shadow-md"
className="mb-1 mt-2 h-6 w-full text-zinc-200 shadow-md"
onClick={props.popup ? handlePopup : handle}
>
<p className="rules-button-text m-auto mr-2">{'Are you sure?'}</p>

View File

@@ -29,7 +29,7 @@ const TestMediaItem = (props: ITestMediaItem) => {
const [loading, setLoading] = useState(true)
const [ruleGroup, setRuleGroup] = useState<{
dataType: EPlexDataType
id: String
id: string
}>()
const [selectedSeasons, setSelectedSeasons] = useState<number>(-1)
const [selectedEpisodes, setSelectedEpisodes] = useState<number>(-1)
@@ -146,7 +146,8 @@ const TestMediaItem = (props: ITestMediaItem) => {
useEffect(() => {
GetApiHandler(`/rules/collection/${props.collectionId}`).then((resp) => {
setRuleGroup(resp), setLoading(false)
setRuleGroup(resp)
setLoading(false)
})
}, [])
@@ -168,7 +169,7 @@ const TestMediaItem = (props: ITestMediaItem) => {
}, [selectedMediaId])
return !loading && ruleGroup ? (
<div className={'w-full h-full'}>
<div className={'h-full w-full'}>
<Modal
loading={false}
backgroundClickable={false}

View File

@@ -155,14 +155,14 @@ const CollectionDetail: React.FC<ICollectionDetail> = (
return (
<div className="w-full">
<div className="m-auto mb-3 flex w-full">
<h1 className="w-full whitespace-nowrap overflow-hidden overflow-ellipsis flex text-lg font-bold text-zinc-200 sm:m-0 xl:m-0 justify-center sm:justify-start">
<h1 className="flex w-full justify-center overflow-hidden overflow-ellipsis whitespace-nowrap text-lg font-bold text-zinc-200 sm:m-0 sm:justify-start xl:m-0">
{`${props.title}`}
</h1>
</div>
<div>
<div className="flex justify-center items-center h-full">
<div className="mt-0 mb-4 w-fit sm:w-full">
<div className="flex h-full items-center justify-center">
<div className="mb-4 mt-0 w-fit sm:w-full">
<TabbedLinks
onChange={(t) => setSelectedTab(t)}
routes={tabbedRoutes}
@@ -173,11 +173,11 @@ const CollectionDetail: React.FC<ICollectionDetail> = (
</div>
<div className="flex justify-center sm:justify-start">
<button
className="edit-button mb-4 flex rounded h-9 text-zinc-200 shadow-md"
className="edit-button mb-4 flex h-9 rounded text-zinc-200 shadow-md"
onClick={() => setMediaTestModalOpen(true)}
>
{<PlayIcon className="m-auto h-5 ml-5" />}{' '}
<p className="m-auto rules-button-text ml-1 mr-5">Test Media</p>
{<PlayIcon className="m-auto ml-5 h-5" />}{' '}
<p className="rules-button-text m-auto ml-1 mr-5">Test Media</p>
</button>
</div>

View File

@@ -40,10 +40,10 @@ const CollectionItem = (props: ICollectionItem) => {
<div className="collection-backdrop"></div>
</div>
) : undefined}
<div className="inset-0 z-0 h-fit p-3 ">
<div className="inset-0 z-0 h-fit p-3">
<div className="overflow-hidden overflow-ellipsis whitespace-nowrap text-base font-bold text-white sm:text-lg">
<a
className="hover:underline hover:cursor-pointer"
className="hover:cursor-pointer hover:underline"
// onClick={() => props.onClick(props.collection)}
{...(props.onClick
? { onClick: () => props.onClick!(props.collection) }
@@ -54,7 +54,7 @@ const CollectionItem = (props: ICollectionItem) => {
: props.collection.title}
</a>
</div>
<div className="max-h-12 h-12 line-clamp-2 whitespace-normal text-base text-zinc-400 hover:overflow-y-scroll sm:text-lg sm:h-14 sm:max-h-14">
<div className="line-clamp-2 h-12 max-h-12 whitespace-normal text-base text-zinc-400 hover:overflow-y-scroll sm:h-14 sm:max-h-14 sm:text-lg">
{props.collection.manualCollection
? `Handled by rule: '${props.collection.title}'`
: props.collection.description}

View File

@@ -16,8 +16,8 @@ const CollectionOverview = (props: ICollectionOverview) => {
<div>
<LibrarySwitcher onSwitch={props.onSwitchLibrary} />
<div className="m-auto mb-3 flex ">
<div className="m-auto sm:m-0 ">
<div className="m-auto mb-3 flex">
<div className="m-auto sm:m-0">
<ExecuteButton
onClick={debounce(props.doActions, 5000, {
leading: true,
@@ -38,7 +38,7 @@ const CollectionOverview = (props: ICollectionOverview) => {
{props.collections?.map((col) => (
<li
key={+col.id!}
className="collection relative mb-5 flex h-fit flex-col overflow-hidden rounded-xl bg-zinc-800 bg-cover bg-center p-4 text-zinc-400 shadow ring-1 ring-zinc-700 sm:mb-0 sm:mr-5 xs:w-full transform-gpu"
className="collection relative mb-5 flex h-fit transform-gpu flex-col overflow-hidden rounded-xl bg-zinc-800 bg-cover bg-center p-4 text-zinc-400 shadow ring-1 ring-zinc-700 xs:w-full sm:mb-0 sm:mr-5"
>
<CollectionItem
key={col.id}

View File

@@ -66,7 +66,7 @@ const Collection = () => {
id != 9999
? LibrariesCtx.libraries.find((el) => +el.key === id)
: undefined
lib ? setLibrary(lib) : setLibrary(undefined)
setLibrary(lib)
}
useEffect(() => {

View File

@@ -8,11 +8,11 @@ interface IAddButton {
const AddButton = (props: IAddButton) => {
return (
<button
className="add-button bg-amber-600 hover:bg-amber-500 flex m-auto h-9 rounded text-zinc-200 shadow-md"
className="add-button m-auto flex h-9 rounded bg-amber-600 text-zinc-200 shadow-md hover:bg-amber-500"
onClick={props.onClick}
>
{<PlusCircleIcon className="m-auto h-5 ml-4" />}
<p className="m-auto rules-button-text ml-1 mr-4">{props.text}</p>
{<PlusCircleIcon className="m-auto ml-4 h-5" />}
<p className="rules-button-text m-auto ml-1 mr-4">{props.text}</p>
</button>
)
}

View File

@@ -41,12 +41,19 @@ const CommunityRuleTableRow = (props: ICommunityRuleTableRow) => {
<td
onClick={onClick}
onDoubleClick={onDoubleClick}
className="md:w-105 whitespace-wrap inline-block w-60 overflow-hidden overflow-ellipsis px-4 py-4 text-left text-sm leading-5 text-white max-h-44"
className="md:w-105 whitespace-wrap inline-block max-h-44 w-60 overflow-hidden overflow-ellipsis px-4 py-4 text-left text-sm leading-5 text-white"
>
{props.rule.name}
</td>
<td className="px-4 py-4 text-center text-sm leading-5 text-white">
<div className="content-left flex" title={props.thumbsActive ? '' : 'You Already submitted karma for this rule'}>
<div
className="content-left flex"
title={
props.thumbsActive
? ''
: 'You Already submitted karma for this rule'
}
>
<ChevronUpIcon
onClick={
props.thumbsActive &&

View File

@@ -219,7 +219,7 @@ const CommunityRuleModal = (props: ICommunityRuleModal) => {
<SearchBar onSearch={handleSearch} />
{originalRules.length > 0 ? (
<div className="flex flex-col">
<div className="my-2 -mx-4 overflow-x-auto md:mx-0 lg:mx-0">
<div className="-mx-4 my-2 overflow-x-auto md:mx-0 lg:mx-0">
<div className="inline-block min-w-full py-2 align-middle">
<div className="overflow-hidden rounded-lg shadow md:mx-0 lg:mx-0">
<table className="ml-2 mr-2 min-w-full table-fixed">
@@ -298,7 +298,7 @@ const CommunityRuleModal = (props: ICommunityRuleModal) => {
title={'Community Rule Description'}
iconSvg={''}
>
<div className="block max-h-full w-full max-w-full overflow-auto bg-zinc-600 p-3 text-zinc-200">
<div className="block max-h-full w-full max-w-full overflow-auto bg-zinc-600 p-3 text-zinc-200">
{originalRules.find((r) => r.id === clickedRule)?.description}
</div>
</Modal>

View File

@@ -9,7 +9,7 @@ interface IDocsButton {
const DocsButton = (props: IDocsButton) => {
return (
<span className="h-full w-full inline-flex">
<span className="inline-flex h-full w-full">
<Link
legacyBehavior
href={`https://docs.maintainerr.info/${props.page ? props.page : ''}`}

View File

@@ -9,10 +9,10 @@ interface IEditButton {
const EditButton = (props: IEditButton) => {
return (
<button
className="bg-amber-600 hover:bg-amber-500 right-5 m-auto flex h-8 w-full rounded text-zinc-200 shadow-md"
className="right-5 m-auto flex h-8 w-full rounded bg-amber-600 text-zinc-200 shadow-md hover:bg-amber-500"
onClick={props.onClick}
>
<div className='m-auto ml-auto flex'>
<div className="m-auto ml-auto flex">
{props.svgIcon}
<p className="button-text m-auto ml-1 text-zinc-200">{props.text}</p>
</div>

View File

@@ -22,16 +22,16 @@ const ExecuteButton = (props: IExecuteButton) => {
return (
<button
className="edit-button flex m-auto rounded h-9 text-zinc-200 shadow-md"
className="edit-button m-auto flex h-9 rounded text-zinc-200 shadow-md"
disabled={clicked}
onClick={onClick}
>
{clicked ? (
<SmallLoadingSpinner className="h-5 m-auto ml-2" />
<SmallLoadingSpinner className="m-auto ml-2 h-5" />
) : (
<PlayIcon className="m-auto h-5 ml-4" />
<PlayIcon className="m-auto ml-4 h-5" />
)}{' '}
<p className="m-auto rules-button-text ml-1 mr-4">{props.text}</p>
<p className="rules-button-text m-auto ml-1 mr-4">{props.text}</p>
</button>
)
}

View File

@@ -10,11 +10,11 @@ const InfoButton = (props: IInfoButton) => {
return (
<button
disabled={props.enabled !== undefined ? !props.enabled : false}
className="bg-zinc-900 hover:bg-zinc-800 md:ml-2 flex mb-2 w-24 rounded h-9 text-zinc-200 shadow-md disabled:opacity-50"
className="mb-2 flex h-9 w-24 rounded bg-zinc-900 text-zinc-200 shadow-md hover:bg-zinc-800 disabled:opacity-50 md:ml-2"
onClick={props.onClick}
>
{<InformationCircleIcon className="m-auto h-5 ml-5" />}{' '}
<p className="m-auto rules-button-text ml-1 mr-5">{props.text}</p>
{<InformationCircleIcon className="m-auto ml-5 h-5" />}{' '}
<p className="rules-button-text m-auto ml-1 mr-5">{props.text}</p>
</button>
)
}

View File

@@ -4,7 +4,7 @@ import GetApiHandler from '../../../utils/ApiHandler'
interface ILibrarySwitcher {
onSwitch: (libraryId: number) => void
allPossible?: Boolean
allPossible?: boolean
}
const LibrarySwitcher = (props: ILibrarySwitcher) => {
@@ -19,9 +19,9 @@ const LibrarySwitcher = (props: ILibrarySwitcher) => {
GetApiHandler('/plex/libraries').then((resp) => {
if (resp) {
LibrariesCtx.addLibraries(resp)
props.allPossible !== undefined && !props.allPossible
? props.onSwitch(+resp[0].key)
: undefined
if (props.allPossible !== undefined && !props.allPossible) {
props.onSwitch(+resp[0].key)
}
} else {
LibrariesCtx.addLibraries([])
}

View File

@@ -181,7 +181,7 @@ const MediaCard: React.FC<IMediaCard> = ({
{/* on collection page and for manually added */}
{collectionPage && isManual && !showDetail ? (
<div className="absolute left-1/2 transform -translate-x-1/2 bottom-0 flex items-center justify-between p-2">
<div className="absolute bottom-0 left-1/2 flex -translate-x-1/2 transform items-center justify-between p-2">
<div
className={`pointer-events-none z-40 rounded-full shadow ${
mediaType === 'movie'
@@ -252,7 +252,12 @@ const MediaCard: React.FC<IMediaCard> = ({
leaveTo="opacity-0"
>
<div className="absolute inset-0 z-40 flex items-center justify-center rounded-xl bg-zinc-800 bg-opacity-75 text-zinc-200">
<Spinner className="h-10 w-10" />
<CachedImage
priority
src={Spinner}
className="h-10 w-10"
alt=""
/>
</div>
</Transition>
@@ -315,7 +320,7 @@ const MediaCard: React.FC<IMediaCard> = ({
<Button
buttonType="twin-primary-l"
buttonSize="md"
className="mb-1 mt-2 h-6 w-1/2 text-zinc-200 shadow-md"
className="mb-1 mt-2 h-6 w-1/2 text-zinc-200 shadow-md"
onClick={() => {
setAddModal(true)
}}

View File

@@ -82,21 +82,20 @@ const Modal: React.FC<ModalProps> = ({
}) => {
const modalRef = useRef<HTMLDivElement>(null)
useClickOutside(modalRef, () => {
typeof onCancel === 'function' && backgroundClickable
? onCancel()
: undefined
if (typeof onCancel === 'function' && backgroundClickable) {
onCancel()
}
})
useLockBodyScroll(true, disableScrollLock)
return ReactDOM.createPortal(
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div
className="fixed top-0 bottom-0 left-0 right-0 z-50 flex h-full w-full items-center justify-center bg-zinc-800 bg-opacity-70"
className="fixed bottom-0 left-0 right-0 top-0 z-50 flex h-full w-full items-center justify-center bg-zinc-800 bg-opacity-70"
onKeyDown={(e) => {
if (e.key === 'Escape') {
typeof onCancel === 'function' && backgroundClickable
? onCancel()
: undefined
if (typeof onCancel === 'function' && backgroundClickable) {
onCancel()
}
}
}}
>
@@ -123,7 +122,7 @@ const Modal: React.FC<ModalProps> = ({
show={!loading}
>
<div
className={`relative inline-block w-full transform overflow-auto bg-zinc-700 px-4 pt-5 pb-4 text-left align-bottom shadow-xl ring-1 ring-zinc-700 transition-all sm:my-8 ${maxWidthMap[size]} sm:rounded-lg sm:align-middle`}
className={`relative inline-block w-full transform overflow-auto bg-zinc-700 px-4 pb-4 pt-5 text-left align-bottom shadow-xl ring-1 ring-zinc-700 transition-all sm:my-8 ${maxWidthMap[size]} sm:rounded-lg sm:align-middle`}
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
@@ -155,7 +154,7 @@ const Modal: React.FC<ModalProps> = ({
</div>
)}
{typeof onSpecial === 'function' && (
<div className="flex justify-center sm:justify-end mt-4">
<div className="mt-4 flex justify-center sm:justify-end">
<Button
buttonType={specialButtonType}
onClick={onSpecial}

View File

@@ -12,7 +12,9 @@ const Pagination = (props: IPagination) => {
<span className="mb-2 text-sm text-zinc-200">
Showing{' '}
<span className="font-bold text-zinc-400">
{(props.totalItems === 0) ? 0 : (props.currentPage - 1) * props.pageSize + 1}
{props.totalItems === 0
? 0
: (props.currentPage - 1) * props.pageSize + 1}
</span>{' '}
to{' '}
<span className="font-bold text-zinc-400">
@@ -27,7 +29,7 @@ const Pagination = (props: IPagination) => {
{props.currentPage === 1 ? undefined : (
<button
onClick={() => props.handleBackward()}
className="rounded-l bg-zinc-900 py-2 px-4 text-sm font-medium text-white hover:bg-zinc-800"
className="rounded-l bg-zinc-900 px-4 py-2 text-sm font-medium text-white hover:bg-zinc-800"
>
Prev{' '}
</button>
@@ -36,7 +38,7 @@ const Pagination = (props: IPagination) => {
<button
onClick={() => props.handleForward()}
className={
'rounded-r border-0 border-l border-gray-700 bg-zinc-900 py-2 px-4 text-sm font-medium text-white hover:bg-zinc-800 '
'rounded-r border-0 border-l border-gray-700 bg-zinc-900 px-4 py-2 text-sm font-medium text-white hover:bg-zinc-800'
}
>
Next

View File

@@ -20,13 +20,13 @@ const SearchBar = (props: ISearchBar) => {
props.onSearch(text)
}, [text])
let inputHandler = (e: ChangeEvent<HTMLInputElement>) => {
const inputHandler = (e: ChangeEvent<HTMLInputElement>) => {
setText(e.target.value.toLowerCase())
}
return (
<div className="relative flex w-full items-center text-white focus-within:text-zinc-200">
<div className="pointer-events-none absolute left-4 flex items-center">
<div className="pointer-events-none absolute left-4 flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"

View File

@@ -3,8 +3,7 @@ import AsyncSelect from 'react-select/async'
import GetApiHandler from '../../../utils/ApiHandler'
import { IPlexMetadata } from '../../Overview/Content'
import { EPlexDataType } from '../../../utils/PlexDataType-enum'
import { SingleValue } from 'react-select/dist/declarations/src'
import { MediaType } from '../../../contexts/constants-context'
import { SingleValue } from 'react-select'
export interface IMediaOptions {
id: string

View File

@@ -27,8 +27,8 @@ const SectionHeading = (props: ISectionHeading) => {
onClick={addRule}
title={`Add rule to section ${props.id}`}
>
{<DocumentAddIcon className="m-auto h-5 ml-5 text-zinc-200" />}
<p className="m-auto ml-1 mr-5 text-zinc-100 button-text">Add</p>
{<DocumentAddIcon className="m-auto ml-5 h-5 text-zinc-200" />}
<p className="button-text m-auto ml-1 mr-5 text-zinc-100">Add</p>
</button>
) : undefined}
</div>

View File

@@ -23,11 +23,11 @@ export interface ITabbedLink {
}
const TabbedLink = (props: ITabbedLink) => {
let linkClasses =
const linkClasses =
(props.disabled ? 'pointer-events-none touch-none ' : 'cursor-pointer ') +
'px-1 py-4 ml-8 text-md font-semibold leading-5 transition duration-300 leading-5 whitespace-nowrap first:ml-0'
let activeLinkColor = ' border-b text-amber-500 border-amber-600'
let inactiveLinkColor =
const activeLinkColor = ' border-b text-amber-500 border-amber-600'
const inactiveLinkColor =
'border-transparent text-zinc-500 hover:border-b focus:border-b hover:text-zinc-300 hover:border-zinc-400 focus:text-zinc-300 focus:border-zinc-400'
return (

View File

@@ -78,7 +78,7 @@ type TableProps = {
const Table = ({ children }: TableProps) => {
return (
<div className="flex flex-col">
<div className="my-2 -mx-4 overflow-x-auto md:mx-0 lg:mx-0">
<div className="-mx-4 my-2 overflow-x-auto md:mx-0 lg:mx-0">
<div className="inline-block min-w-full py-2 align-middle">
<div className="overflow-hidden rounded-lg shadow md:mx-0 lg:mx-0">
<table className="min-w-full">{children}</table>

View File

@@ -10,7 +10,7 @@ interface ITestButton {
}
interface TestStatus {
clicked: Boolean
clicked: boolean
status: boolean
}
@@ -32,12 +32,10 @@ const TestButton = (props: ITestButton) => {
setLoading(true)
await GetApiHandler(props.testUrl).then((resp: BasicResponse) => {
setClicked({ clicked: true, status: resp.code == 1 ? true : false })
props.onClick
? props.onClick({
status: resp.code === 1 ? true : false,
version: resp.message,
})
: undefined
props.onClick?.({
status: resp.code === 1 ? true : false,
version: resp.message,
})
setLoading(false)
})
}

View File

@@ -1,31 +1,31 @@
import React from 'react';
import { CSSTransition as ReactCSSTransition } from 'react-transition-group';
import { useRef, useEffect, useContext } from 'react';
import React from 'react'
import { CSSTransition as ReactCSSTransition } from 'react-transition-group'
import { useRef, useEffect, useContext } from 'react'
interface CSSTransitionProps {
show?: boolean;
enter?: string;
enterFrom?: string;
enterTo?: string;
leave?: string;
leaveFrom?: string;
leaveTo?: string;
appear?: boolean;
children?: React.ReactNode;
show?: boolean
enter?: string
enterFrom?: string
enterTo?: string
leave?: string
leaveFrom?: string
leaveTo?: string
appear?: boolean
children?: React.ReactNode
}
const TransitionContext = React.createContext<{
parent: { show?: boolean; isInitialRender?: boolean; appear?: boolean };
parent: { show?: boolean; isInitialRender?: boolean; appear?: boolean }
}>({
parent: {},
});
})
function useIsInitialRender() {
const isInitialRender = useRef(true);
const isInitialRender = useRef(true)
useEffect(() => {
isInitialRender.current = false;
}, []);
return isInitialRender.current;
isInitialRender.current = false
}, [])
return isInitialRender.current
}
const CSSTransition: React.FC<CSSTransitionProps> = ({
@@ -39,20 +39,24 @@ const CSSTransition: React.FC<CSSTransitionProps> = ({
appear,
children,
}) => {
const enterClasses = enter.split(' ').filter((s) => s.length);
const enterFromClasses = enterFrom.split(' ').filter((s) => s.length);
const enterToClasses = enterTo.split(' ').filter((s) => s.length);
const leaveClasses = leave.split(' ').filter((s) => s.length);
const leaveFromClasses = leaveFrom.split(' ').filter((s) => s.length);
const leaveToClasses = leaveTo.split(' ').filter((s) => s.length);
const enterClasses = enter.split(' ').filter((s) => s.length)
const enterFromClasses = enterFrom.split(' ').filter((s) => s.length)
const enterToClasses = enterTo.split(' ').filter((s) => s.length)
const leaveClasses = leave.split(' ').filter((s) => s.length)
const leaveFromClasses = leaveFrom.split(' ').filter((s) => s.length)
const leaveToClasses = leaveTo.split(' ').filter((s) => s.length)
const addClasses = (node: HTMLElement, classes: string[]) => {
classes.length && node.classList.add(...classes);
};
if (classes.length) {
node.classList.add(...classes)
}
}
const removeClasses = (node: HTMLElement, classes: string[]) => {
classes.length && node.classList.remove(...classes);
};
if (classes.length) {
node.classList.remove(...classes)
}
}
return (
<ReactCSSTransition
@@ -60,42 +64,42 @@ const CSSTransition: React.FC<CSSTransitionProps> = ({
unmountOnExit
in={show}
addEndListener={(node, done) => {
node.addEventListener('transitionend', done, false);
node.addEventListener('transitionend', done, false)
}}
onEnter={(node: HTMLElement) => {
addClasses(node, [...enterClasses, ...enterFromClasses]);
addClasses(node, [...enterClasses, ...enterFromClasses])
}}
onEntering={(node: HTMLElement) => {
removeClasses(node, enterFromClasses);
addClasses(node, enterToClasses);
removeClasses(node, enterFromClasses)
addClasses(node, enterToClasses)
}}
onEntered={(node: HTMLElement) => {
removeClasses(node, [...enterToClasses, ...enterClasses]);
removeClasses(node, [...enterToClasses, ...enterClasses])
}}
onExit={(node) => {
addClasses(node, [...leaveClasses, ...leaveFromClasses]);
addClasses(node, [...leaveClasses, ...leaveFromClasses])
}}
onExiting={(node) => {
removeClasses(node, leaveFromClasses);
addClasses(node, leaveToClasses);
removeClasses(node, leaveFromClasses)
addClasses(node, leaveToClasses)
}}
onExited={(node) => {
removeClasses(node, [...leaveToClasses, ...leaveClasses]);
removeClasses(node, [...leaveToClasses, ...leaveClasses])
}}
>
{children}
</ReactCSSTransition>
);
};
)
}
const Transition: React.FC<CSSTransitionProps> = ({
show,
appear,
...rest
}) => {
const { parent } = useContext(TransitionContext);
const isInitialRender = useIsInitialRender();
const isChild = show === undefined;
const { parent } = useContext(TransitionContext)
const isInitialRender = useIsInitialRender()
const isChild = show === undefined
if (isChild) {
return (
@@ -104,7 +108,7 @@ const Transition: React.FC<CSSTransitionProps> = ({
show={parent.show}
{...rest}
/>
);
)
}
return (
@@ -119,7 +123,7 @@ const Transition: React.FC<CSSTransitionProps> = ({
>
<CSSTransition appear={appear} show={show} {...rest} />
</TransitionContext.Provider>
);
};
)
}
export default Transition;
export default Transition

View File

@@ -112,7 +112,7 @@ const NavBar: React.FC<NavBarProps> = ({ open, setClosed }) => {
>
<>
<div className="sidebar relative flex w-full max-w-xs flex-1 flex-col bg-zinc-800">
<div className="sidebar-close-button absolute top-0 right-0 -mr-14 p-1">
<div className="sidebar-close-button absolute right-0 top-0 -mr-14 p-1">
<button
className="flex h-12 w-12 items-center justify-center rounded-full text-white focus:bg-zinc-600 focus:outline-none"
aria-label="Close sidebar"
@@ -123,7 +123,7 @@ const NavBar: React.FC<NavBarProps> = ({ open, setClosed }) => {
</div>
<div
ref={navRef}
className="flex h-0 flex-1 flex-col overflow-y-auto pt-4 pb-8 sm:pb-4"
className="flex h-0 flex-1 flex-col overflow-y-auto pb-8 pt-4 sm:pb-4"
>
<div className="flex flex-shrink-0 items-center px-2">
<span className="px-4 text-xl text-zinc-50">
@@ -157,8 +157,7 @@ const NavBar: React.FC<NavBarProps> = ({ open, 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
${
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 ${
link.selected
? 'bg-gradient-to-br from-amber-600 to-amber-800 hover:from-amber-500 hover:to-amber-700'
: 'hover:bg-zinc-700'
@@ -185,10 +184,10 @@ const NavBar: React.FC<NavBarProps> = ({ open, setClosed }) => {
</Transition>
</div>
<div className="fixed top-0 bottom-0 left-0 z-30 hidden lg:flex lg:flex-shrink-0">
<div className="fixed bottom-0 left-0 top-0 z-30 hidden lg:flex lg:flex-shrink-0">
<div className="sidebar flex w-64 flex-col">
<div className="flex h-0 flex-1 flex-col">
<div className="flex flex-1 flex-col overflow-y-auto pt-4 pb-4">
<div className="flex flex-1 flex-col overflow-y-auto pb-4 pt-4">
<div className="flex flex-shrink-0 items-center">
<span className="px-4 text-2xl text-zinc-50">
<Link href="/">
@@ -223,9 +222,7 @@ const NavBar: React.FC<NavBarProps> = ({ open, setClosed }) => {
navBarLink.selected
? 'bg-gradient-to-br from-amber-600 to-amber-800 hover:from-amber-500 hover:to-amber-700'
: 'hover:bg-zinc-700'
}
focus:bg-amber-800 focus:outline-none
`}
} focus:bg-amber-800 focus:outline-none`}
>
{navBarLink.svgIcon}
{navBarLink.name}

View File

@@ -51,7 +51,7 @@ const Layout: React.FC<{ children?: ReactNode }> = (props: {
WebkitBackdropFilter: isScrolled ? 'blur(5px)' : undefined,
}}
>
<div className="flex flex-1 items-center justify-between pr-4 md:pr-4 md:pl-4 transparent-glass-bg">
<div className="transparent-glass-bg flex flex-1 items-center justify-between pr-4 md:pl-4 md:pr-4">
<button
className={`px-4 text-white ${
isScrolled ? 'opacity-90' : 'opacity-70'

View File

@@ -46,8 +46,8 @@ const PlexLoginButton: React.FC<PlexLoginButtonProps> = ({
{loading
? 'Loading'
: isProcessing
? 'Authenticating..'
: 'Authenticate with Plex'}
? 'Authenticating..'
: 'Authenticate with Plex'}
</span>
</button>
</span>

View File

@@ -11,7 +11,7 @@ const Overview = () => {
// const [isLoading, setIsLoading] = useState<Boolean>(false)
const loadingRef = useRef<boolean>(false)
const [loadingExtra, setLoadingExtra] = useState<Boolean>(false)
const [loadingExtra, setLoadingExtra] = useState<boolean>(false)
const [data, setData] = useState<IPlexMetadata[]>([])
const dataRef = useRef<IPlexMetadata[]>([])
@@ -21,7 +21,7 @@ const Overview = () => {
const [selectedLibrary, setSelectedLibrary] = useState<number>()
const selectedLibraryRef = useRef<number>()
const [searchUsed, setsearchUsed] = useState<Boolean>(false)
const [searchUsed, setsearchUsed] = useState<boolean>(false)
const pageData = useRef<number>(0)
const SearchCtx = useContext(SearchContext)
@@ -43,7 +43,7 @@ const Overview = () => {
LibrariesCtx.libraries.length > 0
) {
switchLib(
selectedLibrary ? selectedLibrary : +LibrariesCtx.libraries[0].key
selectedLibrary ? selectedLibrary : +LibrariesCtx.libraries[0].key,
)
}
}, 300)
@@ -58,7 +58,7 @@ const Overview = () => {
pageData.current = resp.length * 50
setData(resp ? resp : [])
setIsLoading(false)
}
},
)
setSelectedLibrary(+LibrariesCtx.libraries[0]?.key)
} else {
@@ -108,7 +108,7 @@ const Overview = () => {
await GetApiHandler(
`/plex/library/${selectedLibraryRef.current}/content/${
pageData.current + 1
}?amount=${fetchAmount}`
}?amount=${fetchAmount}`,
)
if (askedLib === selectedLibraryRef.current) {

View File

@@ -69,7 +69,7 @@ const CommunityRuleUpload = (props: ICommunityRuleUpload) => {
<label htmlFor="name" className="text-label">
Name *
</label>
<div className="form-input ">
<div className="form-input">
<div className="form-input-field">
<input
className="!bg-zinc-800"

View File

@@ -180,15 +180,15 @@ const RuleInput = (props: IRuleInput) => {
? customValType === RuleType.DATE
? customValType
: customValType === RuleType.NUMBER
? customValType
: customValType === RuleType.TEXT &&
secondVal === CustomParams.CUSTOM_DAYS
? RuleType.NUMBER
: customValType === RuleType.TEXT
? customValType
: customValType === RuleType.BOOL
? customValType
: +ruleType
? customValType
: customValType === RuleType.TEXT &&
secondVal === CustomParams.CUSTOM_DAYS
? RuleType.NUMBER
: customValType === RuleType.TEXT
? customValType
: customValType === RuleType.BOOL
? customValType
: +ruleType
: +ruleType,
value: customVal,
},
@@ -247,7 +247,6 @@ const RuleInput = (props: IRuleInput) => {
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [firstval])
useEffect(() => {
@@ -267,13 +266,14 @@ const RuleInput = (props: IRuleInput) => {
} else if (secondVal === CustomParams.CUSTOM_BOOLEAN) {
setCustomValActive(true)
setCustomValType(RuleType.BOOL)
customVal !== '0' ? setCustomVal('1') : undefined
if (customVal !== '0') {
setCustomVal('1')
}
} else {
setCustomValActive(false)
setCustomVal(undefined)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [secondVal])
const getPropFromTuple = (
@@ -299,8 +299,8 @@ const RuleInput = (props: IRuleInput) => {
{props.tagId
? `Rule #${props.tagId}`
: props.id
? `Rule #${props.id}`
: `Rule #1`}
? `Rule #${props.id}`
: `Rule #1`}
</div>
{props.id && props.id > 1 ? (

View File

@@ -39,15 +39,15 @@ const calculateRuleAmount = (
sections: number,
): [number, number[]] => {
const sectionAmounts = [] as number[]
data
? data.rules.forEach((el) =>
el.section !== undefined
? sectionAmounts[el.section]
? sectionAmounts[el.section]++
: (sectionAmounts[el.section] = 1)
: (sectionAmounts[0] = 1),
)
: undefined
if (data) {
data.rules.forEach((el) =>
el.section !== undefined
? sectionAmounts[el.section]
? sectionAmounts[el.section]++
: (sectionAmounts[el.section] = 1)
: (sectionAmounts[0] = 1),
)
}
return [
sections,
@@ -57,12 +57,17 @@ const calculateRuleAmount = (
const calculateRuleAmountArr = (ruleAmount: [number, number[]]) => {
let s = 0,
r = 0,
lenS = ruleAmount[0]
r = 0
const lenS = ruleAmount[0]
const worker: [number[], [number[]]] = [[], [[]]]
while (++s <= lenS) worker[0].push(s), s > 1 ? worker[1].push([]) : undefined
while (++s <= lenS) {
worker[0].push(s)
if (s > 1) {
worker[1].push([])
}
}
for (const sec of worker[0]) {
r = 0
@@ -73,10 +78,10 @@ const calculateRuleAmountArr = (ruleAmount: [number, number[]]) => {
}
const RuleCreator = (props: iRuleCreator) => {
const initialSections = props.editData
? (props.editData.rules[props.editData.rules.length - 1]?.section! + 1 ??
undefined)
: undefined
const initialSections =
props.editData && props.editData.rules.length > 0
? props.editData.rules[props.editData.rules.length - 1].section! + 1
: undefined
const initialRuleAmount: [number, number[]] = initialSections
? calculateRuleAmount(props.editData, initialSections)
: [1, [1]]
@@ -151,11 +156,13 @@ const RuleCreator = (props: iRuleCreator) => {
added.current = [...added.current, ruleId]
rulesCreated.current.map((e) => {
e.id >= ruleId ? (e.id = e.id + 1) : e.id
if (e.id >= ruleId) {
e.id = e.id + 1
}
return e
})
let rules = [...ruleAmount[1]]
const rules = [...ruleAmount[1]]
rules[section - 1] = rules[section - 1] + 1
updateRuleAmount([ruleAmount[0], rules])
@@ -250,7 +257,7 @@ const RuleCreator = (props: iRuleCreator) => {
title={`Add a new section`}
>
{<ClipboardListIcon className="m-auto ml-5 h-5" />}
<p className="button-text m-auto ml-1 mr-5 text-zinc-200">
<p className="button-text m-auto ml-1 mr-5 text-zinc-200">
New Section
</p>
</button>

View File

@@ -1,12 +1,12 @@
export interface IRuleJson {
id: number
ruleJson: string
ruleGroupId: number
isActive: boolean
}
id: number
ruleJson: string
ruleGroupId: number
isActive: boolean
}
const Rule = () => {
return <></>
}
const Rule = () => {
return <></>
}
export default Rule;
export default Rule

View File

@@ -54,7 +54,7 @@ interface ICreateApiObject {
visibleOnHome: boolean
deleteAfterDays: number
manualCollection?: boolean
manualCollectionName?: String
manualCollectionName?: string
keepLogsForMonths?: number
}
rules: IRule[]
@@ -840,9 +840,9 @@ const AddModal = (props: AddModal) => {
Specify the rules this group needs to enforce
</p>
</div>
<div className="ml-auto ">
<div className="ml-auto">
<button
className="ml-3 flex h-fit rounded bg-amber-900 p-1 text-zinc-900 shadow-md hover:bg-amber-800 md:h-10"
className="ml-3 flex h-fit rounded bg-amber-900 p-1 text-zinc-900 shadow-md hover:bg-amber-800 md:h-10"
onClick={toggleCommunityRuleModal}
>
{
@@ -854,9 +854,9 @@ const AddModal = (props: AddModal) => {
</button>
</div>
</div>
<div className="mt-4 flex items-center justify-center sm:justify-end max-width-form-head">
<div className="max-width-form-head mt-4 flex items-center justify-center sm:justify-end">
<button
className="ml-3 flex h-fit rounded bg-amber-600 p-1 text-zinc-900 shadow-md hover:bg-amber-500 md:h-10"
className="ml-3 flex h-fit rounded bg-amber-600 p-1 text-zinc-900 shadow-md hover:bg-amber-500 md:h-10"
onClick={toggleYamlImporter}
>
{
@@ -868,7 +868,7 @@ const AddModal = (props: AddModal) => {
</button>
<button
className="ml-3 flex h-fit rounded bg-amber-900 p-1 text-zinc-900 shadow-md hover:bg-amber-800 md:h-10"
className="ml-3 flex h-fit rounded bg-amber-900 p-1 text-zinc-900 shadow-md hover:bg-amber-800 md:h-10"
onClick={toggleYamlExporter}
>
{

View File

@@ -49,7 +49,7 @@ const RuleGroup = (props: {
return (
<div className="relative mb-5 flex w-full flex-col overflow-hidden rounded-xl bg-zinc-800 bg-cover bg-center p-4 text-zinc-400 shadow ring-1 ring-zinc-700 sm:flex-row">
<div className="relative z-10 flex w-full min-w-0 flex-col pr-4 sm:w-5/6 sm:flex-row">
<div className="mb-3 flex flex-col sm:mb-0 sm:w-5/6 ">
<div className="mb-3 flex flex-col sm:mb-0 sm:w-5/6">
<div className="flex text-xs font-medium text-white">
<div className="overflow-hidden overflow-ellipsis whitespace-nowrap text-base font-bold text-white sm:text-lg">
{props.group.name}
@@ -57,11 +57,13 @@ const RuleGroup = (props: {
</div>
<div className="my-0.5 flex text-sm sm:my-1">
<span className="mr-2 font-bold w-full overflow-hidden overflow-ellipsis">{props.group.description}</span>
<span className="mr-2 w-full overflow-hidden overflow-ellipsis font-bold">
{props.group.description}
</span>
</div>
</div>
<div className="w-full flex-col text-left sm:w-1/6 ">
<div className="w-full flex-col text-left sm:w-1/6">
<span className="text-sm font-medium">Status </span>
{props.group.isActive ? (
<span className="text-sm font-bold text-green-900">Active</span>
@@ -71,7 +73,7 @@ const RuleGroup = (props: {
<div className="m-auto mr-2 flex text-sm font-medium">
{`Library ${
LibrariesCtx.libraries.find(
(el) => +el.key === +props.group.libraryId
(el) => +el.key === +props.group.libraryId,
)?.title
}`}
</div>
@@ -88,7 +90,7 @@ const RuleGroup = (props: {
</div>
<div className="m-auto w-full sm:w-1/6">
<div className="mb-2 flex h-auto ">
<div className="mb-2 flex h-auto">
<EditButton
onClick={onEdit}
text="Edit"

View File

@@ -37,7 +37,7 @@ const Rules: React.FC = () => {
}, [selectedLibrary])
const showAddModal = () => {
addModalActive ? setAddModal(false) : setAddModal(true)
setAddModal(!addModalActive)
}
const onSwitchLibrary = (libraryId: number) => {
@@ -106,11 +106,11 @@ const Rules: React.FC = () => {
<div className="w-full">
<LibrarySwitcher onSwitch={onSwitchLibrary} />
<div className="m-auto mb-5 flex ">
<div className="m-auto mb-5 flex">
<div className="ml-auto sm:ml-0">
<AddButton onClick={showAddModal} text="New Rule" />
</div>
<div className="ml-2 mr-auto sm:mr-0 ">
<div className="ml-2 mr-auto sm:mr-0">
<ExecuteButton
onClick={debounce(sync, 5000, {
leading: true,

View File

@@ -5,7 +5,7 @@ const SettingsLander = () => {
const router = useRouter()
useEffect(() => {
document.title = "Maintainerr - Settings"
document.title = 'Maintainerr - Settings'
router.push('/settings/main')
}, [])

View File

@@ -143,8 +143,8 @@ const MainSettings = () => {
<div className="actions mt-5 w-full">
<div className="flex justify-end">
<div className="w-full flex">
<span className="mr-auto flex rounded-md shadow-sm">
<div className="flex w-full">
<span className="mr-auto flex rounded-md shadow-sm">
<DocsButton />
</span>
<span className="ml-auto flex rounded-md shadow-sm">

View File

@@ -1,10 +1,5 @@
import { SaveIcon } from '@heroicons/react/solid'
import {
useContext,
useEffect,
useRef,
useState,
} from 'react'
import { useContext, useEffect, useRef, useState } from 'react'
import SettingsContext from '../../../contexts/settings-context'
import { PostApiHandler } from '../../../utils/ApiHandler'
import Alert from '../../Common/Alert'
@@ -28,7 +23,7 @@ const OverseerrSettings = () => {
const [error, setError] = useState<boolean>()
const [changed, setChanged] = useState<boolean>()
const [testBanner, setTestbanner] = useState<{
status: Boolean
status: boolean
version: string
}>({ status: false, version: '0' })
@@ -79,9 +74,9 @@ const OverseerrSettings = () => {
? hostnameRef.current.value
: hostnameRef.current.value.includes('https://')
? hostnameRef.current.value
: portRef.current.value == '443'
? 'https://' + hostnameRef.current.value
: 'http://' + hostnameRef.current.value
: portRef.current.value == '443'
? 'https://' + hostnameRef.current.value
: 'http://' + hostnameRef.current.value
const payload = {
overseerr_url: addPortToUrl(hostnameVal, +portRef.current.value),
@@ -173,7 +168,9 @@ const OverseerrSettings = () => {
ref={portRef}
value={portRef.current?.value}
defaultValue={port}
onChange={(e) => handleSettingsInputChange(e, portRef, setPort)}
onChange={(e) =>
handleSettingsInputChange(e, portRef, setPort)
}
></input>
</div>
</div>
@@ -198,10 +195,10 @@ const OverseerrSettings = () => {
<div className="actions mt-5 w-full">
<div className="flex w-full flex-wrap sm:flex-nowrap">
<span className="m-auto rounded-md shadow-sm sm:mr-auto sm:ml-3">
<span className="m-auto rounded-md shadow-sm sm:ml-3 sm:mr-auto">
<DocsButton page="Configuration" />
</span>
<div className="m-auto flex sm:m-0 sm:justify-end mt-3 xs:mt-0">
<div className="m-auto mt-3 flex xs:mt-0 sm:m-0 sm:justify-end">
<TestButton
onClick={appTest}
testUrl="/settings/test/overseerr"

View File

@@ -73,10 +73,10 @@ const PlexSettings = () => {
const serverPresetRef = useRef<HTMLInputElement>(null)
const [error, setError] = useState<boolean>()
const [changed, setChanged] = useState<boolean>()
const [tokenValid, setTokenValid] = useState<Boolean>(false)
const [clearTokenClicked, setClearTokenClicked] = useState<Boolean>(false)
const [tokenValid, setTokenValid] = useState<boolean>(false)
const [clearTokenClicked, setClearTokenClicked] = useState<boolean>(false)
const [testBanner, setTestbanner] = useState<{
status: Boolean
status: boolean
version: string
}>({ status: false, version: '0' })
const [availableServers, setAvailableServers] = useState<PlexDevice[]>()
@@ -92,7 +92,7 @@ const PlexSettings = () => {
e: React.FormEvent<HTMLFormElement> | undefined,
plex_token?: { plex_auth_token: string } | undefined,
) => {
e ? e.preventDefault() : undefined
e?.preventDefault()
if (
hostnameRef.current?.value &&
nameRef.current?.value &&
@@ -500,11 +500,11 @@ const PlexSettings = () => {
</div>
<div className="actions mt-5 w-full">
<div className="flex flex-wrap sm:flex-nowrap w-full">
<span className="m-auto sm:mr-auto sm:ml-3 rounded-md shadow-sm">
<div className="flex w-full flex-wrap sm:flex-nowrap">
<span className="m-auto rounded-md shadow-sm sm:ml-3 sm:mr-auto">
<DocsButton page="Configuration" />
</span>
<div className="flex sm:justify-end m-auto sm:m-0 mt-3 xs:mt-0">
<div className="m-auto mt-3 flex xs:mt-0 sm:m-0 sm:justify-end">
<TestButton onClick={appTest} testUrl="/settings/test/plex" />
<span className="ml-3 inline-flex rounded-md shadow-sm">

View File

@@ -134,7 +134,7 @@ const RadarrSettingsModal = (props: IRadarrSettingsModal) => {
const radarrUrl = constructUrl(port)
if (hostname && port && apiKey && serverName) {
let payload: RadarrSettingSaveRequest = {
const payload: RadarrSettingSaveRequest = {
url: `${radarrUrl}${baseUrl ? `/${baseUrl}` : ''}`,
apiKey: apiKey,
serverName: serverName,
@@ -301,9 +301,7 @@ const RadarrSettingsModal = (props: IRadarrSettingsModal) => {
<div className="form-row">
<label htmlFor="baseUrl" className="text-label">
Base URL
<span className="label-tip">
{`No Leading Slash`}
</span>
<span className="label-tip">{`No Leading Slash`}</span>
</label>
<div className="form-input">
<div className="form-input-field">
@@ -337,7 +335,7 @@ const RadarrSettingsModal = (props: IRadarrSettingsModal) => {
<div className="actions mt-5 w-full">
<div className="flex w-full flex-wrap sm:flex-nowrap">
<span className="m-auto rounded-md shadow-sm sm:mr-auto sm:ml-3">
<span className="m-auto rounded-md shadow-sm sm:ml-3 sm:mr-auto">
<DocsButton page="Configuration" />
</span>
</div>

View File

@@ -119,20 +119,20 @@ const RadarrSettings = () => {
{settings.map((setting) => (
<li
key={setting.id}
className="rounded-xl bg-zinc-800 p-4 text-zinc-400 shadow ring-1 ring-zinc-700 h-full"
className="h-full rounded-xl bg-zinc-800 p-4 text-zinc-400 shadow ring-1 ring-zinc-700"
>
<div className="flex gap-x-3 mb-2 items-center">
<div className="mb-2 flex items-center gap-x-3">
<div className="text-base font-medium text-white sm:text-lg">
{setting.serverName}
</div>
{setting.isDefault && (
<div className="bg-amber-600 px-2 py-0.5 rounded text-zinc-200 shadow-md text-xs">
<div className="rounded bg-amber-600 px-2 py-0.5 text-xs text-zinc-200 shadow-md">
Default
</div>
)}
</div>
<p className="text-gray-300 space-x-2 mb-4 truncate">
<p className="mb-4 space-x-2 truncate text-gray-300">
<span className="font-semibold">Address</span>
<a href={setting.url} className="hover:underline">
{setting.url}
@@ -148,7 +148,7 @@ const RadarrSettings = () => {
}}
>
{<DocumentAddIcon className="m-auto" />}{' '}
<p className="font-semibold m-auto">Edit</p>
<p className="m-auto font-semibold">Edit</p>
</Button>
<DeleteButton
onDeleteRequested={() => confirmedDelete(setting.id)}
@@ -157,14 +157,14 @@ const RadarrSettings = () => {
</li>
))}
<li className="rounded-xl bg-zinc-800 p-4 text-zinc-400 shadow border-2 border-dashed border-gray-400 flex items-center justify-center h-full">
<li className="flex h-full items-center justify-center rounded-xl border-2 border-dashed border-gray-400 bg-zinc-800 p-4 text-zinc-400 shadow">
<button
type="button"
className="add-button bg-amber-600 hover:bg-amber-500 flex m-auto h-9 rounded text-zinc-200 shadow-md px-4"
className="add-button m-auto flex h-9 rounded bg-amber-600 px-4 text-zinc-200 shadow-md hover:bg-amber-500"
onClick={showAddModal}
>
{<PlusCircleIcon className="m-auto h-5" />}
<p className="m-auto font-semibold ml-1">Add server</p>
<p className="m-auto ml-1 font-semibold">Add server</p>
</button>
</li>
</ul>
@@ -190,7 +190,7 @@ const RadarrSettings = () => {
>
<p className="mb-4">
This server is currently being used by the following rules:
<ul className="list-disc list-inside">
<ul className="list-inside list-disc">
{collectionsInUseWarning.map((x) => (
<li key={x.id}>{x.title}</li>
))}
@@ -228,7 +228,7 @@ const DeleteButton = ({
}}
>
{<TrashIcon className="m-auto" />}{' '}
<p className="font-semibold m-auto">
<p className="m-auto font-semibold">
{showSureDelete ? <>Are you sure?</> : <>Delete</>}
</p>
</Button>

View File

@@ -134,7 +134,7 @@ const SonarrSettingsModal = (props: ISonarrSettingsModal) => {
const sonarrUrl = constructUrl(port)
if (hostname && port && apiKey && serverName) {
let payload: SonarrSettingSaveRequest = {
const payload: SonarrSettingSaveRequest = {
url: `${sonarrUrl}${baseUrl ? `/${baseUrl}` : ''}`,
apiKey: apiKey,
serverName: serverName,
@@ -301,9 +301,7 @@ const SonarrSettingsModal = (props: ISonarrSettingsModal) => {
<div className="form-row">
<label htmlFor="baseUrl" className="text-label">
Base URL
<span className="label-tip">
{`No Leading Slash`}
</span>
<span className="label-tip">{`No Leading Slash`}</span>
</label>
<div className="form-input">
<div className="form-input-field">
@@ -337,7 +335,7 @@ const SonarrSettingsModal = (props: ISonarrSettingsModal) => {
<div className="actions mt-5 w-full">
<div className="flex w-full flex-wrap sm:flex-nowrap">
<span className="m-auto rounded-md shadow-sm sm:mr-auto sm:ml-3">
<span className="m-auto rounded-md shadow-sm sm:ml-3 sm:mr-auto">
<DocsButton page="Configuration" />
</span>
</div>

View File

@@ -119,20 +119,20 @@ const SonarrSettings = () => {
{settings.map((setting) => (
<li
key={setting.id}
className="rounded-xl bg-zinc-800 p-4 text-zinc-400 shadow ring-1 ring-zinc-700 h-full"
className="h-full rounded-xl bg-zinc-800 p-4 text-zinc-400 shadow ring-1 ring-zinc-700"
>
<div className="flex gap-x-3 mb-2 items-center">
<div className="mb-2 flex items-center gap-x-3">
<div className="text-base font-medium text-white sm:text-lg">
{setting.serverName}
</div>
{setting.isDefault && (
<div className="bg-amber-600 px-2 py-0.5 rounded text-zinc-200 shadow-md text-xs">
<div className="rounded bg-amber-600 px-2 py-0.5 text-xs text-zinc-200 shadow-md">
Default
</div>
)}
</div>
<p className="text-gray-300 space-x-2 mb-4 truncate">
<p className="mb-4 space-x-2 truncate text-gray-300">
<span className="font-semibold">Address</span>
<a href={setting.url} className="hover:underline">
{setting.url}
@@ -148,7 +148,7 @@ const SonarrSettings = () => {
}}
>
{<DocumentAddIcon className="m-auto" />}{' '}
<p className="font-semibold m-auto">Edit</p>
<p className="m-auto font-semibold">Edit</p>
</Button>
<DeleteButton
onDeleteRequested={() => confirmedDelete(setting.id)}
@@ -157,14 +157,14 @@ const SonarrSettings = () => {
</li>
))}
<li className="rounded-xl bg-zinc-800 p-4 text-zinc-400 shadow border-2 border-dashed border-gray-400 flex items-center justify-center h-full">
<li className="flex h-full items-center justify-center rounded-xl border-2 border-dashed border-gray-400 bg-zinc-800 p-4 text-zinc-400 shadow">
<button
type="button"
className="add-button bg-amber-600 hover:bg-amber-500 flex m-auto h-9 rounded text-zinc-200 shadow-md px-4"
className="add-button m-auto flex h-9 rounded bg-amber-600 px-4 text-zinc-200 shadow-md hover:bg-amber-500"
onClick={showAddModal}
>
{<PlusCircleIcon className="m-auto h-5" />}
<p className="m-auto font-semibold ml-1">Add server</p>
<p className="m-auto ml-1 font-semibold">Add server</p>
</button>
</li>
</ul>
@@ -190,7 +190,7 @@ const SonarrSettings = () => {
>
<p className="mb-4">
This server is currently being used by the following rules:
<ul className="list-disc list-inside">
<ul className="list-inside list-disc">
{collectionsInUseWarning.map((x) => (
<li key={x.id}>{x.title}</li>
))}
@@ -228,7 +228,7 @@ const DeleteButton = ({
}}
>
{<TrashIcon className="m-auto" />}{' '}
<p className="font-semibold m-auto">
<p className="m-auto font-semibold">
{showSureDelete ? <>Are you sure?</> : <>Delete</>}
</p>
</Button>

View File

@@ -22,10 +22,7 @@ export interface ISettingsLink {
const SettingsLink: React.FC<ISettingsLink> = (props: ISettingsLink) => {
if (props.isMobile) {
return (
<option
disabled={props.disabled}
value={props.route}
>
<option disabled={props.disabled} value={props.route}>
{props.children}
</option>
)

View File

@@ -22,7 +22,7 @@ const TautulliSettings = () => {
const [error, setError] = useState<boolean>()
const [changed, setChanged] = useState<boolean>()
const [testBanner, setTestbanner] = useState<{
status: Boolean
status: boolean
version: string
}>({ status: false, version: '0' })
@@ -164,9 +164,7 @@ const TautulliSettings = () => {
<div className="form-row">
<label htmlFor="baseUrl" className="text-label">
Base URL
<span className="label-tip">
{`No Leading Slash`}
</span>
<span className="label-tip">{`No Leading Slash`}</span>
</label>
<div className="form-input">
<div className="form-input-field">
@@ -200,10 +198,10 @@ const TautulliSettings = () => {
<div className="actions mt-5 w-full">
<div className="flex w-full flex-wrap sm:flex-nowrap">
<span className="m-auto rounded-md shadow-sm sm:mr-auto sm:ml-3">
<span className="m-auto rounded-md shadow-sm sm:ml-3 sm:mr-auto">
<DocsButton page="Configuration" />
</span>
<div className="m-auto flex sm:m-0 sm:justify-end mt-3 xs:mt-0">
<div className="m-auto mt-3 flex xs:mt-0 sm:m-0 sm:justify-end">
<TestButton
onClick={appTest}
testUrl="/settings/test/tautulli"

View File

@@ -11,7 +11,7 @@ interface CSSTransitionProps {
leaveFrom?: string
leaveTo?: string
appear?: boolean
children?: React.ReactNode;
children?: React.ReactNode
}
const TransitionContext = React.createContext<{
@@ -47,11 +47,15 @@ const CSSTransition: React.FC<CSSTransitionProps> = ({
const leaveToClasses = leaveTo.split(' ').filter((s) => s.length)
const addClasses = (node: HTMLElement, classes: string[]) => {
classes.length && node.classList.add(...classes)
if (classes.length) {
node.classList.add(...classes)
}
}
const removeClasses = (node: HTMLElement, classes: string[]) => {
classes.length && node.classList.remove(...classes)
if (classes.length) {
node.classList.remove(...classes)
}
}
return (

View File

@@ -7,6 +7,7 @@ import {
CodeIcon,
ServerIcon,
} from '@heroicons/react/outline'
import type { VersionResponse } from '@maintainerr/server/app/dto/version-response.dto'
enum messages {
DEVELOP = 'Maintainerr Develop',
@@ -14,13 +15,6 @@ enum messages {
OUT_OF_DATE = 'Out of Date',
}
interface VersionResponse {
status: 1 | 0
version: string
commitTag: string
updateAvailable: boolean
}
interface VersionStatusProps {
onClick?: () => void
}

View File

@@ -14,7 +14,7 @@ interface IInteractionProvider {
}
export const InteractionProvider: React.FC<IInteractionProvider> = (
props: IInteractionProvider
props: IInteractionProvider,
) => {
const isTouch = useInteraction()

View File

@@ -7,7 +7,7 @@ import {
} from 'react'
export interface ISearch {
text: String
text: string
}
const SearchContext = createContext({

View File

@@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useEffect } from 'react'
/**
* useClickOutside
@@ -11,20 +11,20 @@ import { useEffect } from 'react';
*/
const useClickOutside = (
ref: React.RefObject<HTMLElement>,
callback: (e: MouseEvent) => void
callback: (e: MouseEvent) => void,
): void => {
useEffect(() => {
const handleBodyClick = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) {
callback(e);
callback(e)
}
};
document.body.addEventListener('click', handleBodyClick, { capture: true });
}
document.body.addEventListener('click', handleBodyClick, { capture: true })
return () => {
document.body.removeEventListener('click', handleBodyClick);
};
}, [ref, callback]);
};
document.body.removeEventListener('click', handleBodyClick)
}
}, [ref, callback])
}
export default useClickOutside;
export default useClickOutside

View File

@@ -1,78 +1,78 @@
import { useState, useEffect } from 'react';
import { useState, useEffect } from 'react'
export const INTERACTION_TYPE = {
MOUSE: 'mouse',
PEN: 'pen',
TOUCH: 'touch',
};
}
const UPDATE_INTERVAL = 1000; // Throttle updates to the type to prevent flip flopping
const UPDATE_INTERVAL = 1000 // Throttle updates to the type to prevent flip flopping
const useInteraction = (): boolean => {
const [isTouch, setIsTouch] = useState(false);
const [isTouch, setIsTouch] = useState(false)
useEffect(() => {
const hasTapEvent = 'ontouchstart' in window;
setIsTouch(hasTapEvent);
const hasTapEvent = 'ontouchstart' in window
setIsTouch(hasTapEvent)
let localTouch = hasTapEvent;
let lastTouchUpdate = Date.now();
let localTouch = hasTapEvent
let lastTouchUpdate = Date.now()
const shouldUpdate = (): boolean =>
lastTouchUpdate + UPDATE_INTERVAL < Date.now();
lastTouchUpdate + UPDATE_INTERVAL < Date.now()
const onMouseMove = (): void => {
if (localTouch && shouldUpdate()) {
setTimeout(() => {
if (shouldUpdate()) {
setIsTouch(false);
localTouch = false;
setIsTouch(false)
localTouch = false
}
}, UPDATE_INTERVAL);
}, UPDATE_INTERVAL)
}
};
}
const onTouchStart = (): void => {
lastTouchUpdate = Date.now();
lastTouchUpdate = Date.now()
if (!localTouch) {
setIsTouch(true);
localTouch = true;
setIsTouch(true)
localTouch = true
}
};
}
const onPointerMove = (e: PointerEvent): void => {
const { pointerType } = e;
const { pointerType } = e
switch (pointerType) {
case INTERACTION_TYPE.TOUCH:
case INTERACTION_TYPE.PEN:
return onTouchStart();
return onTouchStart()
default:
return onMouseMove();
return onMouseMove()
}
};
}
if (hasTapEvent) {
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('touchstart', onTouchStart);
window.addEventListener('mousemove', onMouseMove)
window.addEventListener('touchstart', onTouchStart)
} else {
window.addEventListener('pointerdown', onPointerMove);
window.addEventListener('pointermove', onPointerMove);
window.addEventListener('pointerdown', onPointerMove)
window.addEventListener('pointermove', onPointerMove)
}
return () => {
if (hasTapEvent) {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('touchstart', onTouchStart);
window.removeEventListener('mousemove', onMouseMove)
window.removeEventListener('touchstart', onTouchStart)
} else {
window.removeEventListener('pointerdown', onPointerMove);
window.removeEventListener('pointermove', onPointerMove);
window.removeEventListener('pointerdown', onPointerMove)
window.removeEventListener('pointermove', onPointerMove)
}
};
}, []);
}
}, [])
return isTouch;
};
return isTouch
}
export default useInteraction;
export default useInteraction

View File

@@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useEffect } from 'react'
/**
* Hook to lock the body scroll whenever a component is mounted or
@@ -12,17 +12,17 @@ import { useEffect } from 'react';
*/
export const useLockBodyScroll = (
isLocked: boolean,
disabled?: boolean
disabled?: boolean,
): void => {
useEffect(() => {
const originalStyle = window.getComputedStyle(document.body).overflow;
const originalStyle = window.getComputedStyle(document.body).overflow
if (isLocked && !disabled) {
document.body.style.overflow = 'hidden';
document.body.style.overflow = 'hidden'
}
return () => {
if (!disabled) {
document.body.style.overflow = originalStyle;
document.body.style.overflow = originalStyle
}
};
}, [isLocked, disabled]);
};
}
}, [isLocked, disabled])
}

View File

@@ -1,3 +1,3 @@
const CatchAll = () => {}
export default CatchAll
export default CatchAll

View File

@@ -5,4 +5,4 @@ const collections: NextPage = () => {
return <Collection />
}
export default collections
export default collections

View File

@@ -8,7 +8,7 @@ const Home: NextPage = () => {
useEffect(() => {
router.push('/overview')
}, [router])
return <></>
}

View File

@@ -1,13 +1,12 @@
import React from 'react';
import LoadingSpinner from '../../../../components/Common/LoadingSpinner';
import React from 'react'
import LoadingSpinner from '../../../../components/Common/LoadingSpinner'
const PlexLoading: React.FC = () => {
return (
<div>
<LoadingSpinner />
</div>
);
};
)
}
export default PlexLoading;
export default PlexLoading

View File

@@ -5,4 +5,4 @@ const overview: NextPage = () => {
return <Overview />
}
export default overview
export default overview

View File

@@ -5,4 +5,4 @@ const rules: NextPage = () => {
return <Rules />
}
export default rules
export default rules

View File

@@ -31,7 +31,7 @@ class PlexOAuth {
public initializeHeaders(): void {
if (!window) {
throw new Error(
'Window is not defined. Are you calling this in the browser?'
'Window is not defined. Are you calling this in the browser?',
)
}
const browser = getParser(window.navigator.userAgent)
@@ -54,13 +54,13 @@ class PlexOAuth {
public async getPin(): Promise<PlexPin> {
if (!this.plexHeaders) {
throw new Error(
'You must initialize the plex headers clientside to login'
'You must initialize the plex headers clientside to login',
)
}
const response = await axios.post(
'https://plex.tv/api/v2/pins?strong=true',
undefined,
{ headers: this.plexHeaders }
{ headers: this.plexHeaders },
)
this.pin = { id: response.data.id, code: response.data.code }
@@ -98,7 +98,7 @@ class PlexOAuth {
if (this.popup) {
this.popup.location.href = `https://app.plex.tv/auth/#!?${this.encodeData(
params
params,
)}`
}
@@ -108,7 +108,7 @@ class PlexOAuth {
private async pinPoll(): Promise<string> {
const executePoll = async (
resolve: (authToken: string) => void,
reject: (e: Error) => void
reject: (e: Error) => void,
) => {
try {
if (!this.pin) {
@@ -117,7 +117,7 @@ class PlexOAuth {
const response = await axios.get(
`https://plex.tv/api/v2/pins/${this.pin.id}`,
{ headers: this.plexHeaders }
{ headers: this.plexHeaders },
)
if (response.data?.authToken) {
@@ -154,7 +154,7 @@ class PlexOAuth {
}): Window | void {
if (!window) {
throw new Error(
'Window is undefined. Are you running this in the browser?'
'Window is undefined. Are you running this in the browser?',
)
}
// Fixes dual-screen position Most browsers Firefox
@@ -165,13 +165,13 @@ class PlexOAuth {
const width = window.innerWidth
? window.innerWidth
: document.documentElement.clientWidth
? document.documentElement.clientWidth
: screen.width
? document.documentElement.clientWidth
: screen.width
const height = window.innerHeight
? window.innerHeight
: document.documentElement.clientHeight
? document.documentElement.clientHeight
: screen.height
? document.documentElement.clientHeight
: screen.height
const left = width / 2 - w / 2 + dualScreenLeft
const top = height / 2 - h / 2 + dualScreenTop
@@ -186,7 +186,7 @@ class PlexOAuth {
', top=' +
top +
', left=' +
left
left,
)
if (newWindow) {
newWindow.focus()

View File

@@ -1,9 +1,8 @@
// these numbers equal the Plex API media types
// Note: if changes are made, also change this in SERVER's EPlexDataType
export enum EPlexDataType {
MOVIES = 1,
SHOWS = 2,
SEASONS = 3,
EPISODES = 4,
}
MOVIES = 1,
SHOWS = 2,
SEASONS = 3,
EPISODES = 4,
}

View File

@@ -44,7 +44,7 @@ export function getPortFromUrl(url: string): string | undefined {
export function getHostname(url: string): string | undefined {
try {
const urlObject = new URL(url)
let baseUrl = urlObject.protocol + '//' + urlObject.hostname
const baseUrl = urlObject.protocol + '//' + urlObject.hostname
return baseUrl
} catch (error) {
console.error('Invalid URL:', error.message)

View File

@@ -61,8 +61,15 @@
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
font-family:
Menlo,
Monaco,
Lucida Console,
Liberation Mono,
DejaVu Sans Mono,
Bitstream Vera Sans Mono,
Courier New,
monospace;
}
.grid {
@@ -81,7 +88,9 @@
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
transition:
color 0.15s ease,
border-color 0.15s ease;
max-width: 300px;
}

View File

@@ -135,15 +135,11 @@
}
.edit-button {
@apply /*
background-color: #3f3f46; */ bg-amber-900;
@apply /* background-color: #3f3f46; */ bg-amber-900;
}
.edit-button:hover {
@apply /*
background: #3a3a42; */ bg-amber-800;
@apply /* background: #3a3a42; */ bg-amber-800;
}
.delete-button {
@@ -177,7 +173,7 @@
}
.slider-header {
@apply relative mt-6 mb-4 flex;
@apply relative mb-4 mt-6 flex;
}
.slider-title {
@@ -244,7 +240,7 @@
}
.media-overview {
@apply flex flex-col pt-8 pb-4 text-white lg:flex-row;
@apply flex flex-col pb-4 pt-8 text-white lg:flex-row;
}
.media-overview-left {
@@ -302,7 +298,7 @@
}
.error-message {
@apply relative top-0 bottom-0 left-0 right-0 flex h-screen flex-col items-center justify-center text-center text-zinc-300;
@apply relative bottom-0 left-0 right-0 top-0 flex h-screen flex-col items-center justify-center text-center text-zinc-300;
}
.heading {
@@ -338,7 +334,7 @@
}
.section {
@apply mt-6 mb-10 text-white;
@apply mb-10 mt-6 text-white;
}
.form-row {
@@ -346,7 +342,7 @@
}
.form-input {
@apply text-sm text-white sm:col-span-2 border-0 bg-inherit;
@apply border-0 bg-inherit text-sm text-white sm:col-span-2;
}
.form-input-field {
@@ -548,7 +544,7 @@
.react-select-container .react-select__input-container,
.react-select-container .react-select__placeholder,
.react-select-container .react-select__single-value {
@apply text-white font-semibold;
@apply font-semibold text-white;
}
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-require-imports */
const defaultTheme = require('tailwindcss/defaultTheme')
module.exports = {

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "es2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
@@ -9,7 +9,7 @@
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"moduleResolution": "bundler",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"useUnknownInCatchVariables": false,
@@ -17,8 +17,13 @@
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
"incremental": true,
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

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