Compare commits

..

1 Commits

Author SHA1 Message Date
snyk-bot
d06ae72c08 fix: package.json & yarn.lock to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-PATHTOREGEXP-7925106
2024-09-18 11:42:11 +00:00
84 changed files with 13093 additions and 3608 deletions

View File

@@ -25,7 +25,7 @@ steps:
path: /cache path: /cache
- name: Frontend install - name: Frontend install
image: node:24.13.1 image: node:18.2.0
commands: commands:
- node -v - node -v
- yarn --version - yarn --version
@@ -42,14 +42,8 @@ steps:
- name: cache - name: cache
path: /cache path: /cache
- name: Lint project using eslint
image: node:24.13.1
commands:
- yarn lint
failure: ignore
- name: Frontend build - name: Frontend build
image: node:24.13.1 image: node:18.2.0
commands: commands:
- yarn build - yarn build
environment: environment:
@@ -62,6 +56,12 @@ steps:
SEASONED_DOMAIN: SEASONED_DOMAIN:
from_secret: SEASONED_DOMAIN from_secret: SEASONED_DOMAIN
- name: Lint project using eslint
image: node:18.2.0
commands:
- yarn lint
failure: ignore
- name: Build and publish docker image - name: Build and publish docker image
image: plugins/docker image: plugins/docker
settings: settings:
@@ -105,8 +105,3 @@ trigger:
include: include:
- push - push
# - pull_request # - pull_request
---
kind: signature
hmac: 6f10b2871d2bd6b5cd26ddf72796325991ba211ba1eb62b657baf993e9d549c8
...

View File

@@ -1,3 +1,4 @@
SEASONED_API=http://localhost:31459 SEASONED_API=
ELASTIC_URL=http://elastic.local:9200/tmdb-movies-shows ELASTIC=
ELASTIC_API_KEY= ELASTIC_INDEX=shows,movies
SEASONED_DOMAIN=

31
.eslintrc Normal file
View File

@@ -0,0 +1,31 @@
{
"root": true,
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": "@typescript-eslint/parser",
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"extends": [
"@vue/eslint-config-airbnb",
"plugin:vue/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
],
"rules": {
"vue/no-v-model-argument": "off",
"no-underscore-dangle": "off",
"vue/multi-word-component-names": "off",
"no-shadow": "off",
"@typescript-eslint/no-shadow": ["error"],
},
"settings": {
"import/resolver": {
webpack: {
config: "./webpack.config.js"
}
}
}
}

1
.gitignore vendored
View File

@@ -4,7 +4,6 @@ src/config.json
# Build directory # Build directory
dist/ dist/
lib/
# Node packages # Node packages
node_modules/ node_modules/

View File

@@ -1,31 +0,0 @@
{
# Disable automatic HTTPS
auto_https off
}
:8080 {
root * {$DIST_PATH:/usr/share/caddy}
file_server
encode gzip zstd
try_files {path} {path}/ /index.html
# Cache favicons aggressively
@favicons path /favicons/*
header @favicons Cache-Control "public, max-age=31536000, immutable"
# Cache static assets based on MIME type
@static {
header Content-Type application/javascript*
header Content-Type text/css*
header Content-Type image/*
header Content-Type font/*
header Content-Type application/font-*
header Content-Type application/woff*
header Content-Type application/json*
}
header @static Cache-Control "public, max-age=2592000, immutable"
}

View File

@@ -1,38 +1,11 @@
FROM node:24.13.1 AS build FROM nginx:latest
# Set the working directory for the build stage COPY public /usr/share/nginx/html
WORKDIR /app COPY nginx.conf /etc/nginx/conf.d/default.conf.template
COPY docker-entrypoint.sh /docker-entrypoint.d/05-docker-entrypoint.sh
# Install dependencies RUN chmod +x /docker-entrypoint.d/05-docker-entrypoint.sh
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
# Copy source files that the build depends on EXPOSE 5000
COPY index.html .
COPY public/ public/
COPY src/ src/
COPY tsconfig.json vite.config.ts ./
ARG SEASONED_API=http://localhost:31459
ENV VITE_SEASONED_API=$SEASONED_API
ARG ELASTIC_URL=http://elastic.local:9200/tmdb-movies-shows
ENV VITE_ELASTIC_URL=$ELASTIC_URL
ARG ELASTIC_API_KEY=
ENV VITE_ELASTIC_API_KEY=$ELASTIC_API_KEY
RUN yarn build
FROM caddy:2.11-alpine
COPY Caddyfile /etc/caddy/Caddyfile
# Copy static files
COPY public /usr/share/caddy
# Copy the static build from the previous stage
COPY --from=build /app/dist /usr/share/caddy
EXPOSE 8080
LABEL org.opencontainers.image.source https://github.com/kevinmidboe/seasoned LABEL org.opencontainers.image.source https://github.com/kevinmidboe/seasoned

9
docker-entrypoint.sh Normal file
View File

@@ -0,0 +1,9 @@
#!/bin/sh
set -eu
export SEASONED_API=${SEASONED_API:-http://localhost:31459}
export SEASONED_DOMAIN=${SEASONED_DOMAIN:-localhost}
envsubst '$SEASONED_API,$SEASONED_DOMAIN' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
exec "$@"

View File

@@ -1,66 +0,0 @@
import path from "node:path";
import { includeIgnoreFile } from "@eslint/compat";
import js from "@eslint/js";
import { defineConfig } from "eslint/config";
import { configs, plugins } from "eslint-config-airbnb-extended";
import { rules as prettierConfigRules } from "eslint-config-prettier";
import prettierPlugin from "eslint-plugin-prettier";
const CUSTOM_RULES = {
"vue/no-v-model-argument": "off",
"no-underscore-dangle": "off",
"vue/multi-word-component-names": "off"
};
const gitignorePath = path.resolve(".", ".gitignore");
// ESLint recommended config
const jsConfig = defineConfig([
{
name: "js/config",
...js.configs.recommended
},
plugins.stylistic,
plugins.importX,
...configs.base.recommended // Airbnb base recommended config
]);
// Node & Airbnb recommended config
const nodeConfig = defineConfig([plugins.node, ...configs.node.recommended]);
// Typescript & Airbnb base TS config
const typescriptConfig = defineConfig([
plugins.typescriptEslint,
...configs.base.typescript
// rules.typescript.typescriptEslintStrict
]);
// Prettier config
const prettierConfig = defineConfig([
{
name: "prettier/plugin/config",
plugins: {
prettier: prettierPlugin
}
},
{
name: "prettier/config",
rules: {
...prettierConfigRules,
"prettier/prettier": "error"
}
}
]);
export default defineConfig([
// Ignore files and folders listed in .gitignore
includeIgnoreFile(gitignorePath),
...jsConfig,
...nodeConfig,
...typescriptConfig,
...prettierConfig,
{
rules: CUSTOM_RULES
}
]);

30
nginx.conf Normal file
View File

@@ -0,0 +1,30 @@
server {
listen 5000 default_server;
listen [::]:5000 default_server;
server_name $SEASONED_DOMAIN;
root /usr/share/nginx/html;
gzip on;
gzip_types application/javascript;
gzip_min_length 1000;
gzip_static on;
location /favicons {
autoindex on;
}
location /dist {
add_header Content-Type application/javascript;
try_files $uri =404;
}
location /api {
proxy_pass $SEASONED_API;
}
location / {
try_files $uri $uri/ /index.html;
index index.html;
}
}

View File

@@ -5,30 +5,57 @@
"author": "Kevin Midboe", "author": "Kevin Midboe",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "NODE_ENV=development vite", "dev": "NODE_ENV=development webpack server",
"build": "yarn vite build", "build": "yarn build:ts && yarn build:webpack",
"lint": "eslint src; prettier -c src", "build:ts": "tsc --project tsconfig.json",
"clean": "rm -rf dist/ yarn-*.log 2>/dev/null", "build:webpack": "NODE_ENV=production webpack-cli build --progress",
"postbuild": "cp public/dist/index.html public/index.html",
"clean": "rm -r public/dist 2> /dev/null; rm public/index.html 2> /dev/null; rm -r lib 2> /dev/null",
"start": "echo 'Start using docker, consult README'",
"lint": "eslint src --ext .ts,.vue",
"docs": "documentation build src/api.ts -f html -o docs/api && documentation build src/api.ts -f md -o docs/api.md" "docs": "documentation build src/api.ts -f html -o docs/api && documentation build src/api.ts -f md -o docs/api.md"
}, },
"dependencies": { "dependencies": {
"chart.js": "3.9.1", "chart.js": "3.9.1",
"vue": "3.5.28", "connect-history-api-fallback": "2.0.0",
"vue-router": "5.0.3", "dotenv": "^16.0.1",
"vuex": "4.1.0" "express": "4.20.0",
"vue": "3.2.37",
"vue-router": "4.1.3",
"vuex": "4.0.2"
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^2.0.2", "@babel/core": "7.18.10",
"@eslint/js": "^10.0.1", "@babel/plugin-transform-runtime": "7.18.10",
"@types/node": "^25.3.0", "@babel/preset-env": "7.18.10",
"@vitejs/plugin-vue": "^5.2.1", "@babel/runtime": "7.18.9",
"eslint": "^10.0.1", "@types/express": "4.17.13",
"eslint-config-airbnb-extended": "^3.0.1", "@types/node": "18.6.1",
"eslint-config-prettier": "^10.1.8", "@typescript-eslint/eslint-plugin": "5.33.0",
"eslint-plugin-prettier": "^5.5.5", "@typescript-eslint/parser": "5.33.0",
"prettier": "^3.8.1", "@vue/cli": "5.0.8",
"@vue/cli-service": "5.0.8",
"@vue/eslint-config-airbnb": "6.0.0",
"babel-loader": "8.2.5",
"css-loader": "6.7.1",
"documentation": "13.2.5",
"eslint": "8.21.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-vue": "9.3.0",
"eslint-plugin-vuejs-accessibility": "1.2.0",
"file-loader": "6.2.0",
"html-webpack-plugin": "5.5.0",
"prettier": "2.7.1",
"sass": "1.54.3", "sass": "1.54.3",
"typescript": "5.9.3", "sass-loader": "13.0.2",
"vite": "^6.0.3" "terser-webpack-plugin": "5.3.3",
"ts-loader": "9.3.1",
"typescript": "4.7.4",
"vue-loader": "17.0.0",
"webpack": "5.74.0",
"webpack-cli": "4.10.0",
"webpack-dev-server": "4.9.3"
} }
} }

View File

@@ -1,5 +1,5 @@
<template> <template>
<div id="content"> <div id="app">
<!-- Header and hamburger navigation --> <!-- Header and hamburger navigation -->
<NavigationHeader class="header" /> <NavigationHeader class="header" />
@@ -28,13 +28,13 @@
</script> </script>
<style lang="scss"> <style lang="scss">
@import "scss/main"; @import "src/scss/main";
@import "scss/media-queries"; @import "src/scss/media-queries";
#content { #app {
display: grid; display: grid;
grid-template-rows: var(--header-size); grid-template-rows: var(--header-size);
grid-template-columns: var(--header-size) 100%; grid-template-columns: var(--header-size) 1fr;
@include mobile { @include mobile {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -59,13 +59,11 @@
.content { .content {
display: grid; display: grid;
grid-column: 2 / 3; grid-column: 2 / 3;
width: calc(100% - var(--header-size));
grid-row: 2; grid-row: 2;
z-index: 5; z-index: 5;
@include mobile { @include mobile {
grid-column: 1 / 3; grid-column: 1 / 3;
width: 100%;
} }
} }
} }

View File

@@ -1,31 +1,29 @@
/* eslint-disable n/no-unsupported-features/node-builtins */ import { IList, IMediaCredits, IPersonCredits } from "./interfaces/IList";
import {
IList,
IMediaCredits,
IPersonCredits,
MediaTypes
} from "./interfaces/IList";
import type { import type {
IRequestStatusResponse, IRequestStatusResponse,
IRequestSubmitResponse IRequestSubmitResponse
} from "./interfaces/IRequestResponse"; } from "./interfaces/IRequestResponse";
const API_HOSTNAME = import.meta.env.VITE_SEASONED_API; const { ELASTIC, ELASTIC_INDEX } = process.env;
const ELASTIC_URL = import.meta.env.VITE_ELASTIC_URL; const API_HOSTNAME = window.location.origin;
const ELASTIC_API_KEY = import.meta.env.VITE_ELASTIC_API_KEY;
// - - - TMDB - - - // - - - TMDB - - -
interface GetMediaOpts {
checkExistance: boolean;
credits: boolean;
releaseDates?: boolean;
}
const getMovie = async (id: number, opts: GetMediaOpts) => { /**
* Fetches tmdb movie by id. Can optionally include cast credits in result object.
* @param {number} id
* @returns {object} Tmdb response
*/
const getMovie = (
id,
{
checkExistance,
credits,
releaseDates
}: { checkExistance: boolean; credits: boolean; releaseDates?: boolean }
) => {
const url = new URL("/api/v2/movie", API_HOSTNAME); const url = new URL("/api/v2/movie", API_HOSTNAME);
url.pathname = `${url.pathname}/${id.toString()}`; url.pathname = `${url.pathname}/${id.toString()}`;
const { checkExistance, credits, releaseDates } = opts;
if (checkExistance) { if (checkExistance) {
url.searchParams.append("check_existance", "true"); url.searchParams.append("check_existance", "true");
} }
@@ -44,12 +42,22 @@ const getMovie = async (id: number, opts: GetMediaOpts) => {
}); });
}; };
// Fetches tmdb show by id. Can optionally include cast credits in result object. /**
const getShow = async (id: number, opts: GetMediaOpts) => { * Fetches tmdb show by id. Can optionally include cast credits in result object.
* @param {number} id
* @param {boolean} [credits=false] Include credits
* @returns {object} Tmdb response
*/
const getShow = (
id,
{
checkExistance,
credits,
releaseDates
}: { checkExistance: boolean; credits: boolean; releaseDates?: boolean }
) => {
const url = new URL("/api/v2/show", API_HOSTNAME); const url = new URL("/api/v2/show", API_HOSTNAME);
url.pathname = `${url.pathname}/${id.toString()}`; url.pathname = `${url.pathname}/${id.toString()}`;
const { checkExistance, credits, releaseDates } = opts;
if (checkExistance) { if (checkExistance) {
url.searchParams.append("check_existance", "true"); url.searchParams.append("check_existance", "true");
} }
@@ -68,8 +76,13 @@ const getShow = async (id: number, opts: GetMediaOpts) => {
}); });
}; };
// Fetches tmdb person by id. Can optionally include cast credits in result object. /**
const getPerson = async (id: number, credits = false) => { * Fetches tmdb person by id. Can optionally include cast credits in result object.
* @param {number} id
* @param {boolean} [credits=false] Include credits
* @returns {object} Tmdb response
*/
const getPerson = (id, credits = false) => {
const url = new URL("/api/v2/person", API_HOSTNAME); const url = new URL("/api/v2/person", API_HOSTNAME);
url.pathname = `${url.pathname}/${id.toString()}`; url.pathname = `${url.pathname}/${id.toString()}`;
if (credits) { if (credits) {
@@ -84,8 +97,12 @@ const getPerson = async (id: number, credits = false) => {
}); });
}; };
// Fetches tmdb movie credits by id. /**
const getMovieCredits = async (id: number): Promise<IMediaCredits> => { * Fetches tmdb movie credits by id.
* @param {number} id
* @returns {object} Tmdb response
*/
const getMovieCredits = (id: number): Promise<IMediaCredits> => {
const url = new URL("/api/v2/movie", API_HOSTNAME); const url = new URL("/api/v2/movie", API_HOSTNAME);
url.pathname = `${url.pathname}/${id.toString()}/credits`; url.pathname = `${url.pathname}/${id.toString()}/credits`;
@@ -97,8 +114,12 @@ const getMovieCredits = async (id: number): Promise<IMediaCredits> => {
}); });
}; };
// Fetches tmdb show credits by id. /**
const getShowCredits = async (id: number): Promise<IMediaCredits> => { * Fetches tmdb show credits by id.
* @param {number} id
* @returns {object} Tmdb response
*/
const getShowCredits = (id: number): Promise<IMediaCredits> => {
const url = new URL("/api/v2/show", API_HOSTNAME); const url = new URL("/api/v2/show", API_HOSTNAME);
url.pathname = `${url.pathname}/${id.toString()}/credits`; url.pathname = `${url.pathname}/${id.toString()}/credits`;
@@ -110,8 +131,12 @@ const getShowCredits = async (id: number): Promise<IMediaCredits> => {
}); });
}; };
// Fetches tmdb person credits by id. /**
const getPersonCredits = async (id: number): Promise<IPersonCredits> => { * Fetches tmdb person credits by id.
* @param {number} id
* @returns {object} Tmdb response
*/
const getPersonCredits = (id: number): Promise<IPersonCredits> => {
const url = new URL("/api/v2/person", API_HOSTNAME); const url = new URL("/api/v2/person", API_HOSTNAME);
url.pathname = `${url.pathname}/${id.toString()}/credits`; url.pathname = `${url.pathname}/${id.toString()}/credits`;
@@ -123,11 +148,13 @@ const getPersonCredits = async (id: number): Promise<IPersonCredits> => {
}); });
}; };
// Fetches tmdb list by name. /**
const getTmdbMovieListByName = async ( * Fetches tmdb list by name.
name: string, * @param {string} name List the fetch
page = 1 * @param {number} [page=1]
): Promise<IList> => { * @returns {object} Tmdb list response
*/
const getTmdbMovieListByName = (name: string, page = 1): Promise<IList> => {
const url = new URL(`/api/v2/movie/${name}`, API_HOSTNAME); const url = new URL(`/api/v2/movie/${name}`, API_HOSTNAME);
url.searchParams.append("page", page.toString()); url.searchParams.append("page", page.toString());
@@ -135,8 +162,12 @@ const getTmdbMovieListByName = async (
// .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error }) // eslint-disable-line no-console // .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error }) // eslint-disable-line no-console
}; };
// Fetches requested items. /**
const getRequests = async (page = 1) => { * Fetches requested items.
* @param {number} [page=1]
* @returns {object} Request response
*/
const getRequests = (page = 1) => {
const url = new URL("/api/v2/request", API_HOSTNAME); const url = new URL("/api/v2/request", API_HOSTNAME);
url.searchParams.append("page", page.toString()); url.searchParams.append("page", page.toString());
@@ -144,25 +175,20 @@ const getRequests = async (page = 1) => {
// .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error }) // eslint-disable-line no-console // .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error }) // eslint-disable-line no-console
}; };
const getUserRequests = async (page = 1) => { const getUserRequests = (page = 1) => {
const url = new URL("/api/v1/user/requests", API_HOSTNAME); const url = new URL("/api/v1/user/requests", API_HOSTNAME);
url.searchParams.append("page", page.toString()); url.searchParams.append("page", page.toString());
const options: RequestInit = { return fetch(url.href).then(resp => resp.json());
headers: { "Content-Type": "application/json" },
credentials: "include"
}; };
return fetch(url.href, options).then(resp => resp.json()); /**
}; * Fetches tmdb movies and shows by query.
* @param {string} query
// Fetches tmdb movies and shows by query. * @param {number} [page=1]
const searchTmdb = async ( * @returns {object} Tmdb response
query: string, */
page = 1, const searchTmdb = (query, page = 1, adult = false, mediaType = null) => {
adult = false,
mediaType = null
) => {
const url = new URL("/api/v2/search", API_HOSTNAME); const url = new URL("/api/v2/search", API_HOSTNAME);
if (mediaType != null && ["movie", "show", "person"].includes(mediaType)) { if (mediaType != null && ["movie", "show", "person"].includes(mediaType)) {
url.pathname += `/${mediaType}`; url.pathname += `/${mediaType}`;
@@ -182,15 +208,17 @@ const searchTmdb = async (
// - - - Torrents - - - // - - - Torrents - - -
// Search for torrents by query /**
const searchTorrents = async (query: string) => { * Search for torrents by query
* @param {string} query
* @param {boolean} credits Include credits
* @returns {object} Torrent response
*/
const searchTorrents = query => {
const url = new URL("/api/v1/pirate/search", API_HOSTNAME); const url = new URL("/api/v1/pirate/search", API_HOSTNAME);
url.searchParams.append("query", query); url.searchParams.append("query", query);
const options: RequestInit = {
credentials: "include"
};
return fetch(url.href, options) return fetch(url.href)
.then(resp => resp.json()) .then(resp => resp.json())
.catch(error => { .catch(error => {
console.error(`api error searching torrents: ${query}`); // eslint-disable-line no-console console.error(`api error searching torrents: ${query}`); // eslint-disable-line no-console
@@ -198,18 +226,19 @@ const searchTorrents = async (query: string) => {
}); });
}; };
// Add magnet to download queue. /**
const addMagnet = async ( * Add magnet to download queue.
magnet: string, * @param {string} magnet Magnet link
name: string, * @param {boolean} name Name of torrent
tmdbId: number | null * @param {boolean} tmdbId
) => { * @returns {object} Success/Failure response
*/
const addMagnet = (magnet: string, name: string, tmdbId: number | null) => {
const url = new URL("/api/v1/pirate/add", API_HOSTNAME); const url = new URL("/api/v1/pirate/add", API_HOSTNAME);
const options: RequestInit = { const options = {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ body: JSON.stringify({
magnet, magnet,
name, name,
@@ -227,11 +256,14 @@ const addMagnet = async (
// - - - Plex/Request - - - // - - - Plex/Request - - -
// Request a movie or show from id. If authorization token is included the user will be linked /**
const request = async ( * Request a movie or show from id. If authorization token is included the user will be linked
id: number, * to the requested item.
type: MediaTypes.Movie | MediaTypes.Show * @param {number} id Movie or show id
): Promise<IRequestSubmitResponse> => { * @param {string} type Movie or show type
* @returns {object} Success/Failure response
*/
const request = (id, type): Promise<IRequestSubmitResponse> => {
const url = new URL("/api/v2/request", API_HOSTNAME); const url = new URL("/api/v2/request", API_HOSTNAME);
const options = { const options = {
@@ -248,11 +280,13 @@ const request = async (
}); });
}; };
// Check request status by tmdb id and type /**
const getRequestStatus = async ( * Check request status by tmdb id and type
id: number, * @param {number} tmdb id
type = null * @param {string} type
): Promise<IRequestStatusResponse> => { * @returns {object} Success/Failure response
*/
const getRequestStatus = (id, type = null): Promise<IRequestStatusResponse> => {
const url = new URL("/api/v2/request", API_HOSTNAME); const url = new URL("/api/v2/request", API_HOSTNAME);
url.pathname = `${url.pathname}/${id.toString()}`; url.pathname = `${url.pathname}/${id.toString()}`;
url.searchParams.append("type", type); url.searchParams.append("type", type);
@@ -262,8 +296,7 @@ const getRequestStatus = async (
.catch(err => Promise.reject(err)); .catch(err => Promise.reject(err));
}; };
/* const watchLink = (title, year) => {
const watchLink = async (title, year) => {
const url = new URL("/api/v1/plex/watch-link", API_HOSTNAME); const url = new URL("/api/v1/plex/watch-link", API_HOSTNAME);
url.searchParams.append("title", title); url.searchParams.append("title", title);
url.searchParams.append("year", year); url.searchParams.append("year", year);
@@ -278,16 +311,14 @@ const movieImages = id => {
return fetch(url.href).then(resp => resp.json()); return fetch(url.href).then(resp => resp.json());
}; };
*/
// - - - Seasoned user endpoints - - - // - - - Seasoned user endpoints - - -
const register = async (username: string, password: string) => { const register = (username, password) => {
const url = new URL("/api/v1/user", API_HOSTNAME); const url = new URL("/api/v1/user", API_HOSTNAME);
const options: RequestInit = { const options = {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ username, password }) body: JSON.stringify({ username, password })
}; };
@@ -303,49 +334,40 @@ const register = async (username: string, password: string) => {
}); });
}; };
const login = async ( const login = (username, password, throwError = false) => {
username: string,
password: string,
throwError = false
) => {
const url = new URL("/api/v1/user/login", API_HOSTNAME); const url = new URL("/api/v1/user/login", API_HOSTNAME);
const options: RequestInit = { const options = {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ username, password }) body: JSON.stringify({ username, password })
}; };
return fetch(url.href, options).then(resp => { return fetch(url.href, options).then(resp => {
if (resp.status === 200) return resp.json(); if (resp.status === 200) return resp.json();
if (throwError) return Promise.reject(resp.text().then(t => new Error(t))); if (throwError) throw resp;
console.error("Error occured when trying to sign in.\nError:", resp); // eslint-disable-line no-console console.error("Error occured when trying to sign in.\nError:", resp); // eslint-disable-line no-console
return Promise.reject(resp); return Promise.reject(resp);
}); });
}; };
const logout = async (throwError = false) => { const logout = (throwError = false) => {
const url = new URL("/api/v1/user/logout", API_HOSTNAME); const url = new URL("/api/v1/user/logout", API_HOSTNAME);
const options: RequestInit = { method: "POST", credentials: "include" }; const options = { method: "POST" };
return fetch(url.href, options).then(resp => { return fetch(url.href, options).then(resp => {
if (resp.status === 200) return resp.json(); if (resp.status === 200) return resp.json();
if (throwError) return Promise.reject(resp.text().then(t => new Error(t))); if (throwError) throw resp;
console.error("Error occured when trying to log out.\nError:", resp); // eslint-disable-line no-console console.error("Error occured when trying to log out.\nError:", resp); // eslint-disable-line no-console
return Promise.reject(resp); return Promise.reject(resp);
}); });
}; };
const getSettings = async () => { const getSettings = () => {
const url = new URL("/api/v1/user/settings", API_HOSTNAME); const url = new URL("/api/v1/user/settings", API_HOSTNAME);
const options: RequestInit = {
headers: { "Content-Type": "application/json" },
credentials: "include"
};
return fetch(url.href, options) return fetch(url.href)
.then(resp => resp.json()) .then(resp => resp.json())
.catch(error => { .catch(error => {
console.log("api error getting user settings"); // eslint-disable-line no-console console.log("api error getting user settings"); // eslint-disable-line no-console
@@ -353,13 +375,12 @@ const getSettings = async () => {
}); });
}; };
const updateSettings = async (settings: any) => { const updateSettings = settings => {
const url = new URL("/api/v1/user/settings", API_HOSTNAME); const url = new URL("/api/v1/user/settings", API_HOSTNAME);
const options: RequestInit = { const options = {
method: "PUT", method: "PUT",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify(settings) body: JSON.stringify(settings)
}; };
@@ -373,14 +394,13 @@ const updateSettings = async (settings: any) => {
// - - - Authenticate with plex - - - // - - - Authenticate with plex - - -
const linkPlexAccount = async (username: string, password: string) => { const linkPlexAccount = (username, password) => {
const url = new URL("/api/v1/user/link_plex", API_HOSTNAME); const url = new URL("/api/v1/user/link_plex", API_HOSTNAME);
const body = { username, password }; const body = { username, password };
const options: RequestInit = { const options = {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify(body) body: JSON.stringify(body)
}; };
@@ -392,12 +412,12 @@ const linkPlexAccount = async (username: string, password: string) => {
}); });
}; };
const unlinkPlexAccount = async () => { const unlinkPlexAccount = () => {
const url = new URL("/api/v1/user/unlink_plex", API_HOSTNAME); const url = new URL("/api/v1/user/unlink_plex", API_HOSTNAME);
const options: RequestInit = {
const options = {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" }
credentials: "include"
}; };
return fetch(url.href, options) return fetch(url.href, options)
@@ -410,21 +430,12 @@ const unlinkPlexAccount = async () => {
// - - - User graphs - - - // - - - User graphs - - -
const fetchGraphData = async ( const fetchGraphData = (urlPath, days, chartType) => {
urlPath: string,
days: number,
chartType: string
) => {
const url = new URL(`/api/v1/user/${urlPath}`, API_HOSTNAME); const url = new URL(`/api/v1/user/${urlPath}`, API_HOSTNAME);
url.searchParams.append("days", String(days)); url.searchParams.append("days", days);
url.searchParams.append("y_axis", chartType); url.searchParams.append("y_axis", chartType);
const options: RequestInit = { return fetch(url.href).then(resp => {
headers: { "Content-Type": "application/json" },
credentials: "include"
};
return fetch(url.href, options).then(resp => {
if (!resp.ok) { if (!resp.ok) {
console.log("DAMN WE FAILED!", resp); // eslint-disable-line no-console console.log("DAMN WE FAILED!", resp); // eslint-disable-line no-console
throw Error(resp.statusText); throw Error(resp.statusText);
@@ -436,7 +447,7 @@ const fetchGraphData = async (
// - - - Random emoji - - - // - - - Random emoji - - -
const getEmoji = async () => { const getEmoji = () => {
const url = new URL("/api/v1/emoji", API_HOSTNAME); const url = new URL("/api/v1/emoji", API_HOSTNAME);
return fetch(url.href) return fetch(url.href)
@@ -450,86 +461,44 @@ const getEmoji = async () => {
// - - - ELASTIC SEARCH - - - // - - - ELASTIC SEARCH - - -
// This elastic index contains titles mapped to ids. Lightning search // This elastic index contains titles mapped to ids. Lightning search
// used for autocomplete // used for autocomplete
interface TimeoutRequestInit extends RequestInit {
timeout: number;
}
async function fetchWithTimeout(url: string, options: TimeoutRequestInit) {
const { timeout = 2000 } = options;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timer);
return response;
}
/** /**
* Search elastic indexes movies and shows by query. Doc includes Tmdb daily export of Movies and * Search elastic indexes movies and shows by query. Doc includes Tmdb daily export of Movies and
* Tv Shows. See tmdb docs for more info: https://developers.themoviedb.org/3/getting-started/daily-file-exports * Tv Shows. See tmdb docs for more info: https://developers.themoviedb.org/3/getting-started/daily-file-exports
* @param {string} query
* @returns {object} List of movies and shows matching query
*/ */
const elasticSearchMoviesAndShows = async (query: string, count = 22) => { const elasticSearchMoviesAndShows = (query, count = 22) => {
const url = new URL(`${ELASTIC_URL}/_search`); const url = new URL(`${ELASTIC_INDEX}/_search`, ELASTIC);
const body = { const body = {
sort: [{ popularity: { order: "desc" } }, "_score"], sort: [{ popularity: { order: "desc" } }, "_score"],
size: count,
query: { query: {
multi_match: { bool: {
query, should: [
fields: ["name", "original_title", "original_name"], {
type: "phrase_prefix", match_phrase_prefix: {
tie_breaker: 0.3 original_name: query
} }
}, },
suggest: { {
text: query, match_phrase_prefix: {
"person-suggest": { original_title: query
prefix: query,
completion: {
field: "name.completion",
fuzzy: {
fuzziness: "AUTO"
} }
} }
]
}
}, },
"movie-suggest": { size: count
prefix: query,
completion: {
field: "original_title.completion",
fuzzy: {
fuzziness: "AUTO"
}
}
},
"show-suggest": {
prefix: query,
completion: {
field: "original_name.completion",
fuzzy: {
fuzziness: "AUTO"
}
}
}
}
}; };
const options = { const options = {
method: "POST", method: "POST",
headers: { headers: { "Content-Type": "application/json" },
"Content-Type": "application/json", body: JSON.stringify(body)
Authorization: `ApiKey ${ELASTIC_API_KEY}`
},
body: JSON.stringify(body),
timeout: 1000
}; };
return fetchWithTimeout(url.href, options) return fetch(url.href, options)
.then(resp => resp.json()) .then(resp => resp.json())
.catch(error => { .catch(error => {
console.log(`api error searching elasticsearch: ${query}`); // eslint-disable-line no-console console.log(`api error searching elasticsearch: ${query}`); // eslint-disable-line no-console
@@ -551,6 +520,8 @@ export {
searchTorrents, searchTorrents,
addMagnet, addMagnet,
request, request,
watchLink,
movieImages,
getRequestStatus, getRequestStatus,
linkPlexAccount, linkPlexAccount,
unlinkPlexAccount, unlinkPlexAccount,

View File

@@ -12,7 +12,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineProps } from "vue"; import { defineProps } from "vue";
import CastListItem from "@/components/CastListItem.vue"; import CastListItem from "src/components/CastListItem.vue";
import type { import type {
IMovie, IMovie,
IShow, IShow,

View File

@@ -9,7 +9,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue"; import { defineProps, computed } from "vue";
import { useStore } from "vuex"; import { useStore } from "vuex";
import type { ICast, ICrew, IMovie, IShow } from "../interfaces/IList"; import type { ICast, ICrew, IMovie, IShow } from "../interfaces/IList";

View File

@@ -3,7 +3,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, watch } from "vue"; import { ref, defineProps, onMounted, watch } from "vue";
import { import {
Chart, Chart,
LineElement, LineElement,

View File

@@ -100,8 +100,8 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/variables"; @import "src/scss/variables";
@import "scss/media-queries"; @import "src/scss/media-queries";
header { header {
width: 100%; width: 100%;

View File

@@ -23,7 +23,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue"; import { defineProps, computed } from "vue";
interface Props { interface Props {
title: string; title: string;
@@ -47,9 +47,9 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/variables"; @import "src/scss/variables";
@import "scss/media-queries"; @import "src/scss/media-queries";
@import "scss/main"; @import "src/scss/main";
header { header {
width: 100%; width: 100%;

View File

@@ -46,7 +46,7 @@
let _type: MediaTypes; let _type: MediaTypes;
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
params.forEach((_, key) => { params.forEach((value, key) => {
if ( if (
key !== MediaTypes.Movie && key !== MediaTypes.Movie &&
key !== MediaTypes.Show && key !== MediaTypes.Show &&
@@ -90,8 +90,8 @@
</script> </script>
<style lang="scss"> <style lang="scss">
@import "scss/variables"; @import "src/scss/variables";
@import "scss/media-queries"; @import "src/scss/media-queries";
.movie-popup { .movie-popup {
position: fixed; position: fixed;
@@ -136,7 +136,7 @@
left: 10px; left: 10px;
width: 20px; width: 20px;
height: 2px; height: 2px;
background-color: white; background: $white;
} }
&:before { &:before {
transform: rotate(45deg); transform: rotate(45deg);
@@ -145,7 +145,7 @@
transform: rotate(-45deg); transform: rotate(-45deg);
} }
&:hover { &:hover {
background-color: var(--highlight-color); background: $green;
} }
} }
} }

View File

@@ -17,6 +17,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineProps } from "vue";
import ResultsListItem from "@/components/ResultsListItem.vue"; import ResultsListItem from "@/components/ResultsListItem.vue";
import type { ListResults } from "../interfaces/IList"; import type { ListResults } from "../interfaces/IList";
@@ -34,8 +35,8 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/media-queries"; @import "src/scss/media-queries";
@import "scss/main"; @import "src/scss/main";
.no-results { .no-results {
width: 100%; width: 100%;

View File

@@ -22,11 +22,7 @@
</div> </div>
</figure> </figure>
<div <div class="movie-item__info">
class="movie-item__info"
@click="openMoviePopup"
@keydown.enter="openMoviePopup"
>
<p v-if="listItem.title || listItem.name" class="movie-item__title"> <p v-if="listItem.title || listItem.name" class="movie-item__title">
{{ listItem.title || listItem.name }} {{ listItem.title || listItem.name }}
</p> </p>
@@ -39,7 +35,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from "vue"; import { ref, computed, defineProps, onMounted } from "vue";
import { useStore } from "vuex"; import { useStore } from "vuex";
import type { Ref } from "vue"; import type { Ref } from "vue";
import type { IMovie, IShow, IPerson } from "../interfaces/IList"; import type { IMovie, IShow, IPerson } from "../interfaces/IList";
@@ -115,9 +111,9 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/variables"; @import "src/scss/variables";
@import "scss/media-queries"; @import "src/scss/media-queries";
@import "scss/main"; @import "src/scss/main";
.movie-item { .movie-item {
padding: 15px; padding: 15px;

View File

@@ -27,7 +27,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from "vue"; import { defineProps, ref, computed, onMounted } from "vue";
import PageHeader from "@/components/PageHeader.vue"; import PageHeader from "@/components/PageHeader.vue";
import ResultsList from "@/components/ResultsList.vue"; import ResultsList from "@/components/ResultsList.vue";
import SeasonedButton from "@/components/ui/SeasonedButton.vue"; import SeasonedButton from "@/components/ui/SeasonedButton.vue";
@@ -172,7 +172,7 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/media-queries"; @import "src/scss/media-queries";
.resultSection { .resultSection {
background-color: var(--background-color); background-color: var(--background-color);

View File

@@ -24,13 +24,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Ref } from "vue";
import { ref, watch, defineProps } from "vue"; import { ref, watch, defineProps } from "vue";
import { useStore } from "vuex"; import { useStore } from "vuex";
import IconMovie from "../../icons/IconMovie.vue"; import IconMovie from "@/icons/IconMovie.vue";
import IconShow from "../../icons/IconShow.vue"; import IconShow from "@/icons/IconShow.vue";
import type { Ref } from "vue";
import { elasticSearchMoviesAndShows } from "../../api"; import { elasticSearchMoviesAndShows } from "../../api";
import { MediaTypes } from "../../interfaces/IList"; import { MediaTypes } from "../../interfaces/IList";
import { Index } from "../../interfaces/IAutocompleteSearch";
import type { import type {
IAutocompleteResult, IAutocompleteResult,
IAutocompleteSearchResults IAutocompleteSearchResults
@@ -54,23 +55,21 @@
const searchResults: Ref<Array<IAutocompleteResult>> = ref([]); const searchResults: Ref<Array<IAutocompleteResult>> = ref([]);
const keyboardNavigationIndex: Ref<number> = ref(0); const keyboardNavigationIndex: Ref<number> = ref(0);
let disableOnFailure = false;
watch( watch(
() => props.query, () => props.query,
newQuery => { newQuery => {
if (newQuery?.length > 0 && !disableOnFailure) if (newQuery?.length > 0)
fetchAutocompleteResults(); /* eslint-disable-line no-use-before-define */ fetchAutocompleteResults(); /* eslint-disable-line no-use-before-define */
} }
); );
function openPopup(result: IAutocompleteResult) { function openPopup(result) {
if (!result.id || !result.type) return; if (!result.id || !result.type) return;
store.dispatch("popup/open", { ...result }); store.dispatch("popup/open", { ...result });
} }
function removeDuplicates(_searchResults: Array<IAutocompleteResult>) { function removeDuplicates(_searchResults) {
const filteredResults = []; const filteredResults = [];
_searchResults.forEach(result => { _searchResults.forEach(result => {
if (result === undefined) return; if (result === undefined) return;
@@ -87,9 +86,9 @@
return filteredResults; return filteredResults;
} }
function elasticTypeToMediaType(type: string): MediaTypes { function elasticIndexToMediaType(index: Index): MediaTypes {
if (type === "movie") return MediaTypes.Movie; if (index === Index.Movies) return MediaTypes.Movie;
if (type === "tv_series") return MediaTypes.Show; if (index === Index.Shows) return MediaTypes.Show;
return null; return null;
} }
@@ -100,13 +99,15 @@
const results: Array<IAutocompleteResult> = []; const results: Array<IAutocompleteResult> = [];
data.forEach(item => { data.forEach(item => {
if (!item._index) return; if (!Object.values(Index).includes(item._index)) {
return;
}
results.push({ results.push({
title: item._source?.original_name || item._source.original_title, title: item._source?.original_name || item._source.original_title,
id: item._source.id, id: item._source.id,
adult: item._source.adult, adult: item._source.adult,
type: elasticTypeToMediaType(item._source.type) type: elasticIndexToMediaType(item._index)
}); });
}); });
@@ -115,36 +116,27 @@
}); });
} }
async function fetchAutocompleteResults() { function fetchAutocompleteResults() {
keyboardNavigationIndex.value = 0; keyboardNavigationIndex.value = 0;
searchResults.value = []; searchResults.value = [];
return elasticSearchMoviesAndShows(props.query, numberOfResults) elasticSearchMoviesAndShows(props.query, numberOfResults)
.catch(error => {
// TODO display error
disableOnFailure = true;
throw error;
})
.then(elasticResponse => parseElasticResponse(elasticResponse)) .then(elasticResponse => parseElasticResponse(elasticResponse))
.then(_searchResults => { .then(_searchResults => {
emit("update:results", _searchResults); emit("update:results", _searchResults);
searchResults.value = _searchResults; searchResults.value = _searchResults;
})
.catch(error => {
// TODO display error
disableOnFailure = true;
throw error;
}); });
} }
// on load functions // on load functions
fetchAutocompleteResults(); fetchAutocompleteResults();
// end on load functions
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/variables"; @import "src/scss/variables";
@import "scss/media-queries"; @import "src/scss/media-queries";
@import "scss/main"; @import "src/scss/main";
$sizes: 22; $sizes: 22;
@for $i from 0 through $sizes { @for $i from 0 through $sizes {
@@ -222,9 +214,7 @@
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
transition: transition: color 0.1s ease, fill 0.4s ease;
color 0.1s ease,
fill 0.4s ease;
span { span {
overflow-x: hidden; overflow-x: hidden;

View File

@@ -61,8 +61,8 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/variables"; @import "src/scss/variables";
@import "scss/media-queries"; @import "src/scss/media-queries";
.spacer { .spacer {
@include mobile-only { @include mobile-only {

View File

@@ -16,7 +16,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useStore } from "vuex"; import { useStore } from "vuex";
import { computed } from "vue"; import { computed, defineProps } from "vue";
import type INavigationIcon from "../../interfaces/INavigationIcon"; import type INavigationIcon from "../../interfaces/INavigationIcon";
interface Props { interface Props {
@@ -37,7 +37,7 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/media-queries"; @import "src/scss/media-queries";
.navigation-link { .navigation-link {
display: grid; display: grid;
@@ -47,13 +47,8 @@
padding: 1rem 0.15rem; padding: 1rem 0.15rem;
text-align: center; text-align: center;
background-color: var(--background-color-secondary); background-color: var(--background-color-secondary);
transition: transition: transform 0.3s ease, color 0.3s ease, stoke 0.3s ease,
transform 0.3s ease, fill 0.3s ease, background-color 0.5s ease;
color 0.3s ease,
stoke 0.3s ease,
fill 0.3s ease,
background-color 0.5s ease;
transition: all 0.3s ease;
&:hover { &:hover {
transform: scale(1.05); transform: scale(1.05);

View File

@@ -77,7 +77,7 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/media-queries"; @import "src/scss/media-queries";
.navigation-icons { .navigation-icons {
display: grid; display: grid;

View File

@@ -42,27 +42,22 @@
</div> </div>
</template> </template>
<!-- Handles constructing markup and state for dropdown.
Markup:
Consist of: search icon, input & close button.
State:
State is passing input variable `query` to dropdown and carrying state
of selected dropdown element as variable `index`. This is because
index is manipulated based on arrow key events from same input as
the `query`.
-->
<script setup lang="ts"> <script setup lang="ts">
import type { Ref } from "vue";
import { ref, computed } from "vue"; import { ref, computed } from "vue";
import { useStore } from "vuex"; import { useStore } from "vuex";
import { useRouter, useRoute } from "vue-router"; import { useRouter, useRoute } from "vue-router";
import AutocompleteDropdown from "./AutocompleteDropdown.vue"; import AutocompleteDropdown from "@/components/header/AutocompleteDropdown.vue";
import IconSearch from "../../icons/IconSearch.vue"; import IconSearch from "@/icons/IconSearch.vue";
import IconClose from "../../icons/IconClose.vue"; import IconClose from "@/icons/IconClose.vue";
import { IAutocompleteResult } from "../../interfaces/IAutocompleteSearch"; import type { Ref } from "vue";
import type { MediaTypes } from "../../interfaces/IList";
interface ISearchResult {
title: string;
id: number;
adult: boolean;
type: MediaTypes;
}
const store = useStore(); const store = useStore();
const router = useRouter(); const router = useRouter();
@@ -71,7 +66,7 @@ the `query`.
const query: Ref<string> = ref(null); const query: Ref<string> = ref(null);
const disabled: Ref<boolean> = ref(false); const disabled: Ref<boolean> = ref(false);
const dropdownIndex: Ref<number> = ref(-1); const dropdownIndex: Ref<number> = ref(-1);
const dropdownResults: Ref<IAutocompleteResult[]> = ref([]); const dropdownResults: Ref<ISearchResult[]> = ref([]);
const inputIsActive: Ref<boolean> = ref(false); const inputIsActive: Ref<boolean> = ref(false);
const inputElement: Ref<HTMLInputElement> = ref(null); const inputElement: Ref<HTMLInputElement> = ref(null);
@@ -90,9 +85,8 @@ the `query`.
query.value = decodeURIComponent(params.get("query")); query.value = decodeURIComponent(params.get("query"));
} }
const ELASTIC_URL = import.meta.env.VITE_ELASTIC_URL; const { ELASTIC } = process.env;
const ELASTIC_API_KEY = import.meta.env.VITE_ELASTIC_API_KEY; if (ELASTIC === undefined || ELASTIC === "") {
if (!ELASTIC_URL || !ELASTIC_API_KEY) {
disabled.value = true; disabled.value = true;
} }
@@ -151,7 +145,6 @@ the `query`.
function handleSubmit() { function handleSubmit() {
if (!query.value || query.value.length === 0) return; if (!query.value || query.value.length === 0) return;
// if index is set, navigation has happened. Open popup else search
if (dropdownIndex.value >= 0) { if (dropdownIndex.value >= 0) {
const resultItem = dropdownResults.value[dropdownIndex.value]; const resultItem = dropdownResults.value[dropdownIndex.value];
@@ -172,9 +165,9 @@ the `query`.
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/variables"; @import "src/scss/variables";
@import "scss/media-queries"; @import "src/scss/media-queries";
@import "scss/main"; @import "src/scss/main";
.close-icon { .close-icon {
position: absolute; position: absolute;

View File

@@ -10,6 +10,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineProps, defineEmits } from "vue";
interface Props { interface Props {
active?: boolean; active?: boolean;
disabled?: boolean; disabled?: boolean;
@@ -24,7 +26,7 @@
</script> </script>
<style lang="scss"> <style lang="scss">
@import "scss/media-queries"; @import "src/scss/media-queries";
li.sidebar-list-element { li.sidebar-list-element {
display: flex; display: flex;
@@ -65,7 +67,7 @@
&.active > div > svg, &.active > div > svg,
&.active > svg { &.active > svg {
fill: var(--highlight-color); fill: var(--color-green);
} }
&.disabled { &.disabled {

View File

@@ -14,7 +14,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from "vue"; import { ref, defineProps, onMounted } from "vue";
import type { Ref } from "vue"; import type { Ref } from "vue";
import IconArrowDown from "../../icons/IconArrowDown.vue"; import IconArrowDown from "../../icons/IconArrowDown.vue";
@@ -27,6 +27,7 @@
const overflow: Ref<boolean> = ref(false); const overflow: Ref<boolean> = ref(false);
const descriptionElement: Ref<HTMLElement> = ref(null); const descriptionElement: Ref<HTMLElement> = ref(null);
// eslint-disable-next-line no-undef
function removeElements(elems: NodeListOf<Element>) { function removeElements(elems: NodeListOf<Element>) {
elems.forEach(el => el.remove()); elems.forEach(el => el.remove());
} }
@@ -66,7 +67,7 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/media-queries"; @import "src/scss/media-queries";
.movie-description { .movie-description {
font-weight: 300; font-weight: 300;
@@ -92,7 +93,6 @@
border: none; border: none;
background: none; background: none;
width: 100%; width: 100%;
height: 30px;
display: flex; display: flex;
align-items: center; align-items: center;
text-align: center; text-align: center;

View File

@@ -8,6 +8,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineProps } from "vue";
interface Props { interface Props {
title: string; title: string;
detail?: string | number; detail?: string | number;
@@ -17,7 +19,7 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/media-queries"; @import "src/scss/media-queries";
.movie-detail { .movie-detail {
margin-bottom: 20px; margin-bottom: 20px;
@@ -35,7 +37,7 @@
font-weight: 400; font-weight: 400;
text-transform: uppercase; text-transform: uppercase;
font-size: 1.2rem; font-size: 1.2rem;
color: var(--highlight-color); color: var(--color-green);
@include mobile { @include mobile {
font-size: 1.1rem; font-size: 1.1rem;

View File

@@ -165,26 +165,25 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from "vue"; import { ref, computed, defineProps, onMounted } from "vue";
import { useStore } from "vuex"; import { useStore } from "vuex";
import type { Ref } from "vue";
// import img from "@/directives/v-image"; // import img from "@/directives/v-image";
import IconProfile from "../../icons/IconProfile.vue"; import IconProfile from "@/icons/IconProfile.vue";
import IconThumbsUp from "../../icons/IconThumbsUp.vue"; import IconThumbsUp from "@/icons/IconThumbsUp.vue";
import IconThumbsDown from "../../icons/IconThumbsDown.vue"; import IconThumbsDown from "@/icons/IconThumbsDown.vue";
import IconInfo from "../../icons/IconInfo.vue"; import IconInfo from "@/icons/IconInfo.vue";
import IconRequest from "../../icons/IconRequest.vue"; import IconRequest from "@/icons/IconRequest.vue";
import IconRequested from "../../icons/IconRequested.vue"; import IconRequested from "@/icons/IconRequested.vue";
import IconBinoculars from "../../icons/IconBinoculars.vue"; import IconBinoculars from "@/icons/IconBinoculars.vue";
import IconPlay from "../../icons/IconPlay.vue"; import IconPlay from "@/icons/IconPlay.vue";
import TorrentList from "../torrent/TruncatedTorrentResults.vue"; import TorrentList from "@/components/torrent/TruncatedTorrentResults.vue";
import CastList from "../CastList.vue"; import CastList from "@/components/CastList.vue";
import Detail from "./Detail.vue"; import Detail from "@/components/popup/Detail.vue";
import ActionButton from "./ActionButton.vue"; import ActionButton from "@/components/popup/ActionButton.vue";
import Description from "./Description.vue"; import Description from "@/components/popup/Description.vue";
import LoadingPlaceholder from "../ui/LoadingPlaceholder.vue"; import LoadingPlaceholder from "@/components/ui/LoadingPlaceholder.vue";
import type { IColors } from "../../interfaces/IColors.ts"; import type { Ref } from "vue";
import type { import type {
IMovie, IMovie,
IShow, IShow,
@@ -215,7 +214,6 @@
const props = defineProps<Props>(); const props = defineProps<Props>();
const ASSET_URL = "https://image.tmdb.org/t/p/"; const ASSET_URL = "https://image.tmdb.org/t/p/";
const COLORS_URL = "https://colors.schleppe.cloud/colors";
const ASSET_SIZES = ["w500", "w780", "original"]; const ASSET_SIZES = ["w500", "w780", "original"];
const media: Ref<IMovie | IShow> = ref(); const media: Ref<IMovie | IShow> = ref();
@@ -236,8 +234,6 @@
if (!media.value) return "/assets/placeholder.png"; if (!media.value) return "/assets/placeholder.png";
if (!media.value?.poster) return "/assets/no-image.svg"; if (!media.value?.poster) return "/assets/no-image.svg";
// compute & update highlight colors from poster image
colorsFromPoster(media.value.poster); // eslint-disable-line
return `${ASSET_URL}${ASSET_SIZES[0]}${media.value.poster}`; return `${ASSET_URL}${ASSET_SIZES[0]}${media.value.poster}`;
}); });
@@ -336,34 +332,6 @@
const tmdbURL = `https://www.themoviedb.org/${tmdbType}/${props.id}`; const tmdbURL = `https://www.themoviedb.org/${tmdbType}/${props.id}`;
window.location.href = tmdbURL; window.location.href = tmdbURL;
} }
function colorMain(colors: IColors) {
const parent = document.getElementsByClassName(
"movie-popup"
)[0] as HTMLElement;
parent.style.setProperty("--highlight-color", colors.s ?? colors.p);
parent.style.setProperty("--highlight-bg", colors.bg);
parent.style.setProperty("--highlight-secondary", colors.p);
parent.style.setProperty("--text-color", "#ffffff");
parent.style.setProperty("--text-color-90", "rgba(255, 255, 255, 0.9)");
parent.style.setProperty("--text-color-70", "rgba(255, 255, 255, 0.7)");
parent.style.setProperty("--text-color-50", "rgba(255, 255, 255, 0.5)");
parent.style.setProperty("--text-color-10", "rgba(255, 255, 255, 0.1)");
parent.style.setProperty("--text-color-5", "rgba(255, 255, 255, 0.05)");
}
async function colorsFromPoster(posterPath: string) {
const url = new URL(COLORS_URL);
url.searchParams.append("id", posterPath.replace("/", ""));
url.searchParams.append("size", "w342");
fetch(url.href)
.then(resp => {
if (resp.ok) return resp.json();
throw new Error(`invalid status: '${resp.status}' from server.`);
})
.then(colorMain)
.catch(error => console.log("unable to get colors, error:", error)); // eslint-disable-line no-console
}
// On created functions // On created functions
fetchMedia(); fetchMedia();
@@ -374,10 +342,10 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/loading-placeholder"; @import "src/scss/loading-placeholder";
@import "scss/variables"; @import "src/scss/variables";
@import "scss/media-queries"; @import "src/scss/media-queries";
@import "scss/main"; @import "src/scss/main";
header { header {
$duration: 0.2s; $duration: 0.2s;
@@ -424,7 +392,6 @@
.movie__poster { .movie__poster {
display: none; display: none;
border-radius: 1.6rem;
@include desktop { @include desktop {
background: var(--background-color); background: var(--background-color);
@@ -435,7 +402,7 @@
> img { > img {
width: 100%; width: 100%;
border-radius: inherit; border-radius: 10px;
} }
} }
} }
@@ -454,8 +421,8 @@
flex-direction: row; flex-direction: row;
} }
background-color: var(--highlight-bg, var(--background-color)); background-color: $background-color;
color: var(--text-color); color: $text-color;
} }
} }
@@ -464,9 +431,7 @@
width: 100%; width: 100%;
opacity: 0; opacity: 0;
transform: scale(0.97) translateZ(0); transform: scale(0.97) translateZ(0);
transition: transition: opacity 0.5s ease, transform 0.5s ease;
opacity 0.5s ease,
transform 0.5s ease;
&.is-loaded { &.is-loaded {
opacity: 1; opacity: 1;
@@ -485,26 +450,21 @@
text-align: left; text-align: left;
padding: 140px 30px 0 40px; padding: 140px 30px 0 40px;
} }
h1 { h1 {
color: var(--highlight-color); color: var(--color-green);
font-weight: 500; font-weight: 500;
line-height: 1.2; line-height: 1.4;
font-size: 2.2rem; font-size: 24px;
font-weight: 600;
letter-spacing: 1px;
margin-bottom: 0; margin-bottom: 0;
@include tablet-min { @include tablet-min {
font-size: 30px; font-size: 30px;
font-size: 2.2rem;
} }
} }
i { i {
display: block; display: block;
color: rgba(255, 255, 255, 0.8); color: rgba(255, 255, 255, 0.8);
color: var(--highlight-secondary);
margin-top: 1rem; margin-top: 1rem;
} }
} }
@@ -514,7 +474,7 @@
width: 100%; width: 100%;
order: 2; order: 2;
padding: 20px; padding: 20px;
border-top: 1px solid var(--text-color-50); border-top: 1px solid $text-color-5;
@include tablet-min { @include tablet-min {
order: 1; order: 1;
width: 45%; width: 45%;
@@ -573,7 +533,7 @@
} }
.torrents { .torrents {
background-color: var(--highlight-bg, var(--background-color)); background-color: var(--background-color);
padding: 0 1rem; padding: 0 1rem;
@include mobile { @include mobile {

View File

@@ -70,7 +70,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from "vue"; import { ref, computed, defineProps } from "vue";
import CastList from "@/components/CastList.vue"; import CastList from "@/components/CastList.vue";
import Detail from "@/components/popup/Detail.vue"; import Detail from "@/components/popup/Detail.vue";
import Description from "@/components/popup/Description.vue"; import Description from "@/components/popup/Description.vue";
@@ -165,10 +165,10 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/loading-placeholder"; @import "src/scss/loading-placeholder";
@import "scss/variables"; @import "src/scss/variables";
@import "scss/media-queries"; @import "src/scss/media-queries";
@import "scss/main"; @import "src/scss/main";
section.person { section.person {
overflow: hidden; overflow: hidden;

View File

@@ -85,7 +85,7 @@
try { try {
validate(); validate();
} catch (error) { } catch (error) {
console.log("not valid! error:", error); // eslint-disable-line no-console console.log("not valid!"); // eslint-disable-line no-console
} }
// const body: ResetPasswordPayload = { // const body: ResetPasswordPayload = {

View File

@@ -43,7 +43,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from "vue"; import { ref, computed, defineEmits } from "vue";
import { useStore } from "vuex"; import { useStore } from "vuex";
import seasonedInput from "@/components/ui/SeasonedInput.vue"; import seasonedInput from "@/components/ui/SeasonedInput.vue";
import SeasonedButton from "@/components/ui/SeasonedButton.vue"; import SeasonedButton from "@/components/ui/SeasonedButton.vue";

View File

@@ -20,7 +20,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, inject } from "vue"; import { ref, watch, inject, defineProps } from "vue";
import { useStore } from "vuex"; import { useStore } from "vuex";
import Loader from "@/components/ui/Loader.vue"; import Loader from "@/components/ui/Loader.vue";
import TorrentTable from "@/components/torrent/TorrentTable.vue"; import TorrentTable from "@/components/torrent/TorrentTable.vue";
@@ -102,9 +102,9 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/variables"; @import "src/scss/variables";
@import "scss/media-queries"; @import "src/scss/media-queries";
@import "scss/elements"; @import "src/scss/elements";
h2 { h2 {
font-size: 20px; font-size: 20px;
@@ -115,8 +115,13 @@
margin: 1rem 0; margin: 1rem 0;
} }
.container {
background-color: $background-color;
}
.no-results { .no-results {
display: flex; display: flex;
padding-bottom: 2rem;
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
@@ -137,7 +142,7 @@
} }
@include mobile { @include mobile {
padding: 0 0.8rem; text-align: left;
} }
} }

View File

@@ -52,7 +52,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref, defineProps, defineEmits } from "vue";
import IconMagnet from "@/icons/IconMagnet.vue"; import IconMagnet from "@/icons/IconMagnet.vue";
import type { Ref } from "vue"; import type { Ref } from "vue";
import { sortableSize } from "../../utils"; import { sortableSize } from "../../utils";
@@ -155,9 +155,9 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/variables"; @import "src/scss/variables";
@import "scss/media-queries"; @import "src/scss/media-queries";
@import "scss/elements"; @import "src/scss/elements";
table { table {
border-spacing: 0; border-spacing: 0;
@@ -185,7 +185,6 @@
text-transform: uppercase; text-transform: uppercase;
cursor: pointer; cursor: pointer;
background-color: var(--table-background-color); background-color: var(--table-background-color);
background-color: var(--highlight-color);
// background-color: black; // background-color: black;
// color: var(--color-green); // color: var(--color-green);
letter-spacing: 0.8px; letter-spacing: 0.8px;
@@ -233,9 +232,6 @@
} }
// alternate background color per row // alternate background color per row
tr {
background-color: var(--background-color);
}
tr:nth-child(even) { tr:nth-child(even) {
background-color: var(--background-70); background-color: var(--background-70);
} }

View File

@@ -1,12 +1,11 @@
<template> <template>
<div> <div>
<div class="search-results">
<torrent-search-results <torrent-search-results
:query="query" :query="query"
:tmdb-id="tmdbId" :tmdb-id="tmdbId"
:class="{ truncated: _truncated }" :class="{ truncated: truncated }"
><div ><div
v-if="_truncated" v-if="truncated"
class="load-more" class="load-more"
tabindex="0" tabindex="0"
role="button" role="button"
@@ -16,25 +15,22 @@
<icon-arrow-down /> <icon-arrow-down />
</div> </div>
</torrent-search-results> </torrent-search-results>
</div>
<div class="edit-query-btn-container"> <div class="edit-query-btn-container">
<a :href="`/torrents?query=${encodeURIComponent(props.query)}`"> <seasonedButton @click="openInTorrentPage"
<button> >View on torrent page</seasonedButton
<span class="text">View on torrent page</span >
><span class="icon"><icon-arrow-down /></span>
</button>
</a>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, defineProps, computed } from "vue"; import { ref, defineProps } from "vue";
import { useRouter } from "vue-router";
import TorrentSearchResults from "@/components/torrent/TorrentSearchResults.vue"; import TorrentSearchResults from "@/components/torrent/TorrentSearchResults.vue";
import SeasonedButton from "@/components/ui/SeasonedButton.vue";
import IconArrowDown from "@/icons/IconArrowDown.vue"; import IconArrowDown from "@/icons/IconArrowDown.vue";
import type { Ref } from "vue"; import type { Ref } from "vue";
import store from "../../store";
interface Props { interface Props {
query: string; query: string;
@@ -42,13 +38,18 @@
} }
const props = defineProps<Props>(); const props = defineProps<Props>();
const router = useRouter();
const truncated: Ref<boolean> = ref(true); const truncated: Ref<boolean> = ref(true);
const _truncated = computed(() => { function openInTorrentPage() {
const val = store.getters["torrentModule/resultCount"]; if (!props.query?.length) {
if (val > 10 && truncated.value) return true; router.push("/torrents");
return false; return;
}); }
router.push({ path: "/torrents", query: { query: props.query } });
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -74,68 +75,14 @@
); );
} }
.search-results {
svg { svg {
height: 30px; height: 30px;
fill: var(--text-color); fill: var(--text-color);
} }
}
.edit-query-btn-container { .edit-query-btn-container {
display: flex; display: flex;
justify-content: center; justify-content: center;
padding: 1rem; padding: 1rem;
padding-bottom: 2rem;
a button {
--height: 45px;
transition: all 0.8s ease !important;
position: relative;
font-size: 1rem;
line-height: 1.5;
letter-spacing: 0.2px;
font-family: Arial, Helvetica, sans-serif;
font-weight: 600;
color: var(--highlight-bg, var(--background-color));
background-color: var(--text-color);
min-height: var(--height);
padding: 0rem 1.5rem;
margin: 0;
border: 2px solid var(--text-color);
border-radius: calc(var(--height) / 2);
cursor: pointer;
outline: none;
overflow-x: hidden;
&:hover {
background-color: var(--highlight-bg, var(--background-color));
color: var(--text-color);
padding: 0 2rem;
span.text {
margin-left: -0.5rem;
margin-right: 0.5rem;
}
span.icon {
right: 1rem;
}
}
span.icon {
--size: 1rem;
display: block;
transform: rotate(-90deg);
transform-origin: top left;
stroke: var(--text-color);
fill: var(--text-color);
height: var(--size);
width: var(--size);
margin-top: -4px;
position: absolute;
right: 1rem;
right: -1rem;
}
}
} }
</style> </style>

1895
src/components/torrents.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/media-queries"; @import "src/scss/media-queries";
.nav__hamburger { .nav__hamburger {
display: block; display: block;

View File

@@ -12,6 +12,7 @@
--></template> --></template>
<script setup lang="ts"> <script setup lang="ts">
import { defineProps } from "vue";
import LoaderHeightType from "../../interfaces/ILoader"; import LoaderHeightType from "../../interfaces/ILoader";
interface Props { interface Props {
@@ -22,7 +23,7 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/variables"; @import "src/scss/variables";
.loader { .loader {
display: flex; display: flex;

View File

@@ -10,6 +10,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineProps } from "vue";
interface Props { interface Props {
count?: number; count?: number;
lineClass?: string; lineClass?: string;
@@ -20,5 +22,5 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/loading-placeholder"; @import "src/scss/loading-placeholder";
</style> </style>

View File

@@ -9,6 +9,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineProps, defineEmits } from "vue";
interface Props { interface Props {
active?: boolean; active?: boolean;
fullWidth?: boolean; fullWidth?: boolean;
@@ -23,8 +25,8 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/variables"; @import "src/scss/variables";
@import "scss/media-queries"; @import "src/scss/media-queries";
button { button {
display: inline-block; display: inline-block;
@@ -42,10 +44,7 @@
background: $background-color-secondary; background: $background-color-secondary;
cursor: pointer; cursor: pointer;
outline: none; outline: none;
transition: transition: background 0.5s ease, color 0.5s ease, border-color 0.5s ease;
background 0.5s ease,
color 0.5s ease,
border-color 0.5s ease;
@include desktop { @include desktop {
font-size: 0.8rem; font-size: 0.8rem;

View File

@@ -26,7 +26,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from "vue"; import { ref, computed, defineProps, defineEmits } from "vue";
import IconKey from "@/icons/IconKey.vue"; import IconKey from "@/icons/IconKey.vue";
import IconEmail from "@/icons/IconEmail.vue"; import IconEmail from "@/icons/IconEmail.vue";
import IconBinoculars from "@/icons/IconBinoculars.vue"; import IconBinoculars from "@/icons/IconBinoculars.vue";
@@ -74,8 +74,8 @@
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/variables"; @import "src/scss/variables";
@import "scss/media-queries"; @import "src/scss/media-queries";
.group { .group {
display: flex; display: flex;

View File

@@ -22,6 +22,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineProps, defineEmits } from "vue";
import type { import type {
ErrorMessageTypes, ErrorMessageTypes,
IErrorMessage IErrorMessage
@@ -64,8 +65,8 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/variables"; @import "src/scss/variables";
@import "scss/media-queries"; @import "src/scss/media-queries";
.fade-active { .fade-active {
transition: opacity 0.4s; transition: opacity 0.4s;

View File

@@ -13,6 +13,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineProps, defineEmits } from "vue";
interface Props { interface Props {
options: string[]; options: string[];
selected?: string; selected?: string;
@@ -33,7 +35,7 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/variables"; @import "src/scss/variables";
$background: $background-ui; $background: $background-ui;
$background-selected: $background-color-secondary; $background-selected: $background-color-secondary;

View File

@@ -1,4 +1,3 @@
/*
let setValue = function(el, binding) { let setValue = function(el, binding) {
let value = binding.value; let value = binding.value;
let dateArray = value.split('-'); let dateArray = value.split('-');
@@ -14,4 +13,3 @@ module.exports = {
setValue(el, binding); setValue(el, binding);
} }
} }
*/

View File

@@ -1,6 +1,5 @@
/* let setValue = function(el, binding) {
const setValue = function(el, binding) { let img = new Image();
const img = new Image();
img.src = binding.value; img.src = binding.value;
img.onload = function() { img.onload = function() {
@@ -17,5 +16,4 @@ module.exports = {
update(el, binding){ update(el, binding){
setValue(el, binding); setValue(el, binding);
} }
}; }
*/

View File

@@ -1,11 +1,5 @@
<template> <template>
<svg <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
version="1.1"
height="100%"
width="100%"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 32"
>
<path <path
d="M28.725 8.058l-12.725 12.721-12.725-12.721-1.887 1.887 13.667 13.667c0.258 0.258 0.6 0.392 0.942 0.392s0.683-0.129 0.942-0.392l13.667-13.667-1.879-1.887z" d="M28.725 8.058l-12.725 12.721-12.725-12.721-1.887 1.887 13.667 13.667c0.258 0.258 0.6 0.392 0.942 0.392s0.683-0.129 0.942-0.392l13.667-13.667-1.879-1.887z"
/> />

View File

@@ -1,11 +1,8 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta <meta name="viewport" content="width=device-width, initial-scale=1" />
name="viewport"
content="width=device-width, viewport-fit=cover, initial-scale=1"
/>
<link <link
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&amp;subset=cyrillic" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&amp;subset=cyrillic"
@@ -27,9 +24,7 @@
<meta name="theme-color" content="#081c24" /> <meta name="theme-color" content="#081c24" />
</head> </head>
<body> <body>
<div id="app"></div> <div id="entry"></div>
<script type="module" src="/src/main.ts"></script>
</body> </body>
<script <script

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-use-before-define */
import { MediaTypes } from "./IList"; import { MediaTypes } from "./IList";
export interface IAutocompleteResult { export interface IAutocompleteResult {
@@ -12,7 +13,6 @@ export interface IAutocompleteSearchResults {
timed_out: boolean; timed_out: boolean;
_shards: Shards; _shards: Shards;
hits: Hits; hits: Hits;
suggest: Suggest;
} }
export interface Shards { export interface Shards {
@@ -29,7 +29,7 @@ export interface Hits {
} }
export interface Hit { export interface Hit {
_index: string; _index: Index;
_type: Type; _type: Type;
_id: string; _id: string;
_score: number; _score: number;
@@ -37,25 +37,9 @@ export interface Hit {
sort: number[]; sort: number[];
} }
export interface Suggest { export enum Index {
"movie-suggest": SuggestOptions[]; Movies = "movies",
"person-suggest": SuggestOptions[]; Shows = "shows"
"show-suggest": SuggestOptions[];
}
export interface SuggestOptions {
text: string;
offset: number;
length: number;
options: Option[];
}
export interface Option {
text: string;
_index: string;
_id: string;
_score: number;
_source: Source;
} }
export interface Source { export interface Source {
@@ -73,8 +57,6 @@ export interface Source {
agent: Agent; agent: Agent;
original_title: string; original_title: string;
original_name?: string; original_name?: string;
name?: string;
type: string;
} }
export interface Agent { export interface Agent {

View File

@@ -1,5 +0,0 @@
export interface IColors {
bg: string;
p: string;
s?: string;
}

View File

@@ -1,3 +1,5 @@
/* eslint-disable no-use-before-define */
export enum GraphTypes { export enum GraphTypes {
Plays = "plays", Plays = "plays",
Duration = "duration" Duration = "duration"
@@ -10,12 +12,12 @@ export enum GraphValueTypes {
export interface IGraphDataset { export interface IGraphDataset {
name: string; name: string;
data: number[]; data: Array<number>;
} }
export interface IGraphData { export interface IGraphData {
labels: string[]; labels: Array<string>;
series: IGraphDataset[]; series: Array<IGraphDataset>;
} }
export interface IGraphResponse { export interface IGraphResponse {

View File

@@ -67,7 +67,7 @@ export interface IMovie {
backdrop: string; backdrop: string;
release_date: string | Date; release_date: string | Date;
rating: number; rating: number;
genres: MovieGenres[]; genres: Array<MovieGenres>;
production_status: MovieProductionStatus; production_status: MovieProductionStatus;
tagline: string; tagline: string;
runtime: number; runtime: number;
@@ -88,9 +88,9 @@ export interface IShow {
seasons?: number; seasons?: number;
episodes?: number; episodes?: number;
popularity?: number; popularity?: number;
genres?: ShowGenres[]; genres?: Array<ShowGenres>;
production_status?: string; production_status?: string;
runtime?: number[]; runtime?: Array<number>;
exists_in_plex?: boolean; exists_in_plex?: boolean;
type: MediaTypes.Show; type: MediaTypes.Show;
} }
@@ -135,19 +135,19 @@ export interface ICrew {
} }
export interface IMediaCredits { export interface IMediaCredits {
cast: ICast[]; cast: Array<ICast>;
crew: ICrew[]; crew: Array<ICrew>;
id: number; id: number;
} }
export interface IPersonCredits { export interface IPersonCredits {
cast: (IMovie | IShow)[]; cast: Array<IMovie | IShow>;
crew: ICrew[]; crew: Array<ICrew>;
id: number; id: number;
type?: string; type?: string;
} }
export type ListResults = (IMovie | IShow | IPerson | IRequest)[]; export type ListResults = Array<IMovie | IShow | IPerson | IRequest>;
export interface IList { export interface IList {
results: ListResults; results: ListResults;

View File

@@ -1,7 +1,7 @@
export default interface INavigationIcon { export default interface INavigationIcon {
title: string; title: string;
route: string; route: string;
icon: any; icon: any; // eslint-disable-line @typescript-eslint/no-explicit-any
requiresAuth?: boolean; requiresAuth?: boolean;
useStroke?: boolean; useStroke?: boolean;
} }

View File

@@ -1,6 +1,6 @@
import type ITorrent from "./ITorrent"; import type ITorrent from "./ITorrent";
export default interface IStateTorrent { export default interface IStateTorrent {
results: ITorrent[]; results: Array<ITorrent>;
resultCount: number | null; resultCount: number | null;
} }

View File

@@ -7,5 +7,5 @@ export default interface ITorrent {
seed: string; seed: string;
leech: string; leech: string;
url: string | null; url: string | null;
release_type: string[]; release_type: Array<string>;
} }

View File

@@ -3,7 +3,8 @@ import router from "./routes";
import store from "./store"; import store from "./store";
import Toast from "./plugins/Toast"; import Toast from "./plugins/Toast";
import App from "./App.vue"; // eslint-disable-next-line @typescript-eslint/no-var-requires
const App = require("./App.vue").default;
store.dispatch("darkmodeModule/findAndSetDarkmodeSupported"); store.dispatch("darkmodeModule/findAndSetDarkmodeSupported");
store.dispatch("user/initUserFromCookie"); store.dispatch("user/initUserFromCookie");
@@ -13,5 +14,4 @@ const app = createApp(App);
app.use(router); app.use(router);
app.use(store); app.use(store);
app.use(Toast); app.use(Toast);
app.mount("#entry");
app.mount("#app");

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-param-reassign */
import IStateDarkmode from "../interfaces/IStateDarkmode"; import IStateDarkmode from "../interfaces/IStateDarkmode";
const state: IStateDarkmode = { const state: IStateDarkmode = {
@@ -11,7 +10,9 @@ export default {
namespaced: true, namespaced: true,
state, state,
getters: { getters: {
darkmodeSupported: (state: IStateDarkmode) => state.darkmodeSupported darkmodeSupported: (state: IStateDarkmode) => {
return state.darkmodeSupported;
}
}, },
mutations: { mutations: {
SET_DARKMODE_SUPPORT: ( SET_DARKMODE_SUPPORT: (

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-param-reassign */
import type IStateDocumentTitle from "../interfaces/IStateDocumentTitle"; import type IStateDocumentTitle from "../interfaces/IStateDocumentTitle";
const capitalize = (string: string) => { const capitalize = (string: string) => {
@@ -27,7 +26,7 @@ const state: IStateDocumentTitle = {
title: undefined title: undefined
}; };
/* eslint-disable @typescript-eslint/no-shadow */ /* eslint-disable @typescript-eslint/no-shadow, no-return-assign */
export default { export default {
namespaced: true, namespaced: true,
state, state,

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-param-reassign */
import type IStateHamburger from "../interfaces/IStateHamburger"; import type IStateHamburger from "../interfaces/IStateHamburger";
const state: IStateHamburger = { const state: IStateHamburger = {

View File

@@ -1,16 +1,15 @@
/* eslint-disable no-param-reassign */
import { MediaTypes } from "../interfaces/IList"; import { MediaTypes } from "../interfaces/IList";
import type { IStatePopup, IPopupQuery } from "../interfaces/IStatePopup"; import type { IStatePopup, IPopupQuery } from "../interfaces/IStatePopup";
/* eslint-disable-next-line import-x/no-cycle */ /* eslint-disable-next-line import/no-cycle */
import router from "../routes"; import router from "../routes";
const removeIncludedQueryParams = (params: URLSearchParams, key: string) => { const removeIncludedQueryParams = (params, key) => {
if (params.has(key)) params.delete(key); if (params.has(key)) params.delete(key);
return params; return params;
}; };
function paramsToObject(entries: Iterator<[string, string]>) { function paramsToObject(entries) {
const result = {}; const result = {};
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
for (const [key, value] of entries) { for (const [key, value] of entries) {
@@ -66,7 +65,7 @@ export default {
actions: { actions: {
open: ({ commit }, { id, type }: { id: number; type: MediaTypes }) => { open: ({ commit }, { id, type }: { id: number; type: MediaTypes }) => {
if (!Number.isNaN(id)) { if (!Number.isNaN(id)) {
id = Number(id); id = Number(id); /* eslint-disable-line no-param-reassign */
} }
commit("SET_OPEN", { id, type }); commit("SET_OPEN", { id, type });

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-param-reassign */
import type ITorrent from "../interfaces/ITorrent"; import type ITorrent from "../interfaces/ITorrent";
import type IStateTorrent from "../interfaces/IStateTorrent"; import type IStateTorrent from "../interfaces/IStateTorrent";
@@ -12,12 +11,16 @@ export default {
namespaced: true, namespaced: true,
state, state,
getters: { getters: {
results: (state: IStateTorrent) => state.results, results: (state: IStateTorrent) => {
resultCount: (state: IStateTorrent) => state.resultCount return state.results;
},
resultCount: (state: IStateTorrent) => {
return state.resultCount;
}
}, },
mutations: { mutations: {
SET_RESULTS: (state: IStateTorrent, results: ITorrent[]) => { SET_RESULTS: (state: IStateTorrent, results: Array<ITorrent>) => {
state.results = results; state.results = results;
}, },
SET_RESULT_COUNT: (state: IStateTorrent, count: number) => { SET_RESULT_COUNT: (state: IStateTorrent, count: number) => {
@@ -29,7 +32,7 @@ export default {
} }
}, },
actions: { actions: {
setResults({ commit }, results: ITorrent[]) { setResults({ commit }, results: Array<ITorrent>) {
commit("SET_RESULTS", results); commit("SET_RESULTS", results);
}, },
setResultCount({ commit }, count: number) { setResultCount({ commit }, count: number) {

117
src/modules/user.js Normal file
View File

@@ -0,0 +1,117 @@
import { refreshToken } from "@/api";
import { parseJwt } from "@/utils";
function getCookie(name) {
var arrayb = document.cookie.split(";");
for (const item of arrayb) {
const query = `${name}=`;
if (!item.startsWith(query)) continue;
return item.substr(query.length);
}
return null;
}
function setCookie(name, value, options = {}) {
options = {
path: "/",
// add other defaults here if necessary
...options
};
if (options.expires instanceof Date) {
options.expires = options.expires.toUTCString();
}
let updatedCookie =
encodeURIComponent(name) + "=" + encodeURIComponent(value);
for (let optionKey in options) {
updatedCookie += "; " + optionKey;
let optionValue = options[optionKey];
if (optionValue !== true) {
updatedCookie += "=" + optionValue;
}
}
document.cookie = updatedCookie;
}
function deleteCookie(name) {
setCookie(name, "", {
"max-age": Date.now()
});
}
export default {
namespaced: true,
state: {
token: null,
admin: false,
settings: null,
username: null
},
getters: {
username: state => state.username,
settings: state => state.settings,
token: state => state.token,
// loggedIn: state => true,
loggedIn: state => state && state.username !== null,
admin: state => state.admin,
plexUserId: state => {
if (state && state.settings && state.settings.plexUserId)
return state.settings.plexUserId;
return null;
}
},
mutations: {
SET_TOKEN: (state, token) => (state.token = token),
SET_USERNAME: (state, username) => (state.username = username),
SET_SETTINGS: (state, settings) => (state.settings = settings),
SET_ADMIN: (state, admin) => (state.admin = admin),
LOGOUT: state => {
state.token = null;
state.username = null;
state.settings = null;
state.admin = false;
// deleteCookie('authorization');
}
},
actions: {
initUserFromCookie: async ({ dispatch }) => {
const jwtToken = getCookie("authorization");
if (!jwtToken) return null;
const token = parseJwt(jwtToken);
return await dispatch("setupStateFromToken", token);
},
setupStateFromToken: ({ commit }, token) => {
try {
const { username, admin, settings } = token;
if (!username) {
return false;
}
commit("SET_TOKEN", token);
commit("SET_USERNAME", username);
commit("SET_SETTINGS", settings);
commit("SET_ADMIN", admin != undefined);
return true;
} catch (error) {
console.error("Unable to parse JWT, failed with error:", error);
return false;
}
},
setSettings: ({ commit }, settings) => {
if (!(settings instanceof Object)) {
throw "Parameter is not a object.";
}
commit("SET_SETTINGS", settings);
},
logout: ({ commit }) => commit("LOGOUT"),
login: async ({ dispatch }) => await dispatch("initUserFromCookie")
}
};

View File

@@ -1,178 +0,0 @@
/* eslint-disable no-param-reassign */
import { Module } from "vuex";
import { parseJwt } from "../utils";
/* --------------------------------------------------------------------------- */
/* ── Utility helpers (cookie handling) ────────────────────────────────── */
/* --------------------------------------------------------------------------- */
export interface CookieOptions {
path?: string;
expires?: number | string | boolean;
[option: string]: string | number | boolean | undefined;
}
/**
* Read a cookie value.
*/
export function getCookie(name: string): string | null {
const array = document.cookie.split(";");
let match = null;
array.forEach((item: string) => {
const query = `${name}=`;
if (!item.trim().startsWith(query)) return;
match = item.trim().substring(query.length);
});
return match;
}
/**
* Write a cookie.
*/
export function setCookie(
name: string,
value: string,
options: CookieOptions = {}
): void {
const opts: CookieOptions = {
path: "/",
...options
};
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
/* eslint-disable-next-line no-restricted-syntax */
for (const [key, val] of Object.entries(opts)) {
cookie += `; ${key}`;
if (val !== true) cookie += `=${val}`;
}
document.cookie = cookie;
}
/**
* Delete a cookie.
*/
export function deleteCookie(name: string): void {
setCookie(name, "", { "max-age": 0 });
}
/* --------------------------------------------------------------------------- */
/* ── State / Types ─────────────────────────────────────────────────── */
/* --------------------------------------------------------------------------- */
export interface Settings {
/** Example property replace with your real shape. */
plexUserId?: string | null;
// add the rest of your settings fields here
}
export interface UserState {
token: string | null;
admin: boolean;
settings: Settings | null;
username: string | null;
}
export interface RootState {
val?: string;
// your root state interface leave empty if you don't use it
}
/* --------------------------------------------------------------------------- */
/* ── Vuex module ──────────────────────────────────────────────────────── */
/* --------------------------------------------------------------------------- */
const userModule: Module<UserState, RootState> = {
namespaced: true,
/* ── State ───────────────────────────────────────────────────── */
state: {
token: null,
admin: false,
settings: null,
username: null
},
/* ── Getters ─────────────────────────────────────────────────── */
getters: {
username: (state): string | null => state.username,
settings: (state): Settings | null => state.settings,
token: (state): string | null => state.token,
loggedIn: (state): boolean => !!state && state.username !== null,
admin: (state): boolean => state.admin,
plexUserId: (state): string | null => state?.settings?.plexUserId ?? null
},
/* ── Mutations ─────────────────────────────────────────────────── */
mutations: {
SET_TOKEN(state, token: string | null): void {
state.token = token;
},
SET_USERNAME(state, username: string | null): void {
state.username = username;
},
SET_SETTINGS(state, settings: Settings | null): void {
state.settings = settings;
},
SET_ADMIN(state, admin: boolean): void {
state.admin = admin;
},
LOGOUT(state): void {
state.token = null;
state.username = null;
state.settings = null;
state.admin = false;
deleteCookie("authorization");
}
},
/* ── Actions ─────────────────────────────────────────────────── */
actions: {
async initUserFromCookie({ dispatch }): Promise<boolean | null> {
const jwtToken = getCookie("authorization");
if (!jwtToken) return null;
const token = parseJwt(jwtToken);
return dispatch("setupStateFromToken", token);
},
setupStateFromToken({ commit }, token: any): boolean {
try {
const { username, admin, settings } = token;
if (!username) return false;
commit("SET_TOKEN", token);
commit("SET_USERNAME", username);
commit("SET_SETTINGS", settings);
commit("SET_ADMIN", admin !== undefined);
return true;
} catch (e) {
// eslint-disable-next-line no-console
console.error("Unable to parse JWT, failed with error:", e);
return false;
}
},
setSettings({ commit }, settings: Settings): void {
if (!(settings instanceof Object)) {
throw new Error("Parameter is not an object.");
}
commit("SET_SETTINGS", settings);
},
logout({ commit }): void {
commit("LOGOUT");
},
login({ dispatch }): Promise<boolean | null> {
return dispatch("initUserFromCookie");
}
}
};
const user = userModule;
export default user;

View File

@@ -28,8 +28,8 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/variables.scss"; @import "src/scss/variables";
@import "scss/media-queries"; @import "src/scss/media-queries";
.button { .button {
font-size: 1.2rem; font-size: 1.2rem;

View File

@@ -141,7 +141,7 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/variables"; @import "src/scss/variables";
.wrapper { .wrapper {
padding: 2rem; padding: 2rem;

View File

@@ -118,8 +118,8 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/variables"; @import "src/scss/variables";
@import "scss/media-queries"; @import "src/scss/media-queries";
.button--group { .button--group {
display: flex; display: flex;

View File

@@ -131,7 +131,7 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/variables"; @import "src/scss/variables";
section { section {
padding: 1.3rem; padding: 1.3rem;

View File

@@ -89,7 +89,7 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/media-queries"; @import "src/scss/media-queries";
.filter { .filter {
margin-top: 0.5rem; margin-top: 0.5rem;

View File

@@ -48,8 +48,8 @@
</script> </script>
<style lang="scss"> <style lang="scss">
@import "scss/variables"; @import "src/scss/variables";
@import "scss/media-queries"; @import "src/scss/media-queries";
.settings { .settings {
padding: 3rem; padding: 3rem;

View File

@@ -112,7 +112,7 @@
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "scss/variables"; @import "src/scss/variables";
section { section {
padding: 1.3rem; padding: 1.3rem;

View File

@@ -28,7 +28,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from "vue"; import { ref, defineProps, onMounted } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import type { Ref } from "vue"; import type { Ref } from "vue";
@@ -94,9 +94,7 @@
background-color: white; background-color: white;
border-radius: 3px; border-radius: 3px;
box-shadow: box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.17), 0 2px 4px 0 rgba(0, 0, 0, 0.08);
0 4px 8px 0 rgba(0, 0, 0, 0.17),
0 2px 4px 0 rgba(0, 0, 0, 0.08);
padding: 0.5rem; padding: 0.5rem;
margin: 1rem 2rem 1rem 0.71rem; margin: 1rem 2rem 1rem 0.71rem;
// max-width: calc(100% - 3rem); // max-width: calc(100% - 3rem);

View File

@@ -1,7 +1,7 @@
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";
import type { RouteRecordRaw, RouteLocationNormalized } from "vue-router"; import type { RouteRecordRaw, RouteLocationNormalized } from "vue-router";
/* eslint-disable-next-line import-x/no-cycle */ /* eslint-disable-next-line import/no-cycle */
import store from "./store"; import store from "./store";
declare global { declare global {
@@ -10,7 +10,7 @@ declare global {
} }
} }
const routes: RouteRecordRaw[] = [ const routes: Array<RouteRecordRaw> = [
{ {
name: "home", name: "home",
path: "/", path: "/",
@@ -99,6 +99,7 @@ const loggedIn = () => store.getters["user/loggedIn"];
const hasPlexAccount = () => store.getters["user/plexUserId"] !== null; const hasPlexAccount = () => store.getters["user/plexUserId"] !== null;
const hamburgerIsOpen = () => store.getters["hamburger/isOpen"]; const hamburgerIsOpen = () => store.getters["hamburger/isOpen"];
/* eslint-disable @typescript-eslint/no-explicit-any */
router.beforeEach( router.beforeEach(
(to: RouteLocationNormalized, from: RouteLocationNormalized, next: any) => { (to: RouteLocationNormalized, from: RouteLocationNormalized, next: any) => {
store.dispatch("documentTitle/updateTitle", to.name); store.dispatch("documentTitle/updateTitle", to.name);

View File

@@ -1,5 +1,5 @@
@import "scss/variables"; @import "src/scss/variables";
@import "scss/media-queries"; @import "src/scss/media-queries";
.filter { .filter {
margin: 1rem; margin: 1rem;

View File

@@ -1,4 +1,4 @@
@import "scss/variables"; @import "src/scss/variables";
// Loading placeholder styling // Loading placeholder styling
@mixin nth-children($points...) { @mixin nth-children($points...) {

View File

@@ -1,4 +1,4 @@
@import "scss/variables"; @import "src/scss/variables";
.noselect { .noselect {
-webkit-touch-callout: none; /* iOS Safari */ -webkit-touch-callout: none; /* iOS Safari */
@@ -14,7 +14,7 @@
} }
html { html {
height: unset; height: 100%;
} }
body { body {
margin: 0; margin: 0;

View File

@@ -1,6 +1,6 @@
// Colors // Colors
// @import "./media-queries"; // @import "./media-queries";
@import "scss/media-queries"; @import "src/scss/media-queries";
:root { :root {
color-scheme: light; color-scheme: light;
@@ -21,8 +21,6 @@
--background-40: rgba(255, 255, 255, 0.4); --background-40: rgba(255, 255, 255, 0.4);
--background-0: rgba(255, 255, 255, 0); --background-0: rgba(255, 255, 255, 0);
--highlight-color: #01d277;
--background-nav-logo: #081c24; --background-nav-logo: #081c24;
--color-green: #01d277; --color-green: #01d277;
--color-green-90: rgba(1, 210, 119, 0.9); --color-green-90: rgba(1, 210, 119, 0.9);

View File

@@ -6,7 +6,7 @@ import torrentModule from "./modules/torrentModule";
import user from "./modules/user"; import user from "./modules/user";
import hamburger from "./modules/hamburger"; import hamburger from "./modules/hamburger";
/* eslint-disable-next-line import-x/no-cycle */ /* eslint-disable-next-line import/no-cycle */
import popup from "./modules/popup"; import popup from "./modules/popup";
const store = createStore({ const store = createStore({

View File

@@ -14,7 +14,9 @@ export const parseJwt = (token: string) => {
const jsonPayload = decodeURIComponent( const jsonPayload = decodeURIComponent(
atob(base64) atob(base64)
.split("") .split("")
.map(c => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`) .map(c => {
return `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`;
})
.join("") .join("")
); );
@@ -60,10 +62,10 @@ export function focusOnNextElement(elementEvent: KeyboardEvent): void {
} }
} }
export function humanMinutes(minutes: number[] | number) { export function humanMinutes(minutes) {
if (minutes instanceof Array) { if (minutes instanceof Array) {
/* eslint-disable-next-line no-param-reassign */ /* eslint-disable-next-line prefer-destructuring, no-param-reassign */
[minutes] = minutes; minutes = minutes[0];
} }
const hours = Math.floor(minutes / 60); const hours = Math.floor(minutes / 60);
@@ -89,7 +91,8 @@ export function setUrlQueryParameter(parameter: string, value: string): void {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append(parameter, value); params.append(parameter, value);
const url = `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : "" const url = `${window.location.protocol}//${window.location.hostname}${
window.location.port ? `:${window.location.port}` : ""
}${window.location.pathname}${params.toString().length ? `?${params}` : ""}`; }${window.location.pathname}${params.toString().length ? `?${params}` : ""}`;
window.history.pushState({}, "search", url); window.history.pushState({}, "search", url);

9
src/vite-env.d.ts vendored
View File

@@ -1,9 +0,0 @@
interface ImportMetaEnv {
readonly VITE_SEASONED_API: string;
readonly VITE_ELASTIC_URL: string;
readonly VITE_ELASTIC_API_KEY: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@@ -12,8 +12,7 @@
"outDir": "lib", "outDir": "lib",
"baseUrl": "/", "baseUrl": "/",
"paths": { "paths": {
"@/*": ["./src/*"], "@": ["src"]
"scss/*": ["./src/scss/*"]
} }
}, },
"include": [ "include": [

View File

@@ -1,14 +0,0 @@
import path from "path";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
// https://vite.dev/config/
export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
scss: path.resolve(__dirname, "./src/scss")
}
},
plugins: [vue()]
});

157
webpack.config.js Normal file
View File

@@ -0,0 +1,157 @@
const path = require("path");
const webpack = require("webpack");
const sass = require("sass");
const HTMLWebpackPlugin = require("html-webpack-plugin");
const { VueLoaderPlugin } = require("vue-loader");
const TerserPlugin = require("terser-webpack-plugin");
const dotenv = require("dotenv").config({ path: "./.env" });
const dotenvExample = require("dotenv").config({ path: "./.env.example" });
const sourcePath = path.resolve(__dirname, "src");
const indexFile = path.join(sourcePath, "index.html");
const javascriptEntry = path.join(sourcePath, "main.ts");
const publicPath = path.resolve(__dirname, "public");
const isProd = process.env.NODE_ENV === "production";
const variables = dotenv.parsed || dotenvExample.parsed;
// Merge inn all process.env values that match dotenv keys
Object.keys(process.env).forEach(key => {
if (key in variables) {
variables[key] = process.env[key];
}
});
module.exports = {
mode: process.env.NODE_ENV,
context: publicPath,
entry: javascriptEntry,
output: {
path: `${publicPath}/dist/`,
publicPath: "/dist/",
filename: "[name].[contenthash].js",
clean: true
},
module: {
rules: [
{
test: /\.js$/,
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"]
},
exclude: /node_modules/
},
{
test: /\.vue$/,
use: ["vue-loader"]
},
{
test: /\.ts$/,
loader: "ts-loader",
exclude: /node_modules/,
options: {
appendTsSuffixTo: [/\.vue$/]
}
},
{
test: /\.scss$/,
use: [
"vue-style-loader",
// isProd ? MiniCssExtractPlugin.loader : "vue-style-loader",
"css-loader",
"sass-loader"
]
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: "file-loader",
options: {
name: "[name].[ext]?[hash]"
}
}
]
},
plugins: [
new VueLoaderPlugin(),
new HTMLWebpackPlugin({
template: indexFile,
filename: "index.html",
minify: isProd
}),
new webpack.DefinePlugin({
"process.env": JSON.stringify(variables)
})
],
resolve: {
extensions: [".js", ".ts", ".vue", ".json", ".scss"],
alias: {
vue: "@vue/runtime-dom",
"@": path.resolve(__dirname, "src"),
src: path.resolve(__dirname, "src"),
assets: `${publicPath}/assets`,
components: path.resolve(__dirname, "src/components")
}
},
devtool: "source-map",
performance: {
hints: false
},
optimization: {
splitChunks: {
chunks: "all",
maxInitialRequests: Infinity,
minSize: 0,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
// get the name. E.g. node_modules/packageName/not/this/part.js
// or node_modules/packageName
const packageName = module.context.match(
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
)[1];
// npm package names are URL-safe, but some servers don't like @ symbols
return `npm.${packageName.replace("@", "")}`;
}
}
}
},
minimizer: [
new TerserPlugin({
parallel: true,
terserOptions: {
sourceMap: true
// https://github.com/webpack-contrib/terser-webpack-plugin#terseroptions
}
})
]
},
devServer: {
static: publicPath,
historyApiFallback: {
index: "/dist/index.html"
},
compress: true,
hot: true,
port: 8080
}
};
if (isProd) {
module.exports.mode = "production";
module.exports.devtool = false;
module.exports.performance.hints = "warning";
}
// enable proxy for anything that hits /Api
// View README or update src/config.ts:SEASONED_API_URL
const { SEASONED_API } = process.env;
if (SEASONED_API) {
module.exports.devServer.proxy = {
"/api": {
target: SEASONED_API,
changeOrigin: true
}
};
}

12928
yarn.lock

File diff suppressed because it is too large Load Diff