Feat: Vue 3 typescripted #76
							
								
								
									
										121
									
								
								.drone.yml
									
									
									
									
									
								
							
							
						
						| @@ -7,38 +7,101 @@ platform: | |||||||
|   os: linux |   os: linux | ||||||
|   arch: amd64 |   arch: amd64 | ||||||
|  |  | ||||||
|  | volumes: | ||||||
|  |   - name: cache | ||||||
|  |     host: | ||||||
|  |       path: /tmp/cache | ||||||
|  |  | ||||||
| steps: | steps: | ||||||
| - name: frontend_install |   - name: Load cached frontend packages | ||||||
|   image: node:13.6.0 |     image: sinlead/drone-cache:1.0.0 | ||||||
|   commands: |     settings: | ||||||
|     - node -v |       action: load | ||||||
|     - yarn --version |       key: yarn.lock | ||||||
| - name: deploy |       mount: node_modules | ||||||
|   image: appleboy/drone-ssh |       prefix: yarn-modules-seasoned | ||||||
|   pull: true |     volumes: | ||||||
|   secrets: |       - name: cache | ||||||
|     - ssh_key |         path: /cache | ||||||
|   when: |  | ||||||
|     event: |   - name: Frontend install | ||||||
|       - push |     image: node:18.2.0 | ||||||
|     branch: |     commands: | ||||||
|       - master |       - node -v | ||||||
|       - drone-test |       - yarn --version | ||||||
|     status: success |       - yarn | ||||||
|   settings: |  | ||||||
|     host: 10.0.0.114 |   - name: Cache frontend packages | ||||||
|     username: root |     image: sinlead/drone-cache:1.0.0 | ||||||
|     key: |     settings: | ||||||
|       from_secret: ssh_key |       action: save | ||||||
|     command_timeout: 600s |       key: yarn.lock | ||||||
|     script: |       mount: node_modules | ||||||
|       - /home/kevin/deploy/seasoned.sh |       prefix: yarn-modules-seasoned | ||||||
|  |     volumes: | ||||||
|  |       - name: cache | ||||||
|  |         path: /cache | ||||||
|  |  | ||||||
|  |   - name: Frontend build | ||||||
|  |     image: node:18.2.0 | ||||||
|  |     commands: | ||||||
|  |       - yarn build | ||||||
|  |     environment: | ||||||
|  |       ELASTIC: | ||||||
|  |         from_secret: ELASTIC | ||||||
|  |       ELASTIC_INDEX: | ||||||
|  |         from_secret: ELASTIC_INDEX | ||||||
|  |       SEASONED_API: | ||||||
|  |         from_secret: SEASONED_API | ||||||
|  |       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 | ||||||
|  |     image: plugins/docker | ||||||
|  |     settings: | ||||||
|  |       registry: ghcr.io | ||||||
|  |       repo: ghcr.io/kevinmidboe/seasoned | ||||||
|  |       dockerfile: Dockerfile | ||||||
|  |       username: | ||||||
|  |         from_secret: GITHUB_USERNAME | ||||||
|  |       password: | ||||||
|  |         from_secret: GITHUB_PASSWORD | ||||||
|  |       tags: latest | ||||||
|  |     when: | ||||||
|  |       event: | ||||||
|  |         - push | ||||||
|  |       branch: | ||||||
|  |         - master | ||||||
|  |  | ||||||
|  |   - name: deploy | ||||||
|  |     image: appleboy/drone-ssh | ||||||
|  |     pull: true | ||||||
|  |     secrets: | ||||||
|  |       - ssh_key | ||||||
|  |     when: | ||||||
|  |       event: | ||||||
|  |         - push | ||||||
|  |       branch: | ||||||
|  |         - master | ||||||
|  |         - drone-test | ||||||
|  |       status: success | ||||||
|  |     settings: | ||||||
|  |       host: 10.0.0.54 | ||||||
|  |       username: root | ||||||
|  |       key: | ||||||
|  |         from_secret: ssh_key | ||||||
|  |       command_timeout: 600s | ||||||
|  |       script: | ||||||
|  |         - /home/kevin/deploy/seasoned.sh | ||||||
|  |  | ||||||
| trigger: | trigger: | ||||||
|   branch: |  | ||||||
|     - master |  | ||||||
|   event: |   event: | ||||||
|     include: |     include: | ||||||
|       - pull_request |  | ||||||
|       - push |       - push | ||||||
|  |       # - pull_request | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								.env.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | |||||||
|  | SEASONED_API= | ||||||
|  | ELASTIC= | ||||||
|  | ELASTIC_INDEX=shows,movies | ||||||
|  | SEASONED_DOMAIN= | ||||||
							
								
								
									
										31
									
								
								.eslintrc
									
									
									
									
									
										Normal 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
									
									
								
							
							
						
						| @@ -1,5 +1,6 @@ | |||||||
| # config file - copy config.json.example | # config file - copy config.json.example | ||||||
| src/config.json | src/config.json | ||||||
|  | .env | ||||||
|  |  | ||||||
| # Build directory | # Build directory | ||||||
| dist/ | dist/ | ||||||
|   | |||||||
| @@ -1,8 +0,0 @@ | |||||||
| <IfModule mod_rewrite.c> |  | ||||||
|   RewriteEngine On |  | ||||||
|   RewriteBase / |  | ||||||
|   RewriteRule ^index\.html$ - [L] |  | ||||||
|   RewriteCond %{REQUEST_FILENAME} !-f |  | ||||||
|   RewriteCond %{REQUEST_FILENAME} !-d |  | ||||||
|   RewriteRule . /index.html [L] |  | ||||||
| </IfModule> |  | ||||||
| @@ -5,6 +5,6 @@ | |||||||
|   "singleQuote": false, |   "singleQuote": false, | ||||||
|   "bracketSpacing": true, |   "bracketSpacing": true, | ||||||
|   "arrowParens": "avoid", |   "arrowParens": "avoid", | ||||||
|   "vueIndentScriptAndStyle": false, |   "vueIndentScriptAndStyle": true, | ||||||
|   "trailingComma": "none" |   "trailingComma": "none" | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										11
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | |||||||
|  | FROM nginx:1.23.1 | ||||||
|  |  | ||||||
|  | COPY public /usr/share/nginx/html | ||||||
|  | COPY nginx.conf /etc/nginx/conf.d/default.conf.template | ||||||
|  | COPY docker-entrypoint.sh /docker-entrypoint.d/05-docker-entrypoint.sh | ||||||
|  |  | ||||||
|  | RUN chmod +x /docker-entrypoint.d/05-docker-entrypoint.sh | ||||||
|  |  | ||||||
|  | EXPOSE 5000 | ||||||
|  |  | ||||||
|  | LABEL org.opencontainers.image.source https://github.com/kevinmidboe/seasoned | ||||||
							
								
								
									
										75
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -1,45 +1,72 @@ | |||||||
| # The Movie Database App | # Seasoned Request | ||||||
|  |  | ||||||
| A Vue.js project. | Seasoned request is frontend vue application for searching, requesting and viewing account watch activity. | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ## Demo |  | ||||||
|  |  | ||||||
| [TMDB Vue App](https://tmdb-vue-app.herokuapp.com/) |  | ||||||
|  |  | ||||||
| ## Config setup | ## Config setup | ||||||
| Set seasonedShows api endpoint and/or elastic.    |  | ||||||
|  - SeasonedShows [can be found here](https://github.com/kevinmidboe/seasonedshows) and is the matching backend to fetch tmdb search results, tmdb lists, request new content, check plex status and lets owner search and add torrents to download. |  | ||||||
|  - Elastic is optional and can be used for a instant search feature for all movies and shows registered in tmdb. |  | ||||||
|  |  | ||||||
| ```json | ```bash | ||||||
| { | # make copy of example environment file | ||||||
|   "SEASONED_URL": "http://localhost:31459/api", | cp .env.example .env | ||||||
|   "ELASTIC_URL": "http://localhost:9200" |  | ||||||
| } |  | ||||||
| ``` | ``` | ||||||
| *Set ELASTIC_URL to undefined or false to disable* |  | ||||||
|  |  | ||||||
| ## Build Setup | ```bash | ||||||
|  | # .env sane default values | ||||||
|  | SEASONED_API= | ||||||
|  | ELASTIC= | ||||||
|  | ELASTIC_INDEX=shows,movies | ||||||
|  | SEASONED_DOMAIN= | ||||||
|  | ``` | ||||||
|  |  | ||||||
| ``` bash | - Leave SEASONED_API empty to request `/api` from same origin and proxy passed by nginx, set if hosting [seasonedShows backend api](https://github.com/KevinMidboe/seasonedShows) locally. | ||||||
|  | - Elastic is optional and can be used for a instant search feature for all movies and shows registered in tmdb, leave empty to disable. | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | # .env example values | ||||||
|  | SEASONED_API=http://localhost:31459 | ||||||
|  | ELASTIC=http://localhost:9200 | ||||||
|  | ELASTIC_INDEX=shows,movies | ||||||
|  | SEASONED_DOMAIN=request.movie | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Build Steps | ||||||
|  |  | ||||||
|  | ```bash | ||||||
| # install dependencies | # install dependencies | ||||||
| npm install | yarn | ||||||
|  |  | ||||||
| # serve with hot reload at localhost:8080 | # build vue project using webpack | ||||||
| npm run dev | yarn build | ||||||
|  |  | ||||||
| # build for production with minification | # test or host built files using docker, might require sudo: | ||||||
| npm run build | docker build -t seasoned . | ||||||
|  | docker run -d -p 5000:5000 --name seasoned-request --env-file .env seasoned | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| For detailed explanation on how things work, consult the [docs for vue-loader](http://vuejs.github.io/vue-loader). | ## Development Steps | ||||||
| This app uses [history mode](https://router.vuejs.org/en/essentials/history-mode.html) |  | ||||||
|  | ```bash | ||||||
|  | # serve project with hot reloading at localhost:8080 | ||||||
|  | yarn dev | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | To proxy requests to `/api` either update `SEASONED_API` in `.env` or run set environment variable, e.g.: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | # export and run | ||||||
|  | export SEASONED_API=http://localhost:31459 | ||||||
|  | yarn dev | ||||||
|  |  | ||||||
|  | # or run with environment variable inline | ||||||
|  | SEASONED_API=http://localhost:31459 yarn dev | ||||||
|  | ``` | ||||||
|  |  | ||||||
| ## Documentation | ## Documentation | ||||||
|  |  | ||||||
| All api functions are documented in `/docs` and [found here](docs/api.md).   | All api functions are documented in `/docs` and [found here](docs/api.md).   | ||||||
| [html version also available](http://htmlpreview.github.io/?https://github.com/KevinMidboe/seasoned/blob/release/v2/docs/api/index.html) | [html version also available](http://htmlpreview.github.io/?https://github.com/KevinMidboe/seasoned/blob/release/v2/docs/api/index.html) | ||||||
|  |  | ||||||
| ## License | ## License | ||||||
|  |  | ||||||
| [MIT](https://github.com/dmtrbrl/tmdb-app/blob/master/LICENSE) | [MIT](https://github.com/dmtrbrl/tmdb-app/blob/master/LICENSE) | ||||||
|   | |||||||
							
								
								
									
										9
									
								
								docker-entrypoint.sh
									
									
									
									
									
										Normal 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 "$@" | ||||||
							
								
								
									
										30
									
								
								nginx.conf
									
									
									
									
									
										Normal 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; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										74
									
								
								package.json
									
									
									
									
									
								
							
							
						
						| @@ -5,41 +5,57 @@ | |||||||
|   "author": "Kevin Midboe", |   "author": "Kevin Midboe", | ||||||
|   "private": true, |   "private": true, | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "dev": "cross-env NODE_ENV=development webpack server", |     "dev": "NODE_ENV=development webpack server", | ||||||
|     "build": "cross-env NODE_ENV=production webpack-cli build --progress", |     "build": "yarn build:ts && yarn build:webpack", | ||||||
|  |     "build:ts": "tsc --project tsconfig.json", | ||||||
|  |     "build:webpack": "NODE_ENV=production webpack-cli build --progress", | ||||||
|     "postbuild": "cp public/dist/index.html public/index.html", |     "postbuild": "cp public/dist/index.html public/index.html", | ||||||
|     "clean": "rm -r public/dist 2> /dev/null; rm public/index.html 2> /dev/null", |     "clean": "rm -r public/dist 2> /dev/null; rm public/index.html 2> /dev/null; rm -r lib 2> /dev/null", | ||||||
|     "start": "node server.js", |     "start": "echo 'Start using docker, consult README'", | ||||||
|     "docs": "documentation build src/api.js -f html -o docs/api && documentation build src/api.js -f md -o docs/api.md" |     "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" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "chart.js": "^2.9.2", |     "chart.js": "3.9.1", | ||||||
|     "connect-history-api-fallback": "1.6.0", |     "connect-history-api-fallback": "2.0.0", | ||||||
|     "cross-env": "6.0.0", |     "dotenv": "^16.0.1", | ||||||
|     "express": "4.17.3", |     "express": "4.18.1", | ||||||
|     "vue": "^3.2.37", |     "vue": "3.2.37", | ||||||
|     "vue-router": "4.1.2", |     "vue-router": "4.1.3", | ||||||
|     "vuex": "3.6.2" |     "vuex": "4.0.2" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@babel/core": "7.17.2", |     "@babel/core": "7.18.10", | ||||||
|     "@babel/plugin-transform-runtime": "7.17.0", |     "@babel/plugin-transform-runtime": "7.18.10", | ||||||
|     "@babel/preset-env": "7.16.11", |     "@babel/preset-env": "7.18.10", | ||||||
|     "@babel/runtime": "7.17.2", |     "@babel/runtime": "7.18.9", | ||||||
|     "@types/node": "^18.6.1", |     "@types/express": "4.17.13", | ||||||
|     "babel-loader": "8.2.3", |     "@types/node": "18.6.1", | ||||||
|     "css-loader": "6.7.0", |     "@typescript-eslint/eslint-plugin": "5.33.0", | ||||||
|     "documentation": "^11.0.0", |     "@typescript-eslint/parser": "5.33.0", | ||||||
|  |     "@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", |     "file-loader": "6.2.0", | ||||||
|     "html-webpack-plugin": "^5.5.0", |     "html-webpack-plugin": "5.5.0", | ||||||
|     "sass": "1.49.9", |     "prettier": "2.7.1", | ||||||
|     "sass-loader": "12.6.0", |     "sass": "1.54.3", | ||||||
|     "terser-webpack-plugin": "5.3.1", |     "sass-loader": "13.0.2", | ||||||
|     "ts-loader": "^9.3.1", |     "terser-webpack-plugin": "5.3.3", | ||||||
|     "typescript": "^4.7.4", |     "ts-loader": "9.3.1", | ||||||
|  |     "typescript": "4.7.4", | ||||||
|     "vue-loader": "17.0.0", |     "vue-loader": "17.0.0", | ||||||
|     "webpack": "5.70.0", |     "webpack": "5.74.0", | ||||||
|     "webpack-cli": "4.9.2", |     "webpack-cli": "4.10.0", | ||||||
|     "webpack-dev-server": "4.7.4" |     "webpack-dev-server": "4.9.3" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ | |||||||
|   height="900" |   height="900" | ||||||
|   viewBox="0 0 600 900" |   viewBox="0 0 600 900" | ||||||
|   fill="none" |   fill="none" | ||||||
|   xmlns="http://www.w3.org/2000/svg" |   version="1.1" xmlns="http://www.w3.org/2000/svg" | ||||||
| > | > | ||||||
|   <rect width="600" height="900" fill="white" /> |   <rect width="600" height="900" fill="white" /> | ||||||
|   <path |   <path | ||||||
|   | |||||||
| Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB | 
| @@ -3,7 +3,7 @@ | |||||||
|   height="900" |   height="900" | ||||||
|   viewBox="0 0 600 900" |   viewBox="0 0 600 900" | ||||||
|   fill="none" |   fill="none" | ||||||
|   xmlns="http://www.w3.org/2000/svg" |   version="1.1" xmlns="http://www.w3.org/2000/svg" | ||||||
| > | > | ||||||
|   <rect width="600" height="900" fill="white" /> |   <rect width="600" height="900" fill="white" /> | ||||||
|   <path |   <path | ||||||
|   | |||||||
| Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB | 
							
								
								
									
										18
									
								
								server.js
									
									
									
									
									
								
							
							
						
						| @@ -1,18 +0,0 @@ | |||||||
| const express = require("express"); |  | ||||||
| const path = require("path"); |  | ||||||
| const history = require("connect-history-api-fallback"); |  | ||||||
|  |  | ||||||
| const publicPath = path.join(__dirname, "public"); |  | ||||||
|  |  | ||||||
| app = express(); |  | ||||||
| app.use("/", express.static(publicPath)); |  | ||||||
| app.use(history({ index: "/" })); |  | ||||||
|  |  | ||||||
| app.get("/", function (req, res) { |  | ||||||
|   res.sendFile(`${publicPath}/index.html`); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| const port = process.env.PORT || 5001; |  | ||||||
| console.log("Server runnning at port:", port); |  | ||||||
|  |  | ||||||
| app.listen(port); |  | ||||||
							
								
								
									
										91
									
								
								src/App.vue
									
									
									
									
									
								
							
							
						
						| @@ -1,14 +1,14 @@ | |||||||
| <template> | <template> | ||||||
|   <div id="app"> |   <div id="app"> | ||||||
|     <!-- Header and hamburger navigation --> |     <!-- Header and hamburger navigation --> | ||||||
|     <NavigationHeader class="header"></NavigationHeader> |     <NavigationHeader class="header" /> | ||||||
|  |  | ||||||
|     <div class="navigation-icons-gutter desktop-only"> |     <div class="navigation-icons-gutter desktop-only"> | ||||||
|       <NavigationIcons /> |       <NavigationIcons /> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <!-- Display the component assigned to the given route (default: home) --> |     <!-- Display the component assigned to the given route (default: home) --> | ||||||
|     <router-view class="content" :key="$route.fullPath"></router-view> |     <router-view :key="router.currentRoute.value.path" class="content" /> | ||||||
|  |  | ||||||
|     <!-- Popup that will show above existing rendered content --> |     <!-- Popup that will show above existing rendered content --> | ||||||
|     <popup /> |     <popup /> | ||||||
| @@ -17,61 +17,54 @@ | |||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| import NavigationHeader from "@/components/header/NavigationHeader"; |   import { useRouter } from "vue-router"; | ||||||
| import NavigationIcons from "@/components/header/NavigationIcons"; |   import NavigationHeader from "@/components/header/NavigationHeader.vue"; | ||||||
| import Popup from "@/components/Popup"; |   import NavigationIcons from "@/components/header/NavigationIcons.vue"; | ||||||
| import DarkmodeToggle from "@/components/ui/DarkmodeToggle"; |   import Popup from "@/components/Popup.vue"; | ||||||
|  |   import DarkmodeToggle from "@/components/ui/DarkmodeToggle.vue"; | ||||||
|  |  | ||||||
| export default { |   const router = useRouter(); | ||||||
|   name: "app", |  | ||||||
|   components: { |  | ||||||
|     NavigationHeader, |  | ||||||
|     NavigationIcons, |  | ||||||
|     Popup, |  | ||||||
|     DarkmodeToggle |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss"> | <style lang="scss"> | ||||||
| @import "src/scss/main"; |   @import "src/scss/main"; | ||||||
| @import "src/scss/media-queries"; |   @import "src/scss/media-queries"; | ||||||
|  |  | ||||||
| #app { |   #app { | ||||||
|   display: grid; |  | ||||||
|   grid-template-rows: var(--header-size); |  | ||||||
|   grid-template-columns: var(--header-size) 1fr; |  | ||||||
|  |  | ||||||
|   @include mobile { |  | ||||||
|     grid-template-columns: 1fr; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .header { |  | ||||||
|     position: fixed; |  | ||||||
|     top: 0; |  | ||||||
|     width: 100%; |  | ||||||
|     z-index: 15; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .navigation-icons-gutter { |  | ||||||
|     position: fixed; |  | ||||||
|     height: 100vh; |  | ||||||
|     margin: 0; |  | ||||||
|     top: var(--header-size); |  | ||||||
|     width: var(--header-size); |  | ||||||
|     background-color: var(--background-color-secondary); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .content { |  | ||||||
|     display: grid; |     display: grid; | ||||||
|     grid-column: 2 / 3; |     grid-template-rows: var(--header-size); | ||||||
|     grid-row: 2; |     grid-template-columns: var(--header-size) 1fr; | ||||||
|     z-index: 5; |  | ||||||
|  |  | ||||||
|     @include mobile { |     @include mobile { | ||||||
|       grid-column: 1 / 3; |       grid-template-columns: 1fr; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .header { | ||||||
|  |       position: fixed; | ||||||
|  |       top: 0; | ||||||
|  |       width: 100%; | ||||||
|  |       z-index: 15; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .navigation-icons-gutter { | ||||||
|  |       position: fixed; | ||||||
|  |       height: 100vh; | ||||||
|  |       margin: 0; | ||||||
|  |       top: var(--header-size); | ||||||
|  |       width: var(--header-size); | ||||||
|  |       background-color: var(--background-color-secondary); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .content { | ||||||
|  |       display: grid; | ||||||
|  |       grid-column: 2 / 3; | ||||||
|  |       grid-row: 2; | ||||||
|  |       z-index: 5; | ||||||
|  |  | ||||||
|  |       @include mobile { | ||||||
|  |         grid-column: 1 / 3; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
							
								
								
									
										210
									
								
								src/api.ts
									
									
									
									
									
								
							
							
						
						| @@ -1,36 +1,24 @@ | |||||||
| import config from "./config"; | import { IList, IMediaCredits, IPersonCredits } from "./interfaces/IList"; | ||||||
| import { IList } from "./interfaces/IList"; |  | ||||||
|  |  | ||||||
| let { SEASONED_URL, ELASTIC_URL, ELASTIC_INDEX } = config; | const { ELASTIC, ELASTIC_INDEX } = process.env; | ||||||
| if (!SEASONED_URL) { | const API_HOSTNAME = window.location.origin; | ||||||
|   SEASONED_URL = window.location.origin; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // TODO |  | ||||||
| //  - Move autorization token and errors here? |  | ||||||
|  |  | ||||||
| const checkStatusAndReturnJson = response => { |  | ||||||
|   if (!response.ok) { |  | ||||||
|     throw response; |  | ||||||
|   } |  | ||||||
|   return response.json(); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| // - - - TMDB - - - | // - - - TMDB - - - | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Fetches tmdb movie by id. Can optionally include cast credits in result object. |  * Fetches tmdb movie by id. Can optionally include cast credits in result object. | ||||||
|  * @param {number} id |  * @param {number} id | ||||||
|  * @param {boolean} [credits=false] Include credits |  | ||||||
|  * @returns {object} Tmdb response |  * @returns {object} Tmdb response | ||||||
|  */ |  */ | ||||||
| const getMovie = ( | const getMovie = ( | ||||||
|   id, |   id, | ||||||
|   checkExistance = false, |   { | ||||||
|   credits = false, |     checkExistance, | ||||||
|   release_dates = false |     credits, | ||||||
|  |     releaseDates | ||||||
|  |   }: { checkExistance: boolean; credits: boolean; releaseDates?: boolean } | ||||||
| ) => { | ) => { | ||||||
|   const url = new URL("/api/v2/movie", SEASONED_URL); |   const url = new URL("/api/v2/movie", API_HOSTNAME); | ||||||
|   url.pathname = `${url.pathname}/${id.toString()}`; |   url.pathname = `${url.pathname}/${id.toString()}`; | ||||||
|   if (checkExistance) { |   if (checkExistance) { | ||||||
|     url.searchParams.append("check_existance", "true"); |     url.searchParams.append("check_existance", "true"); | ||||||
| @@ -38,14 +26,14 @@ const getMovie = ( | |||||||
|   if (credits) { |   if (credits) { | ||||||
|     url.searchParams.append("credits", "true"); |     url.searchParams.append("credits", "true"); | ||||||
|   } |   } | ||||||
|   if (release_dates) { |   if (releaseDates) { | ||||||
|     url.searchParams.append("release_dates", "true"); |     url.searchParams.append("release_dates", "true"); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return fetch(url.href) |   return fetch(url.href) | ||||||
|     .then(resp => resp.json()) |     .then(resp => resp.json()) | ||||||
|     .catch(error => { |     .catch(error => { | ||||||
|       console.error(`api error getting movie: ${id}`); |       console.error(`api error getting movie: ${id}`); // eslint-disable-line no-console | ||||||
|       throw error; |       throw error; | ||||||
|     }); |     }); | ||||||
| }; | }; | ||||||
| @@ -56,8 +44,15 @@ const getMovie = ( | |||||||
|  * @param {boolean} [credits=false] Include credits |  * @param {boolean} [credits=false] Include credits | ||||||
|  * @returns {object} Tmdb response |  * @returns {object} Tmdb response | ||||||
|  */ |  */ | ||||||
| const getShow = (id, checkExistance = false, credits = false) => { | const getShow = ( | ||||||
|   const url = new URL("/api/v2/show", SEASONED_URL); |   id, | ||||||
|  |   { | ||||||
|  |     checkExistance, | ||||||
|  |     credits, | ||||||
|  |     releaseDates | ||||||
|  |   }: { checkExistance: boolean; credits: boolean; releaseDates?: boolean } | ||||||
|  | ) => { | ||||||
|  |   const url = new URL("/api/v2/show", API_HOSTNAME); | ||||||
|   url.pathname = `${url.pathname}/${id.toString()}`; |   url.pathname = `${url.pathname}/${id.toString()}`; | ||||||
|   if (checkExistance) { |   if (checkExistance) { | ||||||
|     url.searchParams.append("check_existance", "true"); |     url.searchParams.append("check_existance", "true"); | ||||||
| @@ -65,19 +60,18 @@ const getShow = (id, checkExistance = false, credits = false) => { | |||||||
|   if (credits) { |   if (credits) { | ||||||
|     url.searchParams.append("credits", "true"); |     url.searchParams.append("credits", "true"); | ||||||
|   } |   } | ||||||
|  |   if (releaseDates) { | ||||||
|  |     url.searchParams.append("release_dates", "true"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   return fetch(url.href) |   return fetch(url.href) | ||||||
|     .then(resp => resp.json()) |     .then(resp => resp.json()) | ||||||
|     .catch(error => { |     .catch(error => { | ||||||
|       console.error(`api error getting show: ${id}`); |       console.error(`api error getting show: ${id}`); // eslint-disable-line no-console | ||||||
|       throw error; |       throw error; | ||||||
|     }); |     }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| function delay(ms) { |  | ||||||
|   return new Promise(resolve => setTimeout(resolve, ms)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Fetches tmdb person by id. Can optionally include cast credits in result object. |  * Fetches tmdb person by id. Can optionally include cast credits in result object. | ||||||
|  * @param {number} id |  * @param {number} id | ||||||
| @@ -85,7 +79,7 @@ function delay(ms) { | |||||||
|  * @returns {object} Tmdb response |  * @returns {object} Tmdb response | ||||||
|  */ |  */ | ||||||
| const getPerson = (id, credits = false) => { | const getPerson = (id, credits = false) => { | ||||||
|   const url = new URL("/api/v2/person", SEASONED_URL); |   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) { | ||||||
|     url.searchParams.append("credits", "true"); |     url.searchParams.append("credits", "true"); | ||||||
| @@ -94,36 +88,24 @@ const getPerson = (id, credits = false) => { | |||||||
|   return fetch(url.href) |   return fetch(url.href) | ||||||
|     .then(resp => resp.json()) |     .then(resp => resp.json()) | ||||||
|     .catch(error => { |     .catch(error => { | ||||||
|       console.error(`api error getting person: ${id}`); |       console.error(`api error getting person: ${id}`); // eslint-disable-line no-console | ||||||
|       throw error; |       throw error; | ||||||
|     }); |     }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const getCredits = (type, id) => { |  | ||||||
|   if (type === "movie") { |  | ||||||
|     return getMovieCredits(id); |  | ||||||
|   } else if (type === "show") { |  | ||||||
|     return getShowCredits(id); |  | ||||||
|   } else if (type === "person") { |  | ||||||
|     return getPersonCredits(id); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return []; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Fetches tmdb movie credits by id. |  * Fetches tmdb movie credits by id. | ||||||
|  * @param {number} id |  * @param {number} id | ||||||
|  * @returns {object} Tmdb response |  * @returns {object} Tmdb response | ||||||
|  */ |  */ | ||||||
| const getMovieCredits = id => { | const getMovieCredits = (id: number): Promise<IMediaCredits> => { | ||||||
|   const url = new URL("/api/v2/movie", SEASONED_URL); |   const url = new URL("/api/v2/movie", API_HOSTNAME); | ||||||
|   url.pathname = `${url.pathname}/${id.toString()}/credits`; |   url.pathname = `${url.pathname}/${id.toString()}/credits`; | ||||||
|  |  | ||||||
|   return fetch(url.href) |   return fetch(url.href) | ||||||
|     .then(resp => resp.json()) |     .then(resp => resp.json()) | ||||||
|     .catch(error => { |     .catch(error => { | ||||||
|       console.error(`api error getting movie: ${id}`); |       console.error(`api error getting movie: ${id}`); // eslint-disable-line no-console | ||||||
|       throw error; |       throw error; | ||||||
|     }); |     }); | ||||||
| }; | }; | ||||||
| @@ -133,14 +115,14 @@ const getMovieCredits = id => { | |||||||
|  * @param {number} id |  * @param {number} id | ||||||
|  * @returns {object} Tmdb response |  * @returns {object} Tmdb response | ||||||
|  */ |  */ | ||||||
| const getShowCredits = id => { | const getShowCredits = (id: number): Promise<IMediaCredits> => { | ||||||
|   const url = new URL("/api/v2/show", SEASONED_URL); |   const url = new URL("/api/v2/show", API_HOSTNAME); | ||||||
|   url.pathname = `${url.pathname}/${id.toString()}/credits`; |   url.pathname = `${url.pathname}/${id.toString()}/credits`; | ||||||
|  |  | ||||||
|   return fetch(url.href) |   return fetch(url.href) | ||||||
|     .then(resp => resp.json()) |     .then(resp => resp.json()) | ||||||
|     .catch(error => { |     .catch(error => { | ||||||
|       console.error(`api error getting show: ${id}`); |       console.error(`api error getting show: ${id}`); // eslint-disable-line no-console | ||||||
|       throw error; |       throw error; | ||||||
|     }); |     }); | ||||||
| }; | }; | ||||||
| @@ -150,14 +132,14 @@ const getShowCredits = id => { | |||||||
|  * @param {number} id |  * @param {number} id | ||||||
|  * @returns {object} Tmdb response |  * @returns {object} Tmdb response | ||||||
|  */ |  */ | ||||||
| const getPersonCredits = id => { | const getPersonCredits = (id: number): Promise<IPersonCredits> => { | ||||||
|   const url = new URL("/api/v2/person", SEASONED_URL); |   const url = new URL("/api/v2/person", API_HOSTNAME); | ||||||
|   url.pathname = `${url.pathname}/${id.toString()}/credits`; |   url.pathname = `${url.pathname}/${id.toString()}/credits`; | ||||||
|  |  | ||||||
|   return fetch(url.href) |   return fetch(url.href) | ||||||
|     .then(resp => resp.json()) |     .then(resp => resp.json()) | ||||||
|     .catch(error => { |     .catch(error => { | ||||||
|       console.error(`api error getting person: ${id}`); |       console.error(`api error getting person: ${id}`); // eslint-disable-line no-console | ||||||
|       throw error; |       throw error; | ||||||
|     }); |     }); | ||||||
| }; | }; | ||||||
| @@ -168,15 +150,12 @@ const getPersonCredits = id => { | |||||||
|  * @param {number} [page=1] |  * @param {number} [page=1] | ||||||
|  * @returns {object} Tmdb list response |  * @returns {object} Tmdb list response | ||||||
|  */ |  */ | ||||||
| const getTmdbMovieListByName = ( | const getTmdbMovieListByName = (name: string, page = 1): Promise<IList> => { | ||||||
|   name: string, |   const url = new URL(`/api/v2/movie/${name}`, API_HOSTNAME); | ||||||
|   page: number = 1 |  | ||||||
| ): Promise<IList> => { |  | ||||||
|   const url = new URL("/api/v2/movie/" + name, SEASONED_URL); |  | ||||||
|   url.searchParams.append("page", page.toString()); |   url.searchParams.append("page", page.toString()); | ||||||
|  |  | ||||||
|   return fetch(url.href).then(resp => resp.json()); |   return fetch(url.href).then(resp => resp.json()); | ||||||
|   // .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error }) |   // .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error }) // eslint-disable-line no-console | ||||||
| }; | }; | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -184,16 +163,16 @@ const getTmdbMovieListByName = ( | |||||||
|  * @param {number} [page=1] |  * @param {number} [page=1] | ||||||
|  * @returns {object} Request response |  * @returns {object} Request response | ||||||
|  */ |  */ | ||||||
| const getRequests = (page: number = 1) => { | const getRequests = (page = 1) => { | ||||||
|   const url = new URL("/api/v2/request", SEASONED_URL); |   const url = new URL("/api/v2/request", API_HOSTNAME); | ||||||
|   url.searchParams.append("page", page.toString()); |   url.searchParams.append("page", page.toString()); | ||||||
|  |  | ||||||
|   return fetch(url.href).then(resp => resp.json()); |   return fetch(url.href).then(resp => resp.json()); | ||||||
|   // .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error }) |   // .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error }) // eslint-disable-line no-console | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const getUserRequests = (page = 1) => { | const getUserRequests = (page = 1) => { | ||||||
|   const url = new URL("/api/v1/user/requests", SEASONED_URL); |   const url = new URL("/api/v1/user/requests", API_HOSTNAME); | ||||||
|   url.searchParams.append("page", page.toString()); |   url.searchParams.append("page", page.toString()); | ||||||
|  |  | ||||||
|   return fetch(url.href).then(resp => resp.json()); |   return fetch(url.href).then(resp => resp.json()); | ||||||
| @@ -206,7 +185,7 @@ const getUserRequests = (page = 1) => { | |||||||
|  * @returns {object} Tmdb response |  * @returns {object} Tmdb response | ||||||
|  */ |  */ | ||||||
| const searchTmdb = (query, page = 1, adult = false, mediaType = null) => { | const searchTmdb = (query, page = 1, adult = false, mediaType = null) => { | ||||||
|   const url = new URL("/api/v2/search", SEASONED_URL); |   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}`; | ||||||
|   } |   } | ||||||
| @@ -218,7 +197,7 @@ const searchTmdb = (query, page = 1, adult = false, mediaType = null) => { | |||||||
|   return fetch(url.href) |   return fetch(url.href) | ||||||
|     .then(resp => resp.json()) |     .then(resp => resp.json()) | ||||||
|     .catch(error => { |     .catch(error => { | ||||||
|       console.error(`api error searching: ${query}, page: ${page}`); |       console.error(`api error searching: ${query}, page: ${page}`); // eslint-disable-line no-console | ||||||
|       throw error; |       throw error; | ||||||
|     }); |     }); | ||||||
| }; | }; | ||||||
| @@ -232,13 +211,13 @@ const searchTmdb = (query, page = 1, adult = false, mediaType = null) => { | |||||||
|  * @returns {object} Torrent response |  * @returns {object} Torrent response | ||||||
|  */ |  */ | ||||||
| const searchTorrents = query => { | const searchTorrents = query => { | ||||||
|   const url = new URL("/api/v1/pirate/search", SEASONED_URL); |   const url = new URL("/api/v1/pirate/search", API_HOSTNAME); | ||||||
|   url.searchParams.append("query", query); |   url.searchParams.append("query", query); | ||||||
|  |  | ||||||
|   return fetch(url.href) |   return fetch(url.href) | ||||||
|     .then(resp => resp.json()) |     .then(resp => resp.json()) | ||||||
|     .catch(error => { |     .catch(error => { | ||||||
|       console.error(`api error searching torrents: ${query}`); |       console.error(`api error searching torrents: ${query}`); // eslint-disable-line no-console | ||||||
|       throw error; |       throw error; | ||||||
|     }); |     }); | ||||||
| }; | }; | ||||||
| @@ -247,26 +226,26 @@ const searchTorrents = query => { | |||||||
|  * Add magnet to download queue. |  * Add magnet to download queue. | ||||||
|  * @param {string} magnet Magnet link |  * @param {string} magnet Magnet link | ||||||
|  * @param {boolean} name Name of torrent |  * @param {boolean} name Name of torrent | ||||||
|  * @param {boolean} tmdb_id |  * @param {boolean} tmdbId | ||||||
|  * @returns {object} Success/Failure response |  * @returns {object} Success/Failure response | ||||||
|  */ |  */ | ||||||
| const addMagnet = (magnet, name, tmdb_id) => { | const addMagnet = (magnet: string, name: string, tmdbId: number | null) => { | ||||||
|   const url = new URL("/api/v1/pirate/add", SEASONED_URL); |   const url = new URL("/api/v1/pirate/add", API_HOSTNAME); | ||||||
|  |  | ||||||
|   const options = { |   const options = { | ||||||
|     method: "POST", |     method: "POST", | ||||||
|     headers: { "Content-Type": "application/json" }, |     headers: { "Content-Type": "application/json" }, | ||||||
|     body: JSON.stringify({ |     body: JSON.stringify({ | ||||||
|       magnet: magnet, |       magnet, | ||||||
|       name: name, |       name, | ||||||
|       tmdb_id: tmdb_id |       tmdb_id: tmdbId | ||||||
|     }) |     }) | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   return fetch(url.href, options) |   return fetch(url.href, options) | ||||||
|     .then(resp => resp.json()) |     .then(resp => resp.json()) | ||||||
|     .catch(error => { |     .catch(error => { | ||||||
|       console.error(`api error adding magnet: ${name} ${error}`); |       console.error(`api error adding magnet: ${name} ${error}`); // eslint-disable-line no-console | ||||||
|       throw error; |       throw error; | ||||||
|     }); |     }); | ||||||
| }; | }; | ||||||
| @@ -281,7 +260,7 @@ const addMagnet = (magnet, name, tmdb_id) => { | |||||||
|  * @returns {object} Success/Failure response |  * @returns {object} Success/Failure response | ||||||
|  */ |  */ | ||||||
| const request = (id, type) => { | const request = (id, type) => { | ||||||
|   const url = new URL("/api/v2/request", SEASONED_URL); |   const url = new URL("/api/v2/request", API_HOSTNAME); | ||||||
|  |  | ||||||
|   const options = { |   const options = { | ||||||
|     method: "POST", |     method: "POST", | ||||||
| @@ -292,7 +271,7 @@ const request = (id, type) => { | |||||||
|   return fetch(url.href, options) |   return fetch(url.href, options) | ||||||
|     .then(resp => resp.json()) |     .then(resp => resp.json()) | ||||||
|     .catch(error => { |     .catch(error => { | ||||||
|       console.error(`api error requesting: ${id}, type: ${type}`); |       console.error(`api error requesting: ${id}, type: ${type}`); // eslint-disable-line no-console | ||||||
|       throw error; |       throw error; | ||||||
|     }); |     }); | ||||||
| }; | }; | ||||||
| @@ -304,28 +283,22 @@ const request = (id, type) => { | |||||||
|  * @returns {object} Success/Failure response |  * @returns {object} Success/Failure response | ||||||
|  */ |  */ | ||||||
| const getRequestStatus = (id, type = undefined) => { | const getRequestStatus = (id, type = undefined) => { | ||||||
|   const url = new URL("/api/v2/request", SEASONED_URL); |   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); | ||||||
|  |  | ||||||
|   return fetch(url.href) |   return fetch(url.href) | ||||||
|     .then(resp => { |     .then(resp => { | ||||||
|       const status = resp.status; |       const { status } = resp; | ||||||
|       if (status === 200) { |       if (status === 200) return true; | ||||||
|         return true; |  | ||||||
|       } else if (status === 404) { |       return false; | ||||||
|         return false; |  | ||||||
|       } else { |  | ||||||
|         console.error( |  | ||||||
|           `api error getting request status for id ${id} and type ${type}` |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|     }) |     }) | ||||||
|     .catch(err => Promise.reject(err)); |     .catch(err => Promise.reject(err)); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const watchLink = (title, year) => { | const watchLink = (title, year) => { | ||||||
|   const url = new URL("/api/v1/plex/watch-link", SEASONED_URL); |   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); | ||||||
|  |  | ||||||
| @@ -335,7 +308,7 @@ const watchLink = (title, year) => { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| const movieImages = id => { | const movieImages = id => { | ||||||
|   const url = new URL(`v2/movie/${id}/images`, SEASONED_URL); |   const url = new URL(`v2/movie/${id}/images`, API_HOSTNAME); | ||||||
|  |  | ||||||
|   return fetch(url.href).then(resp => resp.json()); |   return fetch(url.href).then(resp => resp.json()); | ||||||
| }; | }; | ||||||
| @@ -343,7 +316,7 @@ const movieImages = id => { | |||||||
| // - - - Seasoned user endpoints - - - | // - - - Seasoned user endpoints - - - | ||||||
|  |  | ||||||
| const register = (username, password) => { | const register = (username, password) => { | ||||||
|   const url = new URL("/api/v1/user", SEASONED_URL); |   const url = new URL("/api/v1/user", API_HOSTNAME); | ||||||
|   const options = { |   const options = { | ||||||
|     method: "POST", |     method: "POST", | ||||||
|     headers: { "Content-Type": "application/json" }, |     headers: { "Content-Type": "application/json" }, | ||||||
| @@ -353,17 +326,17 @@ const register = (username, password) => { | |||||||
|   return fetch(url.href, options) |   return fetch(url.href, options) | ||||||
|     .then(resp => resp.json()) |     .then(resp => resp.json()) | ||||||
|     .catch(error => { |     .catch(error => { | ||||||
|       console.error( |       const errorMessage = | ||||||
|         "Unexpected error occured before receiving response. Error:", |         "Unexpected error occured before receiving response. Error:"; | ||||||
|         error |       // eslint-disable-next-line no-console | ||||||
|       ); |       console.error(errorMessage, error); | ||||||
|       // TODO log to sentry the issue here |       // TODO log to sentry the issue here | ||||||
|       throw error; |       throw error; | ||||||
|     }); |     }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const login = (username, password, throwError = false) => { | const login = (username, password, throwError = false) => { | ||||||
|   const url = new URL("/api/v1/user/login", SEASONED_URL); |   const url = new URL("/api/v1/user/login", API_HOSTNAME); | ||||||
|   const options = { |   const options = { | ||||||
|     method: "POST", |     method: "POST", | ||||||
|     headers: { "Content-Type": "application/json" }, |     headers: { "Content-Type": "application/json" }, | ||||||
| @@ -371,38 +344,40 @@ const login = (username, password, throwError = false) => { | |||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   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) throw resp; |     if (throwError) throw resp; | ||||||
|     else console.error("Error occured when trying to sign in.\nError:", resp); |     console.error("Error occured when trying to sign in.\nError:", resp); // eslint-disable-line no-console | ||||||
|  |     return Promise.reject(resp); | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const logout = (throwError = false) => { | const logout = (throwError = false) => { | ||||||
|   const url = new URL("/api/v1/user/logout", SEASONED_URL); |   const url = new URL("/api/v1/user/logout", API_HOSTNAME); | ||||||
|   const options = { method: "POST" }; |   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) throw resp; |     if (throwError) throw resp; | ||||||
|     else console.error("Error occured when trying to log out.\nError:", resp); |     console.error("Error occured when trying to log out.\nError:", resp); // eslint-disable-line no-console | ||||||
|  |     return Promise.reject(resp); | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const getSettings = () => { | const getSettings = () => { | ||||||
|   const url = new URL("/api/v1/user/settings", SEASONED_URL); |   const url = new URL("/api/v1/user/settings", API_HOSTNAME); | ||||||
|  |  | ||||||
|   return fetch(url.href) |   return fetch(url.href) | ||||||
|     .then(resp => resp.json()) |     .then(resp => resp.json()) | ||||||
|     .catch(error => { |     .catch(error => { | ||||||
|       console.log("api error getting user settings"); |       console.log("api error getting user settings"); // eslint-disable-line no-console | ||||||
|       throw error; |       throw error; | ||||||
|     }); |     }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const updateSettings = settings => { | const updateSettings = settings => { | ||||||
|   const url = new URL("/api/v1/user/settings", SEASONED_URL); |   const url = new URL("/api/v1/user/settings", API_HOSTNAME); | ||||||
|  |  | ||||||
|   const options = { |   const options = { | ||||||
|     method: "PUT", |     method: "PUT", | ||||||
| @@ -413,7 +388,7 @@ const updateSettings = settings => { | |||||||
|   return fetch(url.href, options) |   return fetch(url.href, options) | ||||||
|     .then(resp => resp.json()) |     .then(resp => resp.json()) | ||||||
|     .catch(error => { |     .catch(error => { | ||||||
|       console.log("api error updating user settings"); |       console.log("api error updating user settings"); // eslint-disable-line no-console | ||||||
|       throw error; |       throw error; | ||||||
|     }); |     }); | ||||||
| }; | }; | ||||||
| @@ -421,7 +396,7 @@ const updateSettings = settings => { | |||||||
| // - - - Authenticate with plex - - - | // - - - Authenticate with plex - - - | ||||||
|  |  | ||||||
| const linkPlexAccount = (username, password) => { | const linkPlexAccount = (username, password) => { | ||||||
|   const url = new URL("/api/v1/user/link_plex", SEASONED_URL); |   const url = new URL("/api/v1/user/link_plex", API_HOSTNAME); | ||||||
|   const body = { username, password }; |   const body = { username, password }; | ||||||
|  |  | ||||||
|   const options = { |   const options = { | ||||||
| @@ -433,13 +408,13 @@ const linkPlexAccount = (username, password) => { | |||||||
|   return fetch(url.href, options) |   return fetch(url.href, options) | ||||||
|     .then(resp => resp.json()) |     .then(resp => resp.json()) | ||||||
|     .catch(error => { |     .catch(error => { | ||||||
|       console.error(`api error linking plex account: ${username}`); |       console.error(`api error linking plex account: ${username}`); // eslint-disable-line no-console | ||||||
|       throw error; |       throw error; | ||||||
|     }); |     }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const unlinkPlexAccount = (username, password) => { | const unlinkPlexAccount = () => { | ||||||
|   const url = new URL("/api/v1/user/unlink_plex", SEASONED_URL); |   const url = new URL("/api/v1/user/unlink_plex", API_HOSTNAME); | ||||||
|  |  | ||||||
|   const options = { |   const options = { | ||||||
|     method: "POST", |     method: "POST", | ||||||
| @@ -449,21 +424,21 @@ const unlinkPlexAccount = (username, password) => { | |||||||
|   return fetch(url.href, options) |   return fetch(url.href, options) | ||||||
|     .then(resp => resp.json()) |     .then(resp => resp.json()) | ||||||
|     .catch(error => { |     .catch(error => { | ||||||
|       console.error(`api error unlinking plex account: ${username}`); |       console.error(`api error unlinking your plex account`); // eslint-disable-line no-console | ||||||
|       throw error; |       throw error; | ||||||
|     }); |     }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| // - - - User graphs - - - | // - - - User graphs - - - | ||||||
|  |  | ||||||
| const fetchChart = (urlPath, days, chartType) => { | const fetchGraphData = (urlPath, days, chartType) => { | ||||||
|   const url = new URL("/api/v1/user" + urlPath, SEASONED_URL); |   const url = new URL(`/api/v1/user/${urlPath}`, API_HOSTNAME); | ||||||
|   url.searchParams.append("days", days); |   url.searchParams.append("days", days); | ||||||
|   url.searchParams.append("y_axis", chartType); |   url.searchParams.append("y_axis", chartType); | ||||||
|  |  | ||||||
|   return fetch(url.href).then(resp => { |   return fetch(url.href).then(resp => { | ||||||
|     if (!resp.ok) { |     if (!resp.ok) { | ||||||
|       console.log("DAMN WE FAILED!", resp); |       console.log("DAMN WE FAILED!", resp); // eslint-disable-line no-console | ||||||
|       throw Error(resp.statusText); |       throw Error(resp.statusText); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -474,12 +449,12 @@ const fetchChart = (urlPath, days, chartType) => { | |||||||
| // - - - Random emoji - - - | // - - - Random emoji - - - | ||||||
|  |  | ||||||
| const getEmoji = () => { | const getEmoji = () => { | ||||||
|   const url = new URL("/api/v1/emoji", SEASONED_URL); |   const url = new URL("/api/v1/emoji", API_HOSTNAME); | ||||||
|  |  | ||||||
|   return fetch(url.href) |   return fetch(url.href) | ||||||
|     .then(resp => resp.json()) |     .then(resp => resp.json()) | ||||||
|     .catch(error => { |     .catch(error => { | ||||||
|       console.log("api error getting emoji"); |       console.log("api error getting emoji"); // eslint-disable-line no-console | ||||||
|       throw error; |       throw error; | ||||||
|     }); |     }); | ||||||
| }; | }; | ||||||
| @@ -495,7 +470,7 @@ const getEmoji = () => { | |||||||
|  * @returns {object} List of movies and shows matching query |  * @returns {object} List of movies and shows matching query | ||||||
|  */ |  */ | ||||||
| const elasticSearchMoviesAndShows = (query, count = 22) => { | const elasticSearchMoviesAndShows = (query, count = 22) => { | ||||||
|   const url = new URL(`${ELASTIC_INDEX}/_search`, ELASTIC_URL); |   const url = new URL(`${ELASTIC_INDEX}/_search`, ELASTIC); | ||||||
|  |  | ||||||
|   const body = { |   const body = { | ||||||
|     sort: [{ popularity: { order: "desc" } }, "_score"], |     sort: [{ popularity: { order: "desc" } }, "_score"], | ||||||
| @@ -527,7 +502,7 @@ const elasticSearchMoviesAndShows = (query, count = 22) => { | |||||||
|   return fetch(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}`); |       console.log(`api error searching elasticsearch: ${query}`); // eslint-disable-line no-console | ||||||
|       throw error; |       throw error; | ||||||
|     }); |     }); | ||||||
| }; | }; | ||||||
| @@ -539,7 +514,6 @@ export { | |||||||
|   getMovieCredits, |   getMovieCredits, | ||||||
|   getShowCredits, |   getShowCredits, | ||||||
|   getPersonCredits, |   getPersonCredits, | ||||||
|   getCredits, |  | ||||||
|   getTmdbMovieListByName, |   getTmdbMovieListByName, | ||||||
|   searchTmdb, |   searchTmdb, | ||||||
|   getUserRequests, |   getUserRequests, | ||||||
| @@ -557,7 +531,7 @@ export { | |||||||
|   logout, |   logout, | ||||||
|   getSettings, |   getSettings, | ||||||
|   updateSettings, |   updateSettings, | ||||||
|   fetchChart, |   fetchGraphData, | ||||||
|   getEmoji, |   getEmoji, | ||||||
|   elasticSearchMoviesAndShows |   elasticSearchMoviesAndShows | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,44 +1,51 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="cast"> |   <div class="cast"> | ||||||
|     <ol class="persons"> |     <ol class="persons"> | ||||||
|       <CastListItem v-for="person in cast" :person="person" :key="person.id" /> |       <CastListItem | ||||||
|  |         v-for="credit in cast" | ||||||
|  |         :key="credit.id" | ||||||
|  |         :credit-item="credit" | ||||||
|  |       /> | ||||||
|     </ol> |     </ol> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| import CastListItem from "src/components/CastListItem"; |   import { defineProps } from "vue"; | ||||||
|  |   import CastListItem from "src/components/CastListItem.vue"; | ||||||
|  |   import type { | ||||||
|  |     IMovie, | ||||||
|  |     IShow, | ||||||
|  |     IPerson, | ||||||
|  |     ICast, | ||||||
|  |     ICrew | ||||||
|  |   } from "../interfaces/IList"; | ||||||
|  |  | ||||||
| export default { |   interface Props { | ||||||
|   name: "CastList", |     cast: Array<IMovie | IShow | IPerson | ICast | ICrew>; | ||||||
|   components: { CastListItem }, |  | ||||||
|   props: { |  | ||||||
|     cast: { |  | ||||||
|       type: Array, |  | ||||||
|       required: true |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| }; |  | ||||||
|  |   defineProps<Props>(); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss"> | <style lang="scss"> | ||||||
| .cast { |   .cast { | ||||||
|   position: relative; |     position: relative; | ||||||
|   top: 0; |     top: 0; | ||||||
|   left: 0; |     left: 0; | ||||||
|  |  | ||||||
|   ol { |     ol { | ||||||
|     overflow-x: scroll; |       overflow-x: scroll; | ||||||
|     padding: 0; |       padding: 0; | ||||||
|     list-style-type: none; |       list-style-type: none; | ||||||
|     margin: 0; |       margin: 0; | ||||||
|     display: flex; |       display: flex; | ||||||
|  |  | ||||||
|     scrollbar-width: none; /* for Firefox */ |       scrollbar-width: none; /* for Firefox */ | ||||||
|  |  | ||||||
|     &::-webkit-scrollbar { |       &::-webkit-scrollbar { | ||||||
|       display: none; /* for Chrome, Safari, and Opera */ |         display: none; /* for Chrome, Safari, and Opera */ | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,121 +1,117 @@ | |||||||
| <template> | <template> | ||||||
|   <li class="card"> |   <li class="card"> | ||||||
|     <a @click="openCastItem"> |     <a @click="openCastItem" @keydown.enter="openCastItem"> | ||||||
|       <img class="persons--image" :src="pictureUrl" /> |       <img :src="pictureUrl" alt="Movie or person poster image" /> | ||||||
|       <p class="name">{{ person.name || person.title }}</p> |       <p class="name">{{ creditItem.name || creditItem.title }}</p> | ||||||
|       <p class="meta">{{ person.character || person.year }}</p> |       <p class="meta">{{ creditItem.character || creditItem.year }}</p> | ||||||
|     </a> |     </a> | ||||||
|   </li> |   </li> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| import { mapActions } from "vuex"; |   import { defineProps, computed } from "vue"; | ||||||
|  |   import { useStore } from "vuex"; | ||||||
|  |   import type { ICast, ICrew, IMovie, IShow } from "../interfaces/IList"; | ||||||
|  |  | ||||||
| export default { |   interface Props { | ||||||
|   name: "CastListItem", |     creditItem: ICast | ICrew | IMovie | IShow; | ||||||
|   props: { |   } | ||||||
|     person: { |  | ||||||
|       type: Object, |   const props = defineProps<Props>(); | ||||||
|       required: true |   const store = useStore(); | ||||||
|     } |  | ||||||
|   }, |   const pictureUrl = computed(() => { | ||||||
|   methods: { |     const baseUrl = "https://image.tmdb.org/t/p/w185"; | ||||||
|     ...mapActions("popup", ["open"]), |  | ||||||
|     openCastItem() { |     if ("profile_path" in props.creditItem && props.creditItem.profile_path) { | ||||||
|       let { id, type } = this.person; |       return baseUrl + props.creditItem.profile_path; | ||||||
|  |     } | ||||||
|       if (type) { |     if ("poster" in props.creditItem && props.creditItem.poster) { | ||||||
|         this.open({ id, type }); |       return baseUrl + props.creditItem.poster; | ||||||
|       } |     } | ||||||
|     } |  | ||||||
|   }, |     return "/assets/no-image_small.svg"; | ||||||
|   computed: { |   }); | ||||||
|     pictureUrl() { |  | ||||||
|       const { profile_path, poster_path, poster } = this.person; |   function openCastItem() { | ||||||
|       if (profile_path) return "https://image.tmdb.org/t/p/w185" + profile_path; |     store.dispatch("popup/open", { ...props.creditItem }); | ||||||
|       else if (poster_path) |  | ||||||
|         return "https://image.tmdb.org/t/p/w185" + poster_path; |  | ||||||
|       else if (poster) return "https://image.tmdb.org/t/p/w185" + poster; |  | ||||||
|  |  | ||||||
|       return "/assets/no-image_small.svg"; |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| }; |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss"> | <style lang="scss"> | ||||||
| li a p:first-of-type { |   li a p:first-of-type { | ||||||
|   padding-top: 10px; |     padding-top: 10px; | ||||||
| } |  | ||||||
|  |  | ||||||
| li.card p { |  | ||||||
|   font-size: 1em; |  | ||||||
|   padding: 0 10px; |  | ||||||
|   margin: 0; |  | ||||||
|   overflow: hidden; |  | ||||||
|   text-overflow: ellipsis; |  | ||||||
|   max-height: calc(10px + ((16px * var(--line-height)) * 3)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| li.card { |  | ||||||
|   margin: 10px; |  | ||||||
|   margin-right: 4px; |  | ||||||
|   padding-bottom: 10px; |  | ||||||
|   border-radius: 8px; |  | ||||||
|   overflow: hidden; |  | ||||||
|  |  | ||||||
|   min-width: 140px; |  | ||||||
|   width: 140px; |  | ||||||
|   background-color: var(--background-color-secondary); |  | ||||||
|   color: var(--text-color); |  | ||||||
|  |  | ||||||
|   transition: all 0.3s ease; |  | ||||||
|   transform: scale(0.97) translateZ(0); |  | ||||||
|  |  | ||||||
|   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |  | ||||||
|  |  | ||||||
|   &:first-of-type { |  | ||||||
|     margin-left: 0; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &:hover { |   li.card p { | ||||||
|     box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); |     font-size: 1em; | ||||||
|     transform: scale(1.03); |     padding: 0 10px; | ||||||
|   } |     margin: 0; | ||||||
|  |  | ||||||
|   .name { |  | ||||||
|     font-weight: 500; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .character { |  | ||||||
|     font-size: 0.9em; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .meta { |  | ||||||
|     font-size: 0.9em; |  | ||||||
|     color: var(--text-color-70); |  | ||||||
|     display: -webkit-box; |  | ||||||
|     overflow: hidden; |     overflow: hidden; | ||||||
|     -webkit-line-clamp: 1; |     text-overflow: ellipsis; | ||||||
|     -webkit-box-orient: vertical; |     max-height: calc(10px + ((16px * var(--line-height)) * 3)); | ||||||
|     // margin-top: auto; |  | ||||||
|     max-height: calc((0.9em * var(--line-height)) * 1); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   a { |   li.card { | ||||||
|     display: block; |     margin: 10px; | ||||||
|     text-decoration: none; |     margin-right: 4px; | ||||||
|     height: 100%; |     padding-bottom: 10px; | ||||||
|     display: flex; |     border-radius: 8px; | ||||||
|     flex-direction: column; |     overflow: hidden; | ||||||
|   } |     cursor: pointer; | ||||||
|  |  | ||||||
|   img { |     min-width: 140px; | ||||||
|     width: 100%; |     width: 140px; | ||||||
|     height: auto; |     background-color: var(--background-color-secondary); | ||||||
|     max-height: 210px; |     color: var(--text-color); | ||||||
|     background-color: var(--background-color); |  | ||||||
|     object-fit: cover; |     transition: all 0.3s ease; | ||||||
|  |     transform: scale(0.97) translateZ(0); | ||||||
|  |  | ||||||
|  |     box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||||||
|  |  | ||||||
|  |     &:first-of-type { | ||||||
|  |       margin-left: 0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &:hover { | ||||||
|  |       box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); | ||||||
|  |       transform: scale(1.03); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .name { | ||||||
|  |       font-weight: 500; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .character { | ||||||
|  |       font-size: 0.9em; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .meta { | ||||||
|  |       font-size: 0.9em; | ||||||
|  |       color: var(--text-color-70); | ||||||
|  |       display: -webkit-box; | ||||||
|  |       overflow: hidden; | ||||||
|  |       -webkit-line-clamp: 1; | ||||||
|  |       -webkit-box-orient: vertical; | ||||||
|  |       // margin-top: auto; | ||||||
|  |       max-height: calc((0.9em * var(--line-height)) * 1); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     a { | ||||||
|  |       display: block; | ||||||
|  |       text-decoration: none; | ||||||
|  |       height: 100%; | ||||||
|  |       display: flex; | ||||||
|  |       flex-direction: column; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     img { | ||||||
|  |       width: 100%; | ||||||
|  |       height: auto; | ||||||
|  |       max-height: 210px; | ||||||
|  |       background-color: var(--background-color); | ||||||
|  |       object-fit: cover; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
							
								
								
									
										170
									
								
								src/components/Graph.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,170 @@ | |||||||
|  | <template> | ||||||
|  |   <canvas ref="graphCanvas"></canvas> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { ref, defineProps, onMounted, watch } from "vue"; | ||||||
|  |   import { | ||||||
|  |     Chart, | ||||||
|  |     LineElement, | ||||||
|  |     BarElement, | ||||||
|  |     PointElement, | ||||||
|  |     LineController, | ||||||
|  |     BarController, | ||||||
|  |     LinearScale, | ||||||
|  |     CategoryScale, | ||||||
|  |     Legend, | ||||||
|  |     Title, | ||||||
|  |     Tooltip, | ||||||
|  |     ChartType | ||||||
|  |   } from "chart.js"; | ||||||
|  |  | ||||||
|  |   import type { Ref } from "vue"; | ||||||
|  |   import { convertSecondsToHumanReadable } from "../utils"; | ||||||
|  |   import { GraphValueTypes } from "../interfaces/IGraph"; | ||||||
|  |   import type { IGraphDataset, IGraphData } from "../interfaces/IGraph"; | ||||||
|  |  | ||||||
|  |   Chart.register( | ||||||
|  |     LineElement, | ||||||
|  |     BarElement, | ||||||
|  |     PointElement, | ||||||
|  |     LineController, | ||||||
|  |     BarController, | ||||||
|  |     LinearScale, | ||||||
|  |     CategoryScale, | ||||||
|  |     Legend, | ||||||
|  |     Title, | ||||||
|  |     Tooltip | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   interface Props { | ||||||
|  |     name?: string; | ||||||
|  |     data: IGraphData; | ||||||
|  |     type: ChartType; | ||||||
|  |     stacked: boolean; | ||||||
|  |  | ||||||
|  |     datasetDescriptionSuffix: string; | ||||||
|  |     tooltipDescriptionSuffix: string; | ||||||
|  |     graphValueType?: GraphValueTypes; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Chart.defaults.elements.point.radius = 0; | ||||||
|  |   Chart.defaults.elements.point.hitRadius = 10; | ||||||
|  |   // Chart.defaults.elements.point.pointHoverRadius = 10; | ||||||
|  |   Chart.defaults.elements.point.hoverBorderWidth = 4; | ||||||
|  |  | ||||||
|  |   const props = defineProps<Props>(); | ||||||
|  |   const graphCanvas: Ref<HTMLCanvasElement> = ref(null); | ||||||
|  |   let graphInstance = null; | ||||||
|  |  | ||||||
|  |   /* eslint-disable no-use-before-define */ | ||||||
|  |   onMounted(() => generateGraph()); | ||||||
|  |   watch(() => props.data, generateGraph); | ||||||
|  |   /* eslint-enable no-use-before-define */ | ||||||
|  |  | ||||||
|  |   const graphTemplates = [ | ||||||
|  |     { | ||||||
|  |       backgroundColor: "rgba(54, 162, 235, 0.2)", | ||||||
|  |       borderColor: "rgba(54, 162, 235, 1)", | ||||||
|  |       borderWidth: 1, | ||||||
|  |       tension: 0.4 | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       backgroundColor: "rgba(255, 159, 64, 0.2)", | ||||||
|  |       borderColor: "rgba(255, 159, 64, 1)", | ||||||
|  |       borderWidth: 1, | ||||||
|  |       tension: 0.4 | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       backgroundColor: "rgba(255, 99, 132, 0.2)", | ||||||
|  |       borderColor: "rgba(255, 99, 132, 1)", | ||||||
|  |       borderWidth: 1, | ||||||
|  |       tension: 0.4 | ||||||
|  |     } | ||||||
|  |   ]; | ||||||
|  |   // const gridColor = getComputedStyle(document.documentElement).getPropertyValue( | ||||||
|  |   //   "--text-color-5" | ||||||
|  |   // ); | ||||||
|  |  | ||||||
|  |   function hydrateGraphLineOptions(dataset: IGraphDataset, index: number) { | ||||||
|  |     return { | ||||||
|  |       label: `${dataset.name} ${props.datasetDescriptionSuffix}`, | ||||||
|  |       data: dataset.data, | ||||||
|  |       ...graphTemplates[index] | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function removeEmptyDataset(dataset: IGraphDataset) { | ||||||
|  |     /* eslint-disable-next-line no-unneeded-ternary */ | ||||||
|  |     return dataset.data.every(point => point === 0) ? false : true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function generateGraph() { | ||||||
|  |     const datasets = props.data.series | ||||||
|  |       .filter(removeEmptyDataset) | ||||||
|  |       .map(hydrateGraphLineOptions); | ||||||
|  |  | ||||||
|  |     const graphOptions = { | ||||||
|  |       maintainAspectRatio: false, | ||||||
|  |       plugins: { | ||||||
|  |         tooltip: { | ||||||
|  |           callbacks: { | ||||||
|  |             // title: (tooltipItem, data) => `Watch date: ${tooltipItem[0].label}`, | ||||||
|  |             label: tooltipItem => { | ||||||
|  |               const context = tooltipItem.dataset.label.split(" ")[0]; | ||||||
|  |               const text = `${context} ${props.tooltipDescriptionSuffix}`; | ||||||
|  |  | ||||||
|  |               let value = tooltipItem.raw; | ||||||
|  |               if (props.graphValueType === GraphValueTypes.Time) { | ||||||
|  |                 value = convertSecondsToHumanReadable(value); | ||||||
|  |               } | ||||||
|  |  | ||||||
|  |               return ` ${text}: ${value}`; | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       scales: { | ||||||
|  |         xAxes: { | ||||||
|  |           stacked: props.stacked, | ||||||
|  |           gridLines: { | ||||||
|  |             display: false | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         yAxes: { | ||||||
|  |           stacked: props.stacked, | ||||||
|  |           ticks: { | ||||||
|  |             callback: value => { | ||||||
|  |               if (props.graphValueType === GraphValueTypes.Time) { | ||||||
|  |                 return convertSecondsToHumanReadable(value); | ||||||
|  |               } | ||||||
|  |  | ||||||
|  |               return value; | ||||||
|  |             }, | ||||||
|  |             beginAtZero: true | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const chartData = { | ||||||
|  |       labels: props.data.labels.toString().split(","), | ||||||
|  |       datasets | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     if (graphInstance) { | ||||||
|  |       graphInstance.clear(); | ||||||
|  |       graphInstance.data = chartData; | ||||||
|  |       graphInstance.update("none"); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     graphInstance = new Chart(graphCanvas.value, { | ||||||
|  |       type: props.type, | ||||||
|  |       data: chartData, | ||||||
|  |       options: graphOptions | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped></style> | ||||||
| @@ -1,8 +1,6 @@ | |||||||
| <template> | <template> | ||||||
|   <header |   <header ref="headerElement" :class="{ expanded, noselect: true }"> | ||||||
|     :class="{ expanded, noselect: true }" |     <img ref="imageElement" :src="bannerImage" alt="Page banner image" /> | ||||||
|     v-bind:style="{ 'background-image': 'url(' + imageFile + ')' }" |  | ||||||
|   > |  | ||||||
|     <div class="container"> |     <div class="container"> | ||||||
|       <h1 class="title">Request movies or tv shows</h1> |       <h1 class="title">Request movies or tv shows</h1> | ||||||
|       <strong class="subtitle" |       <strong class="subtitle" | ||||||
| @@ -10,150 +8,211 @@ | |||||||
|       > |       > | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div class="expand-icon" @click="expanded = !expanded"> |     <div | ||||||
|  |       class="expand-icon" | ||||||
|  |       @click="expand" | ||||||
|  |       @keydown.enter="expand" | ||||||
|  |       @mouseover="upgradeImage" | ||||||
|  |       @focus="focus" | ||||||
|  |     > | ||||||
|       <IconExpand v-if="!expanded" /> |       <IconExpand v-if="!expanded" /> | ||||||
|       <IconShrink v-else /> |       <IconShrink v-else /> | ||||||
|     </div> |     </div> | ||||||
|   </header> |   </header> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| import IconExpand from "../icons/IconExpand.vue"; |   import { ref } from "vue"; | ||||||
| import IconShrink from "../icons/IconShrink.vue"; |   import IconExpand from "@/icons/IconExpand.vue"; | ||||||
|  |   import IconShrink from "@/icons/IconShrink.vue"; | ||||||
|  |   import type { Ref } from "vue"; | ||||||
|  |  | ||||||
| export default { |   const ASSET_URL = "https://request.movie/assets/"; | ||||||
|   components: { IconExpand, IconShrink }, |   const images: Array<string> = [ | ||||||
|   props: { |     "pulp-fiction.jpg", | ||||||
|     image: { |     "arrival.jpg", | ||||||
|       type: String, |     "disaster-artist.jpg", | ||||||
|       required: false |     "dune.jpg", | ||||||
|     } |     "mandalorian.jpg" | ||||||
|   }, |   ]; | ||||||
|   data() { |  | ||||||
|     return { |   const bannerImage: Ref<string> = ref(); | ||||||
|       images: [ |   const expanded: Ref<boolean> = ref(false); | ||||||
|         "pulp-fiction.jpg", |   const headerElement: Ref<HTMLElement> = ref(null); | ||||||
|         "arrival.jpg", |   const imageElement: Ref<HTMLImageElement> = ref(null); | ||||||
|         "dune.jpg", |   const defaultHeaderHeight: Ref<string> = ref(); | ||||||
|         "mandalorian.jpg" |   // const disableProxy = true; | ||||||
|       ], |  | ||||||
|       imageFile: undefined, |   function expand() { | ||||||
|       expanded: false |     expanded.value = !expanded.value; | ||||||
|     }; |     let height = defaultHeaderHeight?.value; | ||||||
|   }, |  | ||||||
|   beforeMount() { |     if (expanded.value) { | ||||||
|     if (this.image && this.image.length > 0) { |       const aspectRation = | ||||||
|       this.imageFile = this.image; |         imageElement.value.naturalHeight / imageElement.value.naturalWidth; | ||||||
|     } else { |       height = `${imageElement.value.clientWidth * aspectRation}px`; | ||||||
|       this.imageFile = `/assets/${ |       defaultHeaderHeight.value = headerElement.value.style.height; | ||||||
|         this.images[Math.floor(Math.random() * this.images.length)] |  | ||||||
|       }`; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     headerElement.value.style.setProperty("--header-height", height); | ||||||
|   } |   } | ||||||
| }; |  | ||||||
|  |   function focus(event: FocusEvent) { | ||||||
|  |     event.preventDefault(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function randomImage(): string { | ||||||
|  |     const image = images[Math.floor(Math.random() * images.length)]; | ||||||
|  |     return ASSET_URL + image; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   bannerImage.value = randomImage(); | ||||||
|  |  | ||||||
|  |   // function sliceToHeaderSize(url: string): string { | ||||||
|  |   //   let width = headerElement.value?.getBoundingClientRect()?.width || 1349; | ||||||
|  |   //   let height = headerElement.value?.getBoundingClientRect()?.height || 261; | ||||||
|  |  | ||||||
|  |   //   if (disableProxy) return url; | ||||||
|  |  | ||||||
|  |   //   return buildProxyURL(width, height, url); | ||||||
|  |   // } | ||||||
|  |  | ||||||
|  |   // function upgradeImage() { | ||||||
|  |   //   if (disableProxy || imageUpgraded.value == true) return; | ||||||
|  |  | ||||||
|  |   //   const headerSize = 90; | ||||||
|  |   //   const height = window.innerHeight - headerSize; | ||||||
|  |   //   const width = window.innerWidth - headerSize; | ||||||
|  |  | ||||||
|  |   //   const proxyHost = `http://imgproxy.schleppe:8080/insecure/`; | ||||||
|  |   //   const proxySizeOptions = `q:65/plain/`; | ||||||
|  |  | ||||||
|  |   //   bannerImage.value = `${proxyHost}${proxySizeOptions}${ | ||||||
|  |   //     ASSET_URL + image.value | ||||||
|  |   //   }`; | ||||||
|  |   // } | ||||||
|  |  | ||||||
|  |   // function buildProxyURL(width: number, height: number, asset: string): string { | ||||||
|  |   //   const proxyHost = `http://imgproxy.schleppe:8080/insecure/`; | ||||||
|  |   //   const proxySizeOptions = `resize:fill:${width}:${height}:ce/q:65/plain/`; | ||||||
|  |   //   return `${proxyHost}${proxySizeOptions}${asset}`; | ||||||
|  |   // } | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "src/scss/variables"; |   @import "src/scss/variables"; | ||||||
| @import "src/scss/media-queries"; |   @import "src/scss/media-queries"; | ||||||
|  |  | ||||||
| header { |   header { | ||||||
|   width: 100%; |     width: 100%; | ||||||
|   height: 25vh; |     display: flex; | ||||||
|   display: flex; |     align-items: center; | ||||||
|   align-items: center; |     justify-content: center; | ||||||
|   justify-content: center; |     position: relative; | ||||||
|   background-size: cover; |     transition: height 0.5s ease; | ||||||
|   background-repeat: no-repeat; |     overflow: hidden; | ||||||
|   background-position: 50% 50%; |     --header-height: 25vh; | ||||||
|   position: relative; |  | ||||||
|  |  | ||||||
|   &.expanded { |     height: var(--header-height); | ||||||
|     height: calc(100vh - var(--header-size)); |  | ||||||
|     width: calc(100vw - var(--header-size)); |  | ||||||
|  |  | ||||||
|     @include mobile { |     > * { | ||||||
|       width: 100vw; |       z-index: 1; | ||||||
|       height: 100vh; |     } | ||||||
|  |  | ||||||
|  |     img { | ||||||
|  |       position: absolute; | ||||||
|  |       z-index: 0; | ||||||
|  |       object-fit: cover; | ||||||
|  |       width: 100%; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &.expanded { | ||||||
|  |       // height: calc(100vh - var(--header-size)); | ||||||
|  |       // width: calc(100vw - var(--header-size)); | ||||||
|  |  | ||||||
|  |       // @include mobile { | ||||||
|  |       //   width: 100vw; | ||||||
|  |       //   height: 100vh; | ||||||
|  |       // } | ||||||
|  |  | ||||||
|  |       &:before { | ||||||
|  |         background-color: transparent; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .title, | ||||||
|  |       .subtitle { | ||||||
|  |         opacity: 0; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .expand-icon { | ||||||
|  |       visibility: hidden; | ||||||
|  |       opacity: 0; | ||||||
|  |       transition: all 0.5s ease-in-out; | ||||||
|  |       height: 1.8rem; | ||||||
|  |       width: 1.8rem; | ||||||
|  |       fill: var(--text-color-50); | ||||||
|  |  | ||||||
|  |       position: absolute; | ||||||
|  |       top: 0.5rem; | ||||||
|  |       right: 1rem; | ||||||
|  |  | ||||||
|  |       &:hover { | ||||||
|  |         cursor: pointer; | ||||||
|  |         fill: var(--text-color-90); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &:hover { | ||||||
|  |       .expand-icon { | ||||||
|  |         visibility: visible; | ||||||
|  |         opacity: 1; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     &:before { |     &:before { | ||||||
|       background-color: transparent; |       content: ""; | ||||||
|  |       z-index: 1; | ||||||
|  |       position: absolute; | ||||||
|  |       top: 0; | ||||||
|  |       left: 0; | ||||||
|  |       width: 100%; | ||||||
|  |       height: 100%; | ||||||
|  |       background-color: var(--background-70); | ||||||
|  |       transition: inherit; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     .title, |     .container { | ||||||
|     .subtitle { |       text-align: center; | ||||||
|       opacity: 0; |       position: relative; | ||||||
|  |       transition: color 0.5s ease; | ||||||
|     } |     } | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .expand-icon { |     .title { | ||||||
|     visibility: hidden; |       font-weight: 500; | ||||||
|     opacity: 0; |       font-size: 22px; | ||||||
|     transition: all 0.5s ease-in-out; |       text-transform: uppercase; | ||||||
|     height: 1.8rem; |       letter-spacing: 0.5px; | ||||||
|     width: 1.8rem; |       color: $text-color; | ||||||
|     fill: var(--text-color-50); |       margin: 0; | ||||||
|  |  | ||||||
|     position: absolute; |  | ||||||
|     top: 0.5rem; |  | ||||||
|     right: 1rem; |  | ||||||
|  |  | ||||||
|     &:hover { |  | ||||||
|       cursor: pointer; |  | ||||||
|       fill: var(--text-color-90); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &:hover { |  | ||||||
|     .expand-icon { |  | ||||||
|       visibility: visible; |  | ||||||
|       opacity: 1; |       opacity: 1; | ||||||
|  |  | ||||||
|  |       @include tablet-min { | ||||||
|  |         font-size: 2.5rem; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .subtitle { | ||||||
|  |       display: block; | ||||||
|  |       font-size: 14px; | ||||||
|  |       font-weight: 300; | ||||||
|  |       color: $text-color-70; | ||||||
|  |       margin: 5px 0; | ||||||
|  |       opacity: 1; | ||||||
|  |  | ||||||
|  |       @include tablet-min { | ||||||
|  |         font-size: 1.3rem; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &:before { |  | ||||||
|     content: ""; |  | ||||||
|     position: absolute; |  | ||||||
|     top: 0; |  | ||||||
|     left: 0; |  | ||||||
|     width: 100%; |  | ||||||
|     height: 100%; |  | ||||||
|     background-color: var(--background-70); |  | ||||||
|     transition: inherit; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .container { |  | ||||||
|     text-align: center; |  | ||||||
|     position: relative; |  | ||||||
|     transition: color 0.5s ease; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .title { |  | ||||||
|     font-weight: 500; |  | ||||||
|     font-size: 22px; |  | ||||||
|     text-transform: uppercase; |  | ||||||
|     letter-spacing: 0.5px; |  | ||||||
|     color: $text-color; |  | ||||||
|     margin: 0; |  | ||||||
|     opacity: 1; |  | ||||||
|  |  | ||||||
|     @include tablet-min { |  | ||||||
|       font-size: 2.5rem; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .subtitle { |  | ||||||
|     display: block; |  | ||||||
|     font-size: 14px; |  | ||||||
|     font-weight: 300; |  | ||||||
|     color: $text-color-70; |  | ||||||
|     margin: 5px 0; |  | ||||||
|     opacity: 1; |  | ||||||
|  |  | ||||||
|     @include tablet-min { |  | ||||||
|       font-size: 1.3rem; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,125 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <header> |  | ||||||
|     <h2>{{ prettify }}</h2> |  | ||||||
|     <h3>{{ subtitle }}</h3> |  | ||||||
|  |  | ||||||
|     <router-link |  | ||||||
|       v-if="shortList" |  | ||||||
|       :to="urlify" |  | ||||||
|       class="view-more" |  | ||||||
|       :aria-label="`View all ${title}`" |  | ||||||
|     > |  | ||||||
|       View All |  | ||||||
|     </router-link> |  | ||||||
|  |  | ||||||
|     <div v-else-if="info"> |  | ||||||
|       <div v-if="info instanceof Array" class="flex flex-direction-column"> |  | ||||||
|         <span v-for="item in info" :key="item" class="info">{{ item }}</span> |  | ||||||
|       </div> |  | ||||||
|  |  | ||||||
|       <span v-else class="info">{{ info }}</span> |  | ||||||
|     </div> |  | ||||||
|   </header> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| export default { |  | ||||||
|   props: { |  | ||||||
|     title: { |  | ||||||
|       type: String, |  | ||||||
|       required: true |  | ||||||
|     }, |  | ||||||
|     subtitle: { |  | ||||||
|       type: String, |  | ||||||
|       required: false, |  | ||||||
|       default: null |  | ||||||
|     }, |  | ||||||
|     info: { |  | ||||||
|       type: [String, Array], |  | ||||||
|       required: false |  | ||||||
|     }, |  | ||||||
|     link: { |  | ||||||
|       type: String, |  | ||||||
|       required: false |  | ||||||
|     }, |  | ||||||
|     shortList: { |  | ||||||
|       type: Boolean, |  | ||||||
|       required: false, |  | ||||||
|       default: false |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   computed: { |  | ||||||
|     urlify: function () { |  | ||||||
|       return `/list/${this.title.toLowerCase().replace(" ", "_")}`; |  | ||||||
|     }, |  | ||||||
|     prettify: function () { |  | ||||||
|       return this.title.includes("_") |  | ||||||
|         ? this.title.split("_").join(" ") |  | ||||||
|         : this.title; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "src/scss/variables"; |  | ||||||
| @import "src/scss/media-queries"; |  | ||||||
| @import "src/scss/main"; |  | ||||||
|  |  | ||||||
| header { |  | ||||||
|   width: 100%; |  | ||||||
|   display: flex; |  | ||||||
|   justify-content: space-between; |  | ||||||
|   align-items: center; |  | ||||||
|   padding: 0.5rem 0.75rem; |  | ||||||
|   background-color: $background-color; |  | ||||||
|  |  | ||||||
|   position: sticky; |  | ||||||
|   position: -webkit-sticky; |  | ||||||
|   top: $header-size; |  | ||||||
|   z-index: 1; |  | ||||||
|  |  | ||||||
|   h2 { |  | ||||||
|     font-size: 1.4rem; |  | ||||||
|     font-weight: 300; |  | ||||||
|     text-transform: capitalize; |  | ||||||
|     line-height: 1.4rem; |  | ||||||
|     margin: 0; |  | ||||||
|     color: $text-color; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .view-more { |  | ||||||
|     font-size: 0.9rem; |  | ||||||
|     font-weight: 300; |  | ||||||
|     letter-spacing: 0.5px; |  | ||||||
|     color: $text-color-70; |  | ||||||
|     text-decoration: none; |  | ||||||
|     transition: color 0.5s ease; |  | ||||||
|     cursor: pointer; |  | ||||||
|  |  | ||||||
|     &:after { |  | ||||||
|       content: " →"; |  | ||||||
|     } |  | ||||||
|     &:hover { |  | ||||||
|       color: $text-color; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .info { |  | ||||||
|     font-size: 13px; |  | ||||||
|     font-weight: 300; |  | ||||||
|     letter-spacing: 0.5px; |  | ||||||
|     color: $text-color; |  | ||||||
|     text-decoration: none; |  | ||||||
|     text-align: right; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @include tablet-min { |  | ||||||
|     padding-left: 1.25rem; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @include desktop-lg-min { |  | ||||||
|     padding-left: 1.75rem; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
							
								
								
									
										110
									
								
								src/components/PageHeader.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,110 @@ | |||||||
|  | <template> | ||||||
|  |   <header> | ||||||
|  |     <h2>{{ prettify }}</h2> | ||||||
|  |     <h3>{{ subtitle }}</h3> | ||||||
|  |  | ||||||
|  |     <router-link | ||||||
|  |       v-if="shortList" | ||||||
|  |       :to="urlify" | ||||||
|  |       class="view-more" | ||||||
|  |       :aria-label="`View all ${title}`" | ||||||
|  |     > | ||||||
|  |       View All | ||||||
|  |     </router-link> | ||||||
|  |  | ||||||
|  |     <div v-else-if="info"> | ||||||
|  |       <div v-if="info instanceof Array" class="flex flex-direction-column"> | ||||||
|  |         <span v-for="item in info" :key="item" class="info">{{ item }}</span> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <span v-else class="info">{{ info }}</span> | ||||||
|  |     </div> | ||||||
|  |   </header> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { defineProps, computed } from "vue"; | ||||||
|  |  | ||||||
|  |   interface Props { | ||||||
|  |     title: string; | ||||||
|  |     subtitle?: string; | ||||||
|  |     info?: string | Array<string>; | ||||||
|  |     link?: string; | ||||||
|  |     shortList?: boolean; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const props = defineProps<Props>(); | ||||||
|  |  | ||||||
|  |   const urlify = computed(() => { | ||||||
|  |     return `/list/${props.title.toLowerCase().replace(" ", "_")}`; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const prettify = computed(() => { | ||||||
|  |     return props.title.includes("_") | ||||||
|  |       ? props.title.split("_").join(" ") | ||||||
|  |       : props.title; | ||||||
|  |   }); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  |   @import "src/scss/variables"; | ||||||
|  |   @import "src/scss/media-queries"; | ||||||
|  |   @import "src/scss/main"; | ||||||
|  |  | ||||||
|  |   header { | ||||||
|  |     width: 100%; | ||||||
|  |     display: flex; | ||||||
|  |     justify-content: space-between; | ||||||
|  |     align-items: center; | ||||||
|  |     padding: 0.5rem 0.75rem; | ||||||
|  |     background-color: $background-color; | ||||||
|  |  | ||||||
|  |     position: sticky; | ||||||
|  |     position: -webkit-sticky; | ||||||
|  |     top: $header-size; | ||||||
|  |     z-index: 1; | ||||||
|  |  | ||||||
|  |     h2 { | ||||||
|  |       font-size: 1.4rem; | ||||||
|  |       font-weight: 300; | ||||||
|  |       text-transform: capitalize; | ||||||
|  |       line-height: 1.4rem; | ||||||
|  |       margin: 0; | ||||||
|  |       color: $text-color; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .view-more { | ||||||
|  |       font-size: 0.9rem; | ||||||
|  |       font-weight: 300; | ||||||
|  |       letter-spacing: 0.5px; | ||||||
|  |       color: $text-color-70; | ||||||
|  |       text-decoration: none; | ||||||
|  |       transition: color 0.5s ease; | ||||||
|  |       cursor: pointer; | ||||||
|  |  | ||||||
|  |       &:after { | ||||||
|  |         content: " →"; | ||||||
|  |       } | ||||||
|  |       &:hover { | ||||||
|  |         color: $text-color; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .info { | ||||||
|  |       font-size: 13px; | ||||||
|  |       font-weight: 300; | ||||||
|  |       letter-spacing: 0.5px; | ||||||
|  |       color: $text-color; | ||||||
|  |       text-decoration: none; | ||||||
|  |       text-align: right; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @include tablet-min { | ||||||
|  |       padding-left: 1.25rem; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @include desktop-lg-min { | ||||||
|  |       padding-left: 1.75rem; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | </style> | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| <template> | <template> | ||||||
|   <div v-if="isOpen" class="movie-popup" @click="close"> |   <div v-if="isOpen" class="movie-popup" @click="close" @keydown.enter="close"> | ||||||
|     <div class="movie-popup__box" @click.stop> |     <div class="movie-popup__box" @click.stop> | ||||||
|       <person v-if="type === 'person'" :id="id" type="person" /> |       <person v-if="type === 'person'" :id="id" type="person" /> | ||||||
|       <movie v-else :id="id" :type="type"></movie> |       <movie v-else :id="id" :type="type"></movie> | ||||||
| @@ -9,117 +9,144 @@ | |||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| import { mapActions, mapGetters } from "vuex"; |   import { ref, onMounted, onBeforeUnmount } from "vue"; | ||||||
| import Movie from "@/components/popup/Movie"; |   import { useStore } from "vuex"; | ||||||
| import Person from "@/components/popup/Person"; |   import Movie from "@/components/popup/Movie.vue"; | ||||||
|  |   import Person from "@/components/popup/Person.vue"; | ||||||
|  |   import type { Ref } from "vue"; | ||||||
|  |   import { MediaTypes } from "../interfaces/IList"; | ||||||
|  |  | ||||||
| export default { |   interface URLQueryParameters { | ||||||
|   components: { Movie, Person }, |     id: number; | ||||||
|   computed: { |     type: MediaTypes; | ||||||
|     ...mapGetters("popup", ["isOpen", "id", "type"]) |  | ||||||
|   }, |  | ||||||
|   watch: { |  | ||||||
|     isOpen(value) { |  | ||||||
|       value |  | ||||||
|         ? document.getElementsByTagName("body")[0].classList.add("no-scroll") |  | ||||||
|         : document |  | ||||||
|             .getElementsByTagName("body")[0] |  | ||||||
|             .classList.remove("no-scroll"); |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     ...mapActions("popup", ["close", "open"]), |  | ||||||
|     checkEventForEscapeKey(event) { |  | ||||||
|       if (event.keyCode == 27) this.close(); |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   created() { |  | ||||||
|     const params = new URLSearchParams(window.location.search); |  | ||||||
|     let id = null; |  | ||||||
|     let type = null; |  | ||||||
|  |  | ||||||
|     if (params.has("movie")) { |  | ||||||
|       id = Number(params.get("movie")); |  | ||||||
|       type = "movie"; |  | ||||||
|     } else if (params.has("show")) { |  | ||||||
|       id = Number(params.get("show")); |  | ||||||
|       type = "show"; |  | ||||||
|     } else if (params.has("person")) { |  | ||||||
|       id = Number(params.get("person")); |  | ||||||
|       type = "person"; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (id && type) { |  | ||||||
|       this.open({ id, type }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     window.addEventListener("keyup", this.checkEventForEscapeKey); |  | ||||||
|   }, |  | ||||||
|   beforeDestroy() { |  | ||||||
|     window.removeEventListener("keyup", this.checkEventForEscapeKey); |  | ||||||
|   } |   } | ||||||
| }; |  | ||||||
|  |   const store = useStore(); | ||||||
|  |   const isOpen: Ref<boolean> = ref(); | ||||||
|  |   const id: Ref<string> = ref(); | ||||||
|  |   const type: Ref<MediaTypes> = ref(); | ||||||
|  |  | ||||||
|  |   const unsubscribe = store.subscribe((mutation, state) => { | ||||||
|  |     if (!mutation.type.includes("popup")) return; | ||||||
|  |  | ||||||
|  |     isOpen.value = state.popup.open; | ||||||
|  |     id.value = state.popup.id; | ||||||
|  |     type.value = state.popup.type; | ||||||
|  |  | ||||||
|  |     if (isOpen.value) { | ||||||
|  |       document.getElementsByTagName("body")[0].classList.add("no-scroll"); | ||||||
|  |     } else { | ||||||
|  |       document.getElementsByTagName("body")[0].classList.remove("no-scroll"); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   function getFromURLQuery(): URLQueryParameters { | ||||||
|  |     let _id: number; | ||||||
|  |     let _type: MediaTypes; | ||||||
|  |  | ||||||
|  |     const params = new URLSearchParams(window.location.search); | ||||||
|  |     params.forEach((value, key) => { | ||||||
|  |       if ( | ||||||
|  |         key !== MediaTypes.Movie && | ||||||
|  |         key !== MediaTypes.Show && | ||||||
|  |         key !== MediaTypes.Person | ||||||
|  |       ) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       _id = Number(params.get(key)); | ||||||
|  |       _type = key; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return { id: _id, type: _type }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function open(_id: number, _type: string) { | ||||||
|  |     if (!_id || !_type) return; | ||||||
|  |     store.dispatch("popup/open", { id: _id, type: _type }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function close() { | ||||||
|  |     store.dispatch("popup/close"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function checkEventForEscapeKey(event: KeyboardEvent) { | ||||||
|  |     if (event.keyCode !== 27) return; | ||||||
|  |     close(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   window.addEventListener("keyup", checkEventForEscapeKey); | ||||||
|  |  | ||||||
|  |   onMounted(() => { | ||||||
|  |     const query = getFromURLQuery(); | ||||||
|  |     open(query?.id, query?.type); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   onBeforeUnmount(() => { | ||||||
|  |     unsubscribe(); | ||||||
|  |     window.removeEventListener("keyup", checkEventForEscapeKey); | ||||||
|  |   }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss"> | <style lang="scss"> | ||||||
| @import "src/scss/variables"; |   @import "src/scss/variables"; | ||||||
| @import "src/scss/media-queries"; |   @import "src/scss/media-queries"; | ||||||
|  |  | ||||||
| .movie-popup { |   .movie-popup { | ||||||
|   position: fixed; |     position: fixed; | ||||||
|   top: 0; |  | ||||||
|   left: 0; |  | ||||||
|   z-index: 20; |  | ||||||
|   width: 100%; |  | ||||||
|   height: 100%; |  | ||||||
|   background: rgba($dark, 0.93); |  | ||||||
|   -webkit-overflow-scrolling: touch; |  | ||||||
|   overflow: auto; |  | ||||||
|  |  | ||||||
|   &__box { |  | ||||||
|     max-width: 768px; |  | ||||||
|     position: relative; |  | ||||||
|     z-index: 5; |  | ||||||
|     margin: 8vh auto; |  | ||||||
|  |  | ||||||
|     @include mobile { |  | ||||||
|       margin: 0 0 50px 0; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   &__close { |  | ||||||
|     display: block; |  | ||||||
|     position: absolute; |  | ||||||
|     top: 0; |     top: 0; | ||||||
|     right: 0; |     left: 0; | ||||||
|     border: 0; |     z-index: 20; | ||||||
|     background: transparent; |     width: 100%; | ||||||
|     width: 40px; |     height: 100%; | ||||||
|     height: 40px; |     background: rgba($dark, 0.93); | ||||||
|     transition: background 0.5s ease; |     -webkit-overflow-scrolling: touch; | ||||||
|     cursor: pointer; |     overflow: auto; | ||||||
|     z-index: 5; |  | ||||||
|  |  | ||||||
|     &:before, |     &__box { | ||||||
|     &:after { |       max-width: 768px; | ||||||
|       content: ""; |       position: relative; | ||||||
|  |       z-index: 5; | ||||||
|  |       margin: 8vh auto; | ||||||
|  |  | ||||||
|  |       @include mobile { | ||||||
|  |         margin: 0 0 50px 0; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     &__close { | ||||||
|       display: block; |       display: block; | ||||||
|       position: absolute; |       position: absolute; | ||||||
|       top: 19px; |       top: 0; | ||||||
|       left: 10px; |       right: 0; | ||||||
|       width: 20px; |       border: 0; | ||||||
|       height: 2px; |       background: transparent; | ||||||
|       background: $white; |       width: 40px; | ||||||
|     } |       height: 40px; | ||||||
|     &:before { |       transition: background 0.5s ease; | ||||||
|       transform: rotate(45deg); |       cursor: pointer; | ||||||
|     } |       z-index: 5; | ||||||
|     &:after { |  | ||||||
|       transform: rotate(-45deg); |       &:before, | ||||||
|     } |       &:after { | ||||||
|     &:hover { |         content: ""; | ||||||
|       background: $green; |         display: block; | ||||||
|  |         position: absolute; | ||||||
|  |         top: 19px; | ||||||
|  |         left: 10px; | ||||||
|  |         width: 20px; | ||||||
|  |         height: 2px; | ||||||
|  |         background: $white; | ||||||
|  |       } | ||||||
|  |       &:before { | ||||||
|  |         transform: rotate(45deg); | ||||||
|  |       } | ||||||
|  |       &:after { | ||||||
|  |         transform: rotate(-45deg); | ||||||
|  |       } | ||||||
|  |       &:hover { | ||||||
|  |         background: $green; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -6,9 +6,9 @@ | |||||||
|       :class="{ shortList: shortList }" |       :class="{ shortList: shortList }" | ||||||
|     > |     > | ||||||
|       <results-list-item |       <results-list-item | ||||||
|         v-for="(movie, index) in results" |         v-for="(result, index) in results" | ||||||
|         :key="`${movie.type}-${movie.id}-${index}`" |         :key="generateResultKey(index, `${result.type}-${result.id}`)" | ||||||
|         :movie="movie" |         :list-item="result" | ||||||
|       /> |       /> | ||||||
|     </ul> |     </ul> | ||||||
|  |  | ||||||
| @@ -16,68 +16,62 @@ | |||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| import ResultsListItem from "@/components/ResultsListItem"; |   import { defineProps } from "vue"; | ||||||
|  |   import ResultsListItem from "@/components/ResultsListItem.vue"; | ||||||
|  |   import type { ListResults } from "../interfaces/IList"; | ||||||
|  |  | ||||||
| export default { |   interface Props { | ||||||
|   components: { ResultsListItem }, |     results: Array<ListResults>; | ||||||
|   props: { |     shortList?: boolean; | ||||||
|     results: { |     loading?: boolean; | ||||||
|       type: Array, |   } | ||||||
|       required: true |  | ||||||
|     }, |   defineProps<Props>(); | ||||||
|     shortList: { |  | ||||||
|       type: Boolean, |   function generateResultKey(index: string | number | symbol, value: string) { | ||||||
|       required: false, |     return `${String(index)}-${value}`; | ||||||
|       default: false |  | ||||||
|     }, |  | ||||||
|     loading: { |  | ||||||
|       type: Boolean, |  | ||||||
|       required: false, |  | ||||||
|       default: false |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| }; |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss"> | <style lang="scss" scoped> | ||||||
| @import "src/scss/media-queries"; |   @import "src/scss/media-queries"; | ||||||
| @import "src/scss/main"; |   @import "src/scss/main"; | ||||||
|  |  | ||||||
| .no-results { |   .no-results { | ||||||
|   width: 100%; |     width: 100%; | ||||||
|   display: block; |     display: block; | ||||||
|   text-align: center; |     text-align: center; | ||||||
|   margin: 1.5rem; |     margin: 1.5rem; | ||||||
|   font-size: 1.2rem; |     font-size: 1.2rem; | ||||||
| } |  | ||||||
|  |  | ||||||
| .results { |  | ||||||
|   display: grid; |  | ||||||
|   grid-template-columns: repeat(auto-fill, minmax(225px, 1fr)); |  | ||||||
|   grid-auto-rows: auto; |  | ||||||
|   margin: 0; |  | ||||||
|   padding: 0; |  | ||||||
|   list-style: none; |  | ||||||
|  |  | ||||||
|   @include mobile { |  | ||||||
|     grid-template-columns: repeat(2, 1fr); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &.shortList { |   .results { | ||||||
|     overflow: auto; |     display: grid; | ||||||
|     grid-auto-flow: column; |     grid-template-columns: repeat(auto-fill, minmax(225px, 1fr)); | ||||||
|     max-width: 100vw; |     grid-auto-rows: auto; | ||||||
|  |     margin: 0; | ||||||
|  |     padding: 0; | ||||||
|  |     list-style: none; | ||||||
|  |  | ||||||
|     @include noscrollbar; |     @include mobile { | ||||||
|  |       grid-template-columns: repeat(2, 1fr); | ||||||
|     > li { |  | ||||||
|       min-width: 225px; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @include tablet-min { |     &.shortList { | ||||||
|       max-width: calc(100vw - var(--header-size)); |       overflow: auto; | ||||||
|  |       grid-auto-flow: column; | ||||||
|  |       max-width: 100vw; | ||||||
|  |  | ||||||
|  |       @include noscrollbar; | ||||||
|  |  | ||||||
|  |       > li { | ||||||
|  |         min-width: 225px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @include tablet-min { | ||||||
|  |         max-width: calc(100vw - var(--header-size)); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,6 +1,11 @@ | |||||||
| <template> | <template> | ||||||
|   <li class="movie-item" ref="list-item"> |   <li ref="list-item" class="movie-item"> | ||||||
|     <figure ref="poster" class="movie-item__poster" @click="openMoviePopup"> |     <figure | ||||||
|  |       ref="posterElement" | ||||||
|  |       class="movie-item__poster" | ||||||
|  |       @click="openMoviePopup" | ||||||
|  |       @keydown.enter="openMoviePopup" | ||||||
|  |     > | ||||||
|       <img |       <img | ||||||
|         class="movie-item__img" |         class="movie-item__img" | ||||||
|         :alt="posterAltText" |         :alt="posterAltText" | ||||||
| @@ -8,173 +13,171 @@ | |||||||
|         src="/assets/placeholder.png" |         src="/assets/placeholder.png" | ||||||
|       /> |       /> | ||||||
|  |  | ||||||
|       <div v-if="movie.download" class="progress"> |       <div v-if="listItem.download" class="progress"> | ||||||
|         <progress :value="movie.download.progress" max="100"></progress> |         <progress :value="listItem.download.progress" max="100"></progress> | ||||||
|         <span>{{ movie.download.state }}: {{ movie.download.progress }}%</span> |         <span | ||||||
|  |           >{{ listItem.download.state }}: | ||||||
|  |           {{ listItem.download.progress }}%</span | ||||||
|  |         > | ||||||
|       </div> |       </div> | ||||||
|     </figure> |     </figure> | ||||||
|  |  | ||||||
|     <div class="movie-item__info"> |     <div class="movie-item__info"> | ||||||
|       <p v-if="movie.title || movie.name" class="movie-item__title"> |       <p v-if="listItem.title || listItem.name" class="movie-item__title"> | ||||||
|         {{ movie.title || movie.name }} |         {{ listItem.title || listItem.name }} | ||||||
|       </p> |       </p> | ||||||
|       <p v-if="movie.year">{{ movie.year }}</p> |       <p v-if="listItem.year">{{ listItem.year }}</p> | ||||||
|       <p v-if="movie.type == 'person'"> |       <p v-if="listItem.type == 'person'"> | ||||||
|         Known for: {{ movie.known_for_department }} |         Known for: {{ listItem.known_for_department }} | ||||||
|       </p> |       </p> | ||||||
|     </div> |     </div> | ||||||
|   </li> |   </li> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| import { mapActions } from "vuex"; |   import { ref, computed, defineProps, onMounted } from "vue"; | ||||||
| import img from "../directives/v-image"; |   import { useStore } from "vuex"; | ||||||
| import { buildImageProxyUrl } from "../utils"; |   import type { Ref } from "vue"; | ||||||
|  |   import type { IMovie, IShow, IPerson } from "../interfaces/IList"; | ||||||
|  |  | ||||||
| export default { |   interface Props { | ||||||
|   props: { |     listItem: IMovie | IShow | IPerson; | ||||||
|     movie: { |   } | ||||||
|       type: Object, |  | ||||||
|       required: true |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   directives: { |  | ||||||
|     img: img |  | ||||||
|   }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       poster: null, |  | ||||||
|       observed: false |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|   computed: { |  | ||||||
|     posterAltText: function () { |  | ||||||
|       const type = this.movie.type || ""; |  | ||||||
|       const title = this.movie.title || this.movie.name; |  | ||||||
|       return this.movie.poster |  | ||||||
|         ? `Poster for ${type} ${title}` |  | ||||||
|         : `Missing image for ${type} ${title}`; |  | ||||||
|     }, |  | ||||||
|     imageWidth() { |  | ||||||
|       if (this.image) |  | ||||||
|         return Math.ceil(this.image.getBoundingClientRect().width); |  | ||||||
|     }, |  | ||||||
|     imageHeight() { |  | ||||||
|       if (this.image) |  | ||||||
|         return Math.ceil(this.image.getBoundingClientRect().height); |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   beforeMount() { |  | ||||||
|     if (this.movie.poster == null) { |  | ||||||
|       this.poster = "/assets/no-image.svg"; |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this.poster = `https://image.tmdb.org/t/p/w500${this.movie.poster}`; |   const props = defineProps<Props>(); | ||||||
|     // this.poster = this.buildProxyURL( |   const store = useStore(); | ||||||
|     //   this.imageWidth, |  | ||||||
|     //   this.imageHeight, |  | ||||||
|     //   assetUrl |  | ||||||
|     // ); |  | ||||||
|   }, |  | ||||||
|   mounted() { |  | ||||||
|     const poster = this.$refs["poster"]; |  | ||||||
|     this.image = poster.getElementsByTagName("img")[0]; |  | ||||||
|     if (this.image == null) return; |  | ||||||
|  |  | ||||||
|     const imageObserver = new IntersectionObserver((entries, imgObserver) => { |   const IMAGE_BASE_URL = "https://image.tmdb.org/t/p/w500"; | ||||||
|  |   const IMAGE_FALLBACK = "/assets/no-image.svg"; | ||||||
|  |   const poster: Ref<string> = ref(); | ||||||
|  |   const posterElement: Ref<HTMLElement> = ref(null); | ||||||
|  |   const observed: Ref<boolean> = ref(false); | ||||||
|  |  | ||||||
|  |   if (props.listItem?.poster) { | ||||||
|  |     poster.value = IMAGE_BASE_URL + props.listItem.poster; | ||||||
|  |   } else { | ||||||
|  |     poster.value = IMAGE_FALLBACK; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const posterAltText = computed(() => { | ||||||
|  |     const type = props.listItem.type || ""; | ||||||
|  |     let title = ""; | ||||||
|  |  | ||||||
|  |     if ("name" in props.listItem) title = props.listItem.name; | ||||||
|  |     else if ("title" in props.listItem) title = props.listItem.title; | ||||||
|  |  | ||||||
|  |     return props.listItem.poster | ||||||
|  |       ? `Poster for ${type} ${title}` | ||||||
|  |       : `Missing image for ${type} ${title}`; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   function observePosterAndSetImageSource() { | ||||||
|  |     const imageElement = posterElement.value.getElementsByTagName("img")[0]; | ||||||
|  |     if (imageElement == null) return; | ||||||
|  |  | ||||||
|  |     const imageObserver = new IntersectionObserver(entries => { | ||||||
|       entries.forEach(entry => { |       entries.forEach(entry => { | ||||||
|         if (entry.isIntersecting && this.observed == false) { |         if (entry.isIntersecting && observed.value === false) { | ||||||
|           const lazyImage = entry.target; |           const lazyImage = entry.target as HTMLImageElement; | ||||||
|           lazyImage.src = lazyImage.dataset.src; |           lazyImage.src = lazyImage.dataset.src; | ||||||
|           poster.className = poster.className + " is-loaded"; |           posterElement.value.classList.add("is-loaded"); | ||||||
|           this.observed = true; |           observed.value = true; | ||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     imageObserver.observe(this.image); |     imageObserver.observe(imageElement); | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     ...mapActions("popup", ["open"]), |  | ||||||
|     openMoviePopup() { |  | ||||||
|       this.open({ |  | ||||||
|         id: this.movie.id, |  | ||||||
|         type: this.movie.type |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| }; |  | ||||||
|  |   onMounted(observePosterAndSetImageSource); | ||||||
|  |  | ||||||
|  |   function openMoviePopup() { | ||||||
|  |     store.dispatch("popup/open", { ...props.listItem }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // const imageSize = computed(() => { | ||||||
|  |   //   if (!posterElement.value) return; | ||||||
|  |   //   const { height, width } = posterElement.value.getBoundingClientRect(); | ||||||
|  |   //   return { | ||||||
|  |   //     height: Math.ceil(height), | ||||||
|  |   //     width: Math.ceil(width) | ||||||
|  |   //   }; | ||||||
|  |   // }); | ||||||
|  |  | ||||||
|  |   // import img from "../directives/v-image"; | ||||||
|  |   //   directives: { | ||||||
|  |   //     img: img | ||||||
|  |   //   }, | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "src/scss/variables"; |   @import "src/scss/variables"; | ||||||
| @import "src/scss/media-queries"; |   @import "src/scss/media-queries"; | ||||||
| @import "src/scss/main"; |   @import "src/scss/main"; | ||||||
|  |  | ||||||
| .movie-item { |   .movie-item { | ||||||
|   padding: 15px; |     padding: 15px; | ||||||
|   width: 100%; |     width: 100%; | ||||||
|   background-color: var(--background-color); |     background-color: var(--background-color); | ||||||
|  |  | ||||||
|   &:hover &__info > p { |     &:hover &__info > p { | ||||||
|     color: $text-color; |       color: $text-color; | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &__poster { |  | ||||||
|     text-decoration: none; |  | ||||||
|     color: $text-color-70; |  | ||||||
|     font-weight: 300; |  | ||||||
|     position: relative; |  | ||||||
|     transform: scale(0.97) translateZ(0); |  | ||||||
|  |  | ||||||
|     &::before { |  | ||||||
|       content: ""; |  | ||||||
|       position: absolute; |  | ||||||
|       z-index: 1; |  | ||||||
|       width: 100%; |  | ||||||
|       height: 100%; |  | ||||||
|       background-color: var(--background-color); |  | ||||||
|       transition: 1s background-color ease; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     &:hover { |     &__poster { | ||||||
|       transform: scale(1.03); |       text-decoration: none; | ||||||
|       box-shadow: 0 0 10px rgba($dark, 0.1); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     &.is-loaded::before { |  | ||||||
|       background-color: transparent; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     img { |  | ||||||
|       width: 100%; |  | ||||||
|       border-radius: 10px; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &__info { |  | ||||||
|     padding-top: 10px; |  | ||||||
|     font-weight: 300; |  | ||||||
|  |  | ||||||
|     > p { |  | ||||||
|       color: $text-color-70; |       color: $text-color-70; | ||||||
|       margin: 0; |       font-weight: 300; | ||||||
|       font-size: 14px; |       position: relative; | ||||||
|       letter-spacing: 0.5px; |       transform: scale(0.97) translateZ(0); | ||||||
|       transition: color 0.5s ease; |  | ||||||
|       cursor: pointer; |       &::before { | ||||||
|       @include mobile-ls-min { |         content: ""; | ||||||
|         font-size: 12px; |         position: absolute; | ||||||
|  |         z-index: 1; | ||||||
|  |         width: 100%; | ||||||
|  |         height: 100%; | ||||||
|  |         background-color: var(--background-color); | ||||||
|  |         transition: 1s background-color ease; | ||||||
|       } |       } | ||||||
|       @include tablet-min { |  | ||||||
|         font-size: 14px; |       &:hover { | ||||||
|  |         transform: scale(1.03); | ||||||
|  |         box-shadow: 0 0 10px rgba($dark, 0.1); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       &.is-loaded::before { | ||||||
|  |         background-color: transparent; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       img { | ||||||
|  |         width: 100%; | ||||||
|  |         border-radius: 10px; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &__title { |     &__info { | ||||||
|     font-weight: 400; |       padding-top: 10px; | ||||||
|  |       font-weight: 300; | ||||||
|  |  | ||||||
|  |       > p { | ||||||
|  |         color: $text-color-70; | ||||||
|  |         margin: 0; | ||||||
|  |         font-size: 14px; | ||||||
|  |         letter-spacing: 0.5px; | ||||||
|  |         transition: color 0.5s ease; | ||||||
|  |         cursor: pointer; | ||||||
|  |         @include mobile-ls-min { | ||||||
|  |           font-size: 12px; | ||||||
|  |         } | ||||||
|  |         @include tablet-min { | ||||||
|  |           font-size: 14px; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &__title { | ||||||
|  |       font-weight: 400; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,12 +1,12 @@ | |||||||
| <template> | <template> | ||||||
|   <div ref="resultSection" class="resultSection"> |   <div ref="resultSection" class="resultSection"> | ||||||
|     <list-header v-bind="{ title, info, shortList }" /> |     <page-header v-bind="{ title, info, shortList }" /> | ||||||
|  |  | ||||||
|     <div |     <div | ||||||
|       v-if="!loadedPages.includes(1) && loading == false" |       v-if="!loadedPages.includes(1) && loading == false" | ||||||
|       class="button-container" |       class="button-container" | ||||||
|     > |     > | ||||||
|       <seasoned-button @click="loadLess" class="load-button" :fullWidth="true" |       <seasoned-button class="load-button" :full-width="true" @click="loadLess" | ||||||
|         >load previous</seasoned-button |         >load previous</seasoned-button | ||||||
|       > |       > | ||||||
|     </div> |     </div> | ||||||
| @@ -16,187 +16,188 @@ | |||||||
|  |  | ||||||
|     <div ref="loadMoreButton" class="button-container"> |     <div ref="loadMoreButton" class="button-container"> | ||||||
|       <seasoned-button |       <seasoned-button | ||||||
|         class="load-button" |  | ||||||
|         v-if="!loading && !shortList && page != totalPages && results.length" |         v-if="!loading && !shortList && page != totalPages && results.length" | ||||||
|  |         class="load-button" | ||||||
|  |         :full-width="true" | ||||||
|         @click="loadMore" |         @click="loadMore" | ||||||
|         :fullWidth="true" |  | ||||||
|         >load more</seasoned-button |         >load more</seasoned-button | ||||||
|       > |       > | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| import ListHeader from "@/components/ListHeader"; |   import { defineProps, ref, computed, onMounted } from "vue"; | ||||||
| import ResultsList from "@/components/ResultsList"; |   import PageHeader from "@/components/PageHeader.vue"; | ||||||
| import SeasonedButton from "@/components/ui/SeasonedButton"; |   import ResultsList from "@/components/ResultsList.vue"; | ||||||
| import store from "@/store"; |   import SeasonedButton from "@/components/ui/SeasonedButton.vue"; | ||||||
| import { getTmdbMovieListByName } from "@/api"; |   import Loader from "@/components/ui/Loader.vue"; | ||||||
| import Loader from "@/components/ui/Loader"; |   import type { Ref } from "vue"; | ||||||
|  |   import type { IList, ListResults } from "../interfaces/IList"; | ||||||
|  |   import type ISection from "../interfaces/ISection"; | ||||||
|  |  | ||||||
| export default { |   interface Props extends ISection { | ||||||
|   props: { |     title: string; | ||||||
|     apiFunction: { |     apiFunction: (page: number) => Promise<IList>; | ||||||
|       type: Function, |     shortList?: boolean; | ||||||
|       required: true |  | ||||||
|     }, |  | ||||||
|     title: { |  | ||||||
|       type: String, |  | ||||||
|       required: true |  | ||||||
|     }, |  | ||||||
|     shortList: { |  | ||||||
|       type: Boolean, |  | ||||||
|       required: false, |  | ||||||
|       default: false |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   components: { ListHeader, ResultsList, SeasonedButton, Loader }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       results: [], |  | ||||||
|       page: 1, |  | ||||||
|       loadedPages: [], |  | ||||||
|       totalPages: -1, |  | ||||||
|       totalResults: 0, |  | ||||||
|       loading: true, |  | ||||||
|       autoLoad: false, |  | ||||||
|       observer: undefined |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|   computed: { |  | ||||||
|     info() { |  | ||||||
|       if (this.results.length === 0) return [null, null]; |  | ||||||
|       return [this.pageCount, this.resultCount]; |  | ||||||
|     }, |  | ||||||
|     resultCount() { |  | ||||||
|       const loadedResults = this.results.length; |  | ||||||
|       const totalResults = this.totalResults < 10000 ? this.totalResults : "∞"; |  | ||||||
|       return `${loadedResults} of ${totalResults} results`; |  | ||||||
|     }, |  | ||||||
|     pageCount() { |  | ||||||
|       return `Page ${this.page} of ${this.totalPages}`; |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     loadMore() { |  | ||||||
|       if (!this.autoLoad) { |  | ||||||
|         this.autoLoad = true; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       this.loading = true; |  | ||||||
|       let maxPage = [...this.loadedPages].slice(-1)[0]; |  | ||||||
|  |  | ||||||
|       if (maxPage == NaN) return; |  | ||||||
|       this.page = maxPage + 1; |  | ||||||
|       this.getListResults(); |  | ||||||
|     }, |  | ||||||
|     loadLess() { |  | ||||||
|       this.loading = true; |  | ||||||
|       const minPage = this.loadedPages[0]; |  | ||||||
|       if (minPage === 1) return; |  | ||||||
|  |  | ||||||
|       this.page = minPage - 1; |  | ||||||
|       this.getListResults(true); |  | ||||||
|     }, |  | ||||||
|     updateQueryParams() { |  | ||||||
|       let params = new URLSearchParams(window.location.search); |  | ||||||
|       if (params.has("page")) { |  | ||||||
|         params.set("page", this.page); |  | ||||||
|       } else if (this.page > 1) { |  | ||||||
|         params.append("page", this.page); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       window.history.replaceState( |  | ||||||
|         {}, |  | ||||||
|         "search", |  | ||||||
|         `${window.location.protocol}//${window.location.hostname}${ |  | ||||||
|           window.location.port ? `:${window.location.port}` : "" |  | ||||||
|         }${window.location.pathname}${ |  | ||||||
|           params.toString().length ? `?${params}` : "" |  | ||||||
|         }` |  | ||||||
|       ); |  | ||||||
|     }, |  | ||||||
|     getPageFromUrl() { |  | ||||||
|       return new URLSearchParams(window.location.search).get("page"); |  | ||||||
|     }, |  | ||||||
|     getListResults(front = false) { |  | ||||||
|       this.apiFunction(this.page) |  | ||||||
|         .then(results => { |  | ||||||
|           if (!front) this.results = this.results.concat(...results.results); |  | ||||||
|           else this.results = results.results.concat(...this.results); |  | ||||||
|           this.page = results.page; |  | ||||||
|           this.loadedPages.push(this.page); |  | ||||||
|           this.loadedPages = this.loadedPages.sort((a, b) => a - b); |  | ||||||
|           this.totalPages = results.total_pages; |  | ||||||
|           this.totalResults = results.total_results; |  | ||||||
|         }) |  | ||||||
|         .then(this.updateQueryParams) |  | ||||||
|         .finally(() => (this.loading = false)); |  | ||||||
|     }, |  | ||||||
|     setupAutoloadObserver() { |  | ||||||
|       this.observer = new IntersectionObserver(this.handleButtonIntersection, { |  | ||||||
|         root: this.$refs.resultSection.$el, |  | ||||||
|         rootMargin: "0px", |  | ||||||
|         threshold: 0 |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       this.observer.observe(this.$refs.loadMoreButton); |  | ||||||
|     }, |  | ||||||
|     handleButtonIntersection(entries) { |  | ||||||
|       entries.map(entry => |  | ||||||
|         entry.isIntersecting && this.autoLoad ? this.loadMore() : null |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   created() { |  | ||||||
|     this.page = this.getPageFromUrl() || this.page; |  | ||||||
|     if (this.results.length === 0) this.getListResults(); |  | ||||||
|  |  | ||||||
|     if (!this.shortList) { |  | ||||||
|       store.dispatch( |  | ||||||
|         "documentTitle/updateTitle", |  | ||||||
|         `${this.$router.history.current.name} ${this.title}` |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   mounted() { |  | ||||||
|     if (!this.shortList) { |  | ||||||
|       this.setupAutoloadObserver(); |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   beforeDestroy() { |  | ||||||
|     this.observer = undefined; |  | ||||||
|   } |   } | ||||||
| }; |  | ||||||
|  |   const props = defineProps<Props>(); | ||||||
|  |  | ||||||
|  |   const results: Ref<ListResults> = ref([]); | ||||||
|  |   const page: Ref<number> = ref(1); | ||||||
|  |   const loadedPages: Ref<number[]> = ref([]); | ||||||
|  |   const totalResults: Ref<number> = ref(0); | ||||||
|  |   const totalPages: Ref<number> = ref(0); | ||||||
|  |   const loading: Ref<boolean> = ref(true); | ||||||
|  |   const autoLoad: Ref<boolean> = ref(false); | ||||||
|  |   const observer: Ref<IntersectionObserver> = ref(null); | ||||||
|  |   const resultSection = ref(null); | ||||||
|  |   const loadMoreButton = ref(null); | ||||||
|  |  | ||||||
|  |   function pageCountString(_page: number, _totalPages: number) { | ||||||
|  |     return `Page ${_page} of ${_totalPages}`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function resultCountString(_results: ListResults, _totalResults: number) { | ||||||
|  |     const loadedResults = _results.length; | ||||||
|  |     const __totalResults = _totalResults < 10000 ? _totalResults : "∞"; | ||||||
|  |     return `${loadedResults} of ${__totalResults} results`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function setLoading(state: boolean) { | ||||||
|  |     loading.value = state; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const info = computed(() => { | ||||||
|  |     if (results.value.length === 0) return [null, null]; | ||||||
|  |  | ||||||
|  |     const pageCount = pageCountString(page.value, totalPages.value); | ||||||
|  |     const resultCount = resultCountString(results.value, totalResults.value); | ||||||
|  |     return [pageCount, resultCount]; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   function getPageFromUrl() { | ||||||
|  |     const _page = new URLSearchParams(window.location.search).get("page"); | ||||||
|  |     if (!_page) return null; | ||||||
|  |  | ||||||
|  |     return Number(_page); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function updateQueryParams() { | ||||||
|  |     const params = new URLSearchParams(window.location.search); | ||||||
|  |     if (params.has("page")) { | ||||||
|  |       params.set("page", page.value?.toString()); | ||||||
|  |     } else if (page.value > 1) { | ||||||
|  |       params.append("page", page.value?.toString()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     window.history.replaceState( | ||||||
|  |       {}, | ||||||
|  |       "search", | ||||||
|  |       `${window.location.protocol}//${window.location.hostname}${ | ||||||
|  |         window.location.port ? `:${window.location.port}` : "" | ||||||
|  |       }${window.location.pathname}${ | ||||||
|  |         params.toString().length ? `?${params}` : "" | ||||||
|  |       }` | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function getListResults(front = false) { | ||||||
|  |     props | ||||||
|  |       .apiFunction(page.value) | ||||||
|  |       .then(listResponse => { | ||||||
|  |         if (!front) | ||||||
|  |           results.value = results.value.concat(...listResponse.results); | ||||||
|  |         else results.value = listResponse.results.concat(...results.value); | ||||||
|  |  | ||||||
|  |         page.value = listResponse.page; | ||||||
|  |         loadedPages.value.push(page.value); | ||||||
|  |         loadedPages.value = loadedPages.value.sort((a, b) => a - b); | ||||||
|  |         totalPages.value = listResponse.total_pages; | ||||||
|  |         totalResults.value = listResponse.total_results; | ||||||
|  |       }) | ||||||
|  |       .then(updateQueryParams) | ||||||
|  |       .finally(() => setLoading(false)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function loadMore() { | ||||||
|  |     if (!autoLoad.value) { | ||||||
|  |       autoLoad.value = true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     loading.value = true; | ||||||
|  |     const maxPage = [...loadedPages.value].slice(-1)[0]; | ||||||
|  |  | ||||||
|  |     if (Number.isNaN(maxPage)) return; | ||||||
|  |     page.value = maxPage + 1; | ||||||
|  |     getListResults(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function loadLess() { | ||||||
|  |     loading.value = true; | ||||||
|  |     const minPage = loadedPages.value[0]; | ||||||
|  |     if (minPage === 1) return; | ||||||
|  |  | ||||||
|  |     page.value = minPage - 1; | ||||||
|  |     getListResults(true); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function handleButtonIntersection(entries) { | ||||||
|  |     entries.map(entry => | ||||||
|  |       entry.isIntersecting && autoLoad.value ? loadMore() : null | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function setupAutoloadObserver() { | ||||||
|  |     observer.value = new IntersectionObserver(handleButtonIntersection, { | ||||||
|  |       root: resultSection.value.$el, | ||||||
|  |       rootMargin: "0px", | ||||||
|  |       threshold: 0 | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     observer.value.observe(loadMoreButton.value); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   page.value = getPageFromUrl() || page.value; | ||||||
|  |   if (results.value?.length === 0) getListResults(); | ||||||
|  |   onMounted(() => { | ||||||
|  |     if (!props?.shortList) setupAutoloadObserver(); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   //   beforeDestroy() { | ||||||
|  |   //     this.observer = undefined; | ||||||
|  |   //   } | ||||||
|  |   // }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "src/scss/media-queries"; |   @import "src/scss/media-queries"; | ||||||
|  |  | ||||||
| .resultSection { |   .resultSection { | ||||||
|   background-color: var(--background-color); |     background-color: var(--background-color); | ||||||
| } |  | ||||||
|  |  | ||||||
| .button-container { |  | ||||||
|   display: flex; |  | ||||||
|   justify-content: center; |  | ||||||
|   display: flex; |  | ||||||
|   width: 100%; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .load-button { |  | ||||||
|   margin: 2rem 0; |  | ||||||
|  |  | ||||||
|   @include mobile { |  | ||||||
|     margin: 1rem 0; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &:last-of-type { |   .button-container { | ||||||
|     margin-bottom: 4rem; |     display: flex; | ||||||
|  |     justify-content: center; | ||||||
|  |     display: flex; | ||||||
|  |     width: 100%; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .load-button { | ||||||
|  |     margin: 2rem 0; | ||||||
|  |  | ||||||
|     @include mobile { |     @include mobile { | ||||||
|       margin-bottom: 2rem; |       margin: 1rem 0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &:last-of-type { | ||||||
|  |       margin-bottom: 4rem; | ||||||
|  |  | ||||||
|  |       @include mobile { | ||||||
|  |         margin-bottom: 2rem; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,747 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div v-if="show" class="container"> |  | ||||||
|     <h2 class="torrentHeader-text editable"> |  | ||||||
|       Searching for: |  | ||||||
|       <span :contenteditable="!edit" @input="this.handleInput">{{ |  | ||||||
|         query |  | ||||||
|       }}</span> |  | ||||||
|  |  | ||||||
|       <IconSearch |  | ||||||
|         class="icon" |  | ||||||
|         v-if="editedSearchQuery && editedSearchQuery.length" |  | ||||||
|       /> |  | ||||||
|       <IconEdit v-else class="icon" @click="() => (this.edit = !this.edit)" /> |  | ||||||
|     </h2> |  | ||||||
|  |  | ||||||
|     <div v-if="!loading"> |  | ||||||
|       <div v-if="torrents.length > 0"> |  | ||||||
|         <table> |  | ||||||
|           <thead class="table__header noselect"> |  | ||||||
|             <tr> |  | ||||||
|               <th |  | ||||||
|                 v-for="column in columns" |  | ||||||
|                 :key="column" |  | ||||||
|                 @click="sortTable(column)" |  | ||||||
|                 :class="column === selectedColumn ? 'active' : null" |  | ||||||
|               > |  | ||||||
|                 {{ column }} |  | ||||||
|                 <span v-if="prevCol === column && direction">↑</span> |  | ||||||
|                 <span v-if="prevCol === column && !direction">↓</span> |  | ||||||
|               </th> |  | ||||||
|             </tr> |  | ||||||
|             <!--             <th |  | ||||||
|               @click="sortTable('name')" |  | ||||||
|               :class="selectedSortableClass('name')" |  | ||||||
|             > |  | ||||||
|               <span>Name</span> |  | ||||||
|               <span v-if="prevCol === 'name' && direction">↑</span> |  | ||||||
|               <span v-if="prevCol === 'name' && !direction">↓</span> |  | ||||||
|             </th> |  | ||||||
|             <th |  | ||||||
|               @click="sortTable('seed')" |  | ||||||
|               :class="selectedSortableClass('seed')" |  | ||||||
|             > |  | ||||||
|               <span>Seed</span> |  | ||||||
|               <span v-if="prevCol === 'seed' && direction">↑</span> |  | ||||||
|               <span v-if="prevCol === 'seed' && !direction">↓</span> |  | ||||||
|             </th> |  | ||||||
|             <th |  | ||||||
|               @click="sortTable('size')" |  | ||||||
|               :class="selectedSortableClass('size')" |  | ||||||
|             > |  | ||||||
|               <span>Size</span> |  | ||||||
|               <span v-if="prevCol === 'size' && direction">↑</span> |  | ||||||
|               <span v-if="prevCol === 'size' && !direction">↓</span> |  | ||||||
|             </th> |  | ||||||
|  |  | ||||||
|             <th> |  | ||||||
|               <span>Magnet</span> |  | ||||||
|             </th> --> |  | ||||||
|           </thead> |  | ||||||
|  |  | ||||||
|           <tbody> |  | ||||||
|             <tr |  | ||||||
|               v-for="torrent in torrents" |  | ||||||
|               class="table__content" |  | ||||||
|               :key="torrent.magnet" |  | ||||||
|             > |  | ||||||
|               <td @click="expand($event, torrent.name)">{{ torrent.name }}</td> |  | ||||||
|               <td @click="expand($event, torrent.name)">{{ torrent.seed }}</td> |  | ||||||
|               <td @click="expand($event, torrent.name)">{{ torrent.size }}</td> |  | ||||||
|               <td |  | ||||||
|                 @click="sendTorrent(torrent.magnet, torrent.name, $event)" |  | ||||||
|                 class="download" |  | ||||||
|               > |  | ||||||
|                 <IconMagnet /> |  | ||||||
|               </td> |  | ||||||
|             </tr> |  | ||||||
|           </tbody> |  | ||||||
|         </table> |  | ||||||
|  |  | ||||||
|         <div style="display: flex; justify-content: center; padding: 1rem"> |  | ||||||
|           <seasonedButton @click="resetTorrentsAndToggleEditSearchQuery" |  | ||||||
|             >Edit search query</seasonedButton |  | ||||||
|           > |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|  |  | ||||||
|       <div |  | ||||||
|         v-else |  | ||||||
|         style=" |  | ||||||
|           display: flex; |  | ||||||
|           padding-bottom: 2rem; |  | ||||||
|           justify-content: center; |  | ||||||
|           flex-direction: column; |  | ||||||
|           width: 100%; |  | ||||||
|           align-items: center; |  | ||||||
|         " |  | ||||||
|       > |  | ||||||
|         <h2>No results found</h2> |  | ||||||
|         <br /> |  | ||||||
|  |  | ||||||
|         <div class="editQuery" v-if="editSearchQuery"> |  | ||||||
|           <seasonedInput |  | ||||||
|             placeholder="Torrent query" |  | ||||||
|             :value.sync="editedSearchQuery" |  | ||||||
|             @enter="fetchTorrents(editedSearchQuery)" |  | ||||||
|           /> |  | ||||||
|  |  | ||||||
|           <div style="height: 45px; width: 5px"></div> |  | ||||||
|  |  | ||||||
|           <seasonedButton @click="fetchTorrents(editedSearchQuery)" |  | ||||||
|             >Search</seasonedButton |  | ||||||
|           > |  | ||||||
|         </div> |  | ||||||
|  |  | ||||||
|         <seasonedButton |  | ||||||
|           @click="toggleEditSearchQuery" |  | ||||||
|           :active="editSearchQuery ? true : false" |  | ||||||
|           >Edit search query</seasonedButton |  | ||||||
|         > |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|     <div v-else class="torrentloader"><i></i></div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import store from "@/store"; |  | ||||||
| import { sortableSize } from "@/utils"; |  | ||||||
| import { searchTorrents, addMagnet } from "@/api"; |  | ||||||
|  |  | ||||||
| import IconMagnet from "../icons/IconMagnet"; |  | ||||||
| import IconEdit from "../icons/IconEdit"; |  | ||||||
| import IconSearch from "../icons/IconSearch"; |  | ||||||
|  |  | ||||||
| import SeasonedButton from "@/components/ui/SeasonedButton"; |  | ||||||
| import SeasonedInput from "@/components/ui/SeasonedInput"; |  | ||||||
| import ToggleButton from "@/components/ui/ToggleButton"; |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   components: { |  | ||||||
|     IconMagnet, |  | ||||||
|     IconEdit, |  | ||||||
|     IconSearch, |  | ||||||
|     SeasonedButton, |  | ||||||
|     SeasonedInput, |  | ||||||
|     ToggleButton |  | ||||||
|   }, |  | ||||||
|   props: { |  | ||||||
|     query: { |  | ||||||
|       type: String, |  | ||||||
|       require: true |  | ||||||
|     }, |  | ||||||
|     tmdb_id: { |  | ||||||
|       type: Number, |  | ||||||
|       require: true |  | ||||||
|     }, |  | ||||||
|     tmdb_type: String, |  | ||||||
|     admin: Boolean, |  | ||||||
|     show: Boolean |  | ||||||
|   }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       edit: true, |  | ||||||
|       loading: false, |  | ||||||
|       torrents: [], |  | ||||||
|       torrentResponse: undefined, |  | ||||||
|       currentPage: 0, |  | ||||||
|       prevCol: "", |  | ||||||
|       direction: false, |  | ||||||
|       release_types: ["all"], |  | ||||||
|       selectedRelaseType: "all", |  | ||||||
|       editSearchQuery: false, |  | ||||||
|       editedSearchQuery: "", |  | ||||||
|  |  | ||||||
|       columns: ["name", "seed", "size", "magnet"], |  | ||||||
|       selectedColumn: null |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|   created() { |  | ||||||
|     this.fetchTorrents().then(_ => this.sortTable("size")); |  | ||||||
|   }, |  | ||||||
|   watch: { |  | ||||||
|     selectedRelaseType: function (newValue) { |  | ||||||
|       this.applyFilter(newValue); |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     selectedSortableClass(headerName) { |  | ||||||
|       return headerName === this.prevCol ? "active" : ""; |  | ||||||
|     }, |  | ||||||
|     resetTorrentsAndToggleEditSearchQuery() { |  | ||||||
|       this.torrents = []; |  | ||||||
|       this.toggleEditSearchQuery(); |  | ||||||
|     }, |  | ||||||
|     toggleEditSearchQuery() { |  | ||||||
|       this.editSearchQuery = !this.editSearchQuery; |  | ||||||
|     }, |  | ||||||
|     expand(event, name) { |  | ||||||
|       const existingExpandedElement = |  | ||||||
|         document.getElementsByClassName("expanded")[0]; |  | ||||||
|  |  | ||||||
|       const clickedElement = event.target.parentNode; |  | ||||||
|       const scopedStyleDataVariable = Object.keys(clickedElement.dataset)[0]; |  | ||||||
|  |  | ||||||
|       if (existingExpandedElement) { |  | ||||||
|         const expandedSibling = |  | ||||||
|           event.target.parentNode.nextSibling.className === "expanded"; |  | ||||||
|  |  | ||||||
|         existingExpandedElement.remove(); |  | ||||||
|         const table = document.getElementsByTagName("table")[0]; |  | ||||||
|         table.style.display = "block"; |  | ||||||
|  |  | ||||||
|         if (expandedSibling) { |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       const nameRow = document.createElement("tr"); |  | ||||||
|       const nameCol = document.createElement("td"); |  | ||||||
|       nameRow.className = "expanded"; |  | ||||||
|       nameRow.dataset[scopedStyleDataVariable] = ""; |  | ||||||
|       nameCol.innerText = name; |  | ||||||
|       nameCol.dataset[scopedStyleDataVariable] = ""; |  | ||||||
|  |  | ||||||
|       nameRow.appendChild(nameCol); |  | ||||||
|  |  | ||||||
|       clickedElement.insertAdjacentElement("afterend", nameRow); |  | ||||||
|     }, |  | ||||||
|     sendTorrent(magnet, name, event) { |  | ||||||
|       this.$notifications.info({ |  | ||||||
|         title: "Adding torrent 🦜", |  | ||||||
|         description: this.query, |  | ||||||
|         timeout: 3000 |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       event.target.parentNode.classList.add("active"); |  | ||||||
|       addMagnet(magnet, name, this.tmdb_id) |  | ||||||
|         .catch(resp => { |  | ||||||
|           console.log("error:", resp.data); |  | ||||||
|         }) |  | ||||||
|         .then(resp => { |  | ||||||
|           console.log("addTorrent resp: ", resp); |  | ||||||
|           this.$notifications.success({ |  | ||||||
|             title: "Torrent added 🎉", |  | ||||||
|             description: this.query, |  | ||||||
|             timeout: 3000 |  | ||||||
|           }); |  | ||||||
|         }); |  | ||||||
|     }, |  | ||||||
|     sortTable(col, sameDirection = false) { |  | ||||||
|       if (this.prevCol === col && sameDirection === false) { |  | ||||||
|         this.direction = !this.direction; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (col === "name") this.sortName(); |  | ||||||
|       else if (col === "seed") this.sortSeed(); |  | ||||||
|       else if (col === "size") this.sortSize(); |  | ||||||
|  |  | ||||||
|       this.prevCol = col; |  | ||||||
|     }, |  | ||||||
|     sortName() { |  | ||||||
|       const torrentsCopy = [...this.torrents]; |  | ||||||
|       if (this.direction) { |  | ||||||
|         this.torrents = torrentsCopy.sort((a, b) => (a.name < b.name ? 1 : -1)); |  | ||||||
|       } else { |  | ||||||
|         this.torrents = torrentsCopy.sort((a, b) => (a.name > b.name ? 1 : -1)); |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     sortSeed() { |  | ||||||
|       const torrentsCopy = [...this.torrents]; |  | ||||||
|       if (this.direction) { |  | ||||||
|         this.torrents = torrentsCopy.sort( |  | ||||||
|           (a, b) => parseInt(a.seed) - parseInt(b.seed) |  | ||||||
|         ); |  | ||||||
|       } else { |  | ||||||
|         this.torrents = torrentsCopy.sort( |  | ||||||
|           (a, b) => parseInt(b.seed) - parseInt(a.seed) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     sortSize() { |  | ||||||
|       const torrentsCopy = [...this.torrents]; |  | ||||||
|       if (this.direction) { |  | ||||||
|         this.torrents = torrentsCopy.sort( |  | ||||||
|           (a, b) => |  | ||||||
|             parseInt(sortableSize(a.size)) - parseInt(sortableSize(b.size)) |  | ||||||
|         ); |  | ||||||
|       } else { |  | ||||||
|         this.torrents = torrentsCopy.sort( |  | ||||||
|           (a, b) => |  | ||||||
|             parseInt(sortableSize(b.size)) - parseInt(sortableSize(a.size)) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     findRelaseTypes() { |  | ||||||
|       this.torrents.forEach(item => |  | ||||||
|         this.release_types.push(...item.release_type) |  | ||||||
|       ); |  | ||||||
|       this.release_types = [...new Set(this.release_types)]; |  | ||||||
|     }, |  | ||||||
|     applyFilter(item, index) { |  | ||||||
|       this.selectedRelaseType = item; |  | ||||||
|       const torrents = [...this.torrentResponse]; |  | ||||||
|  |  | ||||||
|       if (item === "all") { |  | ||||||
|         this.torrents = torrents; |  | ||||||
|         this.sortTable(this.prevCol, true); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       this.torrents = torrents.filter(torrent => |  | ||||||
|         torrent.release_type.includes(item) |  | ||||||
|       ); |  | ||||||
|       this.sortTable(this.prevCol, true); |  | ||||||
|     }, |  | ||||||
|     updateResultCountInStore() { |  | ||||||
|       store.dispatch("torrentModule/setResults", this.torrents); |  | ||||||
|       store.dispatch( |  | ||||||
|         "torrentModule/setResultCount", |  | ||||||
|         this.torrentResponse.length |  | ||||||
|       ); |  | ||||||
|     }, |  | ||||||
|     filterDeadTorrents(torrents) { |  | ||||||
|       return torrents.filter(torrent => { |  | ||||||
|         if (isNaN(torrent.seed)) return false; |  | ||||||
|         return parseInt(torrent.seed) > 0; |  | ||||||
|       }); |  | ||||||
|     }, |  | ||||||
|     fetchTorrents(query = undefined) { |  | ||||||
|       this.loading = true; |  | ||||||
|       this.editSearchQuery = false; |  | ||||||
|  |  | ||||||
|       return searchTorrents(query || this.query) |  | ||||||
|         .then(data => { |  | ||||||
|           const { results } = data; |  | ||||||
|           if (results) { |  | ||||||
|             this.torrentResponse = results; |  | ||||||
|             this.torrents = this.filterDeadTorrents(results); |  | ||||||
|           } else { |  | ||||||
|             this.torrents = []; |  | ||||||
|           } |  | ||||||
|         }) |  | ||||||
|         .then(this.updateResultCountInStore) |  | ||||||
|         .then(this.findRelaseTypes) |  | ||||||
|         .catch(e => { |  | ||||||
|           console.log("e:", e); |  | ||||||
|           const error = e.toString(); |  | ||||||
|           this.errorMessage = |  | ||||||
|             error.indexOf("401") != -1 ? "Permission denied" : "Nothing found"; |  | ||||||
|         }) |  | ||||||
|         .finally(() => { |  | ||||||
|           this.loading = false; |  | ||||||
|         }); |  | ||||||
|     }, |  | ||||||
|     handleInput(event) { |  | ||||||
|       this.editedSearchQuery = event.target.innerText; |  | ||||||
|       console.log("edit text:", this.editedSearchQuery); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "src/scss/variables"; |  | ||||||
| .expanded { |  | ||||||
|   display: flex; |  | ||||||
|   padding: 0.25rem 1rem; |  | ||||||
|   max-width: 100%; |  | ||||||
|   border-left: 1px solid $text-color; |  | ||||||
|   border-right: 1px solid $text-color; |  | ||||||
|   border-bottom: 1px solid $text-color; |  | ||||||
|  |  | ||||||
|   td { |  | ||||||
|     word-break: break-all; |  | ||||||
|     padding: 0.5rem 0.15rem; |  | ||||||
|     width: 100%; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| $checkboxSize: 20px; |  | ||||||
| $ui-border-width: 2px; |  | ||||||
|  |  | ||||||
| .checkbox { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: row; |  | ||||||
|   margin-bottom: $checkboxSize * 0.5; |  | ||||||
|  |  | ||||||
|   input[type="checkbox"] { |  | ||||||
|     display: block; |  | ||||||
|     opacity: 0; |  | ||||||
|     position: absolute; |  | ||||||
|  |  | ||||||
|     + div { |  | ||||||
|       position: relative; |  | ||||||
|       display: inline-block; |  | ||||||
|       padding-left: 1.25rem; |  | ||||||
|       font-size: 20px; |  | ||||||
|       line-height: $checkboxSize + $ui-border-width * 2; |  | ||||||
|       left: $checkboxSize; |  | ||||||
|       cursor: pointer; |  | ||||||
|  |  | ||||||
|       &::before { |  | ||||||
|         content: ""; |  | ||||||
|         display: inline-block; |  | ||||||
|         position: absolute; |  | ||||||
|         left: -$checkboxSize; |  | ||||||
|         border: $ui-border-width solid var(--color-green); |  | ||||||
|         width: $checkboxSize; |  | ||||||
|         height: $checkboxSize; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       &::after { |  | ||||||
|         transition: all 0.3s ease; |  | ||||||
|         content: ""; |  | ||||||
|         position: absolute; |  | ||||||
|         display: inline-block; |  | ||||||
|         left: -$checkboxSize + $ui-border-width; |  | ||||||
|         top: $ui-border-width; |  | ||||||
|         width: $checkboxSize + $ui-border-width; |  | ||||||
|         height: $checkboxSize + $ui-border-width; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     &:checked { |  | ||||||
|       + div::after { |  | ||||||
|         background-color: var(--color-green); |  | ||||||
|         opacity: 1; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     &:hover:not(checked) { |  | ||||||
|       + div::after { |  | ||||||
|         background-color: var(--color-green); |  | ||||||
|         opacity: 0.4; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     &:focus { |  | ||||||
|       + div::before { |  | ||||||
|         outline: 2px solid Highlight; |  | ||||||
|         outline-style: auto; |  | ||||||
|         outline-color: -webkit-focus-ring-color; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| @import "src/scss/variables"; |  | ||||||
| @import "src/scss/media-queries"; |  | ||||||
| @import "src/scss/elements"; |  | ||||||
|  |  | ||||||
| h2 { |  | ||||||
|   font-size: 20px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| thead { |  | ||||||
|   user-select: none; |  | ||||||
|   -webkit-user-select: none; |  | ||||||
|   color: var(--background-color); |  | ||||||
|   text-transform: uppercase; |  | ||||||
|   cursor: pointer; |  | ||||||
|   background-color: var(--text-color); |  | ||||||
|   letter-spacing: 0.8px; |  | ||||||
|   font-size: 1rem; |  | ||||||
|   border: 1px solid var(--text-color-90); |  | ||||||
|  |  | ||||||
|   th:first-of-type { |  | ||||||
|     border-top-left-radius: 8px; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   th:last-of-type { |  | ||||||
|     border-top-right-radius: 8px; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| tbody { |  | ||||||
|   tr > td:first-of-type { |  | ||||||
|     white-space: unset; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   tr > td:not(td:first-of-type) { |  | ||||||
|     text-align: center; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   tr > td:last-of-type { |  | ||||||
|     cursor: pointer; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   tr td:first-of-type { |  | ||||||
|     border-left: 1px solid var(--text-color-90); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   tr td:last-of-type { |  | ||||||
|     border-right: 1px solid var(--text-color-90); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   tr:last-of-type { |  | ||||||
|     td { |  | ||||||
|       border-bottom: 1px solid var(--text-color-90); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     td:first-of-type { |  | ||||||
|       border-bottom-left-radius: 8px; |  | ||||||
|     } |  | ||||||
|     td:last-of-type { |  | ||||||
|       border-bottom-right-radius: 8px; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   tr:nth-child(even) { |  | ||||||
|     background-color: var(--background-70); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| th, |  | ||||||
| td { |  | ||||||
|   padding: 0.35rem 0.25rem; |  | ||||||
|   white-space: nowrap; |  | ||||||
|  |  | ||||||
|   svg { |  | ||||||
|     width: 24px; |  | ||||||
|     fill: var(--text-color); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .toggle { |  | ||||||
|   max-width: unset !important; |  | ||||||
|   margin: 1rem 0; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .container { |  | ||||||
|   background-color: $background-color; |  | ||||||
|   padding: 0 1rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .torrentHeader { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: center; |  | ||||||
|   padding-bottom: 20px; |  | ||||||
|  |  | ||||||
|   &-text { |  | ||||||
|     font-weight: 400; |  | ||||||
|     text-transform: uppercase; |  | ||||||
|     font-size: 20px; |  | ||||||
|     // color: $green; |  | ||||||
|     text-align: center; |  | ||||||
|     margin: 0; |  | ||||||
|  |  | ||||||
|     .icon { |  | ||||||
|       vertical-align: text-top; |  | ||||||
|       margin-left: 1rem; |  | ||||||
|       fill: var(--text-color); |  | ||||||
|       width: 22px; |  | ||||||
|       height: 22px; |  | ||||||
|       // stroke: white !important; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     &.editable { |  | ||||||
|       cursor: pointer; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &-editIcon { |  | ||||||
|     margin-left: 10px; |  | ||||||
|     margin-top: -3px; |  | ||||||
|     width: 22px; |  | ||||||
|     height: 22px; |  | ||||||
|  |  | ||||||
|     &:hover { |  | ||||||
|       fill: $green; |  | ||||||
|       cursor: pointer; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| table { |  | ||||||
|   // border-collapse: collapse; |  | ||||||
|   border-spacing: 0; |  | ||||||
|   margin-top: 1rem; |  | ||||||
|   width: 100%; |  | ||||||
|   // table-layout: fixed; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // .table__content, |  | ||||||
| // .table__header { |  | ||||||
| //   display: flex; |  | ||||||
| //   padding: 0; |  | ||||||
| //   border-left: 1px solid $text-color; |  | ||||||
| //   border-right: 1px solid $text-color; |  | ||||||
| //   border-bottom: 1px solid $text-color; |  | ||||||
|  |  | ||||||
| //   th, |  | ||||||
| //   td { |  | ||||||
| //     display: flex; |  | ||||||
| //     flex-direction: column; |  | ||||||
| //     flex-basis: 100%; |  | ||||||
|  |  | ||||||
| //     padding: 0.4rem; |  | ||||||
|  |  | ||||||
| //     white-space: nowrap; |  | ||||||
| //     text-overflow: ellipsis; |  | ||||||
| //     overflow: hidden; |  | ||||||
| //     min-width: 75px; |  | ||||||
| //   } |  | ||||||
|  |  | ||||||
| //   th:first-child, |  | ||||||
| //   td:first-child { |  | ||||||
| //     flex: 1; |  | ||||||
| //   } |  | ||||||
|  |  | ||||||
| //   th:not(:first-child), |  | ||||||
| //   td:not(:first-child) { |  | ||||||
| //     flex: 0.2; |  | ||||||
| //   } |  | ||||||
|  |  | ||||||
| //   th:nth-child(2), |  | ||||||
| //   td:nth-child(2) { |  | ||||||
| //     flex: 0.1; |  | ||||||
| //   } |  | ||||||
|  |  | ||||||
| //   @include mobile-only { |  | ||||||
| //     th:first-child, |  | ||||||
| //     td:first-child { |  | ||||||
| //       display: none; |  | ||||||
|  |  | ||||||
| //       &.show { |  | ||||||
| //         display: block; |  | ||||||
| //         align: flex-end; |  | ||||||
| //       } |  | ||||||
| //     } |  | ||||||
|  |  | ||||||
| //     th:not(:first-child), |  | ||||||
| //     td:not(:first-child) { |  | ||||||
| //       flex: 1; |  | ||||||
| //     } |  | ||||||
| //   } |  | ||||||
| // } |  | ||||||
|  |  | ||||||
| .table__content { |  | ||||||
|   td:not(:last-child) { |  | ||||||
|     border-right: 1px solid $text-color; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .table__content:last-child { |  | ||||||
|   margin-bottom: 1rem; |  | ||||||
|  |  | ||||||
|   border-bottom-left-radius: 3px; |  | ||||||
|   border-bottom-right-radius: 3px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // .table__header { |  | ||||||
| //   color: $text-color; |  | ||||||
| //   text-transform: uppercase; |  | ||||||
| //   cursor: pointer; |  | ||||||
| //   background-color: $background-color-secondary; |  | ||||||
|  |  | ||||||
| //   border-top: 1px solid $text-color; |  | ||||||
| //   border-top-left-radius: 3px; |  | ||||||
| //   border-top-right-radius: 3px; |  | ||||||
|  |  | ||||||
| //   th { |  | ||||||
| //     display: flex; |  | ||||||
| //     flex-direction: row; |  | ||||||
| //     font-weight: 400; |  | ||||||
| //     letter-spacing: 0.7px; |  | ||||||
| //     // font-size: 1.08rem; |  | ||||||
| //     font-size: 15px; |  | ||||||
|  |  | ||||||
| //     &::before { |  | ||||||
| //       content: ""; |  | ||||||
| //       min-width: 0.2rem; |  | ||||||
| //     } |  | ||||||
|  |  | ||||||
| //     span:first-child { |  | ||||||
| //       margin-right: 0.6rem; |  | ||||||
| //     } |  | ||||||
| //     span:nth-child(2) { |  | ||||||
| //       margin-right: 0.1rem; |  | ||||||
| //     } |  | ||||||
| //   } |  | ||||||
|  |  | ||||||
| //   th:not(:last-child) { |  | ||||||
| //     border-right: 1px solid $text-color; |  | ||||||
| //   } |  | ||||||
| // } |  | ||||||
|  |  | ||||||
| .editQuery { |  | ||||||
|   display: flex; |  | ||||||
|   width: 70%; |  | ||||||
|   justify-content: center; |  | ||||||
|   margin-bottom: 1rem; |  | ||||||
|  |  | ||||||
|   @include mobile-only { |  | ||||||
|     width: 90%; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .download { |  | ||||||
|   &__icon { |  | ||||||
|     fill: $text-color-70; |  | ||||||
|     height: 1.2rem; |  | ||||||
|  |  | ||||||
|     &:hover { |  | ||||||
|       fill: $text-color; |  | ||||||
|       cursor: pointer; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &.active &__icon { |  | ||||||
|     fill: $green; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .torrentloader { |  | ||||||
|   width: 100%; |  | ||||||
|   padding: 2rem 0; |  | ||||||
|  |  | ||||||
|   i { |  | ||||||
|     animation: load 1s linear infinite; |  | ||||||
|     border: 2px solid $text-color; |  | ||||||
|     border-radius: 50%; |  | ||||||
|     display: block; |  | ||||||
|     height: 30px; |  | ||||||
|     left: 50%; |  | ||||||
|     margin: 0 auto; |  | ||||||
|     width: 30px; |  | ||||||
|  |  | ||||||
|     &:after { |  | ||||||
|       border: 5px solid $green; |  | ||||||
|       border-radius: 50%; |  | ||||||
|       content: ""; |  | ||||||
|       left: 10px; |  | ||||||
|       position: absolute; |  | ||||||
|       top: 16px; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @keyframes load { |  | ||||||
|   100% { |  | ||||||
|     transform: rotate(360deg); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @@ -1,13 +1,12 @@ | |||||||
| <template> | <template> | ||||||
|   <transition name="shut"> |   <transition name="shut"> | ||||||
|     <ul class="dropdown"> |     <ul class="dropdown"> | ||||||
|  |       <!-- eslint-disable-next-line vuejs-accessibility/click-events-have-key-events --> | ||||||
|       <li |       <li | ||||||
|         v-for="result in searchResults" |         v-for="(result, _index) in searchResults" | ||||||
|         :key="`${result.index}-${result.title}-${result.type}`" |         :key="`${_index}-${result.title}-${result.type}`" | ||||||
|  |         :class="`result di-${_index} ${_index === index ? 'active' : ''}`" | ||||||
|         @click="openPopup(result)" |         @click="openPopup(result)" | ||||||
|         :class=" |  | ||||||
|           `result di-${result.index} ${result.index === index ? 'active' : ''}` |  | ||||||
|         " |  | ||||||
|       > |       > | ||||||
|         <IconMovie v-if="result.type == 'movie'" class="type-icon" /> |         <IconMovie v-if="result.type == 'movie'" class="type-icon" /> | ||||||
|         <IconShow v-if="result.type == 'show'" class="type-icon" /> |         <IconShow v-if="result.type == 'show'" class="type-icon" /> | ||||||
| @@ -24,239 +23,243 @@ | |||||||
|   </transition> |   </transition> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| import { mapActions } from "vuex"; |   import { ref, watch, defineProps } from "vue"; | ||||||
| import IconMovie from "src/icons/IconMovie"; |   import { useStore } from "vuex"; | ||||||
| import IconShow from "src/icons/IconShow"; |   import IconMovie from "@/icons/IconMovie.vue"; | ||||||
| import IconPerson from "src/icons/IconPerson"; |   import IconShow from "@/icons/IconShow.vue"; | ||||||
| import { elasticSearchMoviesAndShows } from "@/api"; |   import type { Ref } from "vue"; | ||||||
|  |   import { elasticSearchMoviesAndShows } from "../../api"; | ||||||
|  |   import { MediaTypes } from "../../interfaces/IList"; | ||||||
|  |   import { Index } from "../../interfaces/IAutocompleteSearch"; | ||||||
|  |   import type { | ||||||
|  |     IAutocompleteResult, | ||||||
|  |     IAutocompleteSearchResults | ||||||
|  |   } from "../../interfaces/IAutocompleteSearch"; | ||||||
|  |  | ||||||
| export default { |   interface Props { | ||||||
|   components: { IconMovie, IconShow, IconPerson }, |     query?: string; | ||||||
|   props: { |     index?: number; | ||||||
|     query: { |     results?: Array<IAutocompleteResult>; | ||||||
|       type: String, |   } | ||||||
|       default: null, |  | ||||||
|       required: false |   interface Emit { | ||||||
|     }, |     (e: "update:results", value: Array<IAutocompleteResult>); | ||||||
|     index: { |   } | ||||||
|       type: Number, |  | ||||||
|       default: -1, |   const numberOfResults = 10; | ||||||
|       required: false |   const props = defineProps<Props>(); | ||||||
|     }, |   const emit = defineEmits<Emit>(); | ||||||
|     results: { |   const store = useStore(); | ||||||
|       type: Array, |  | ||||||
|       default: [], |   const searchResults: Ref<Array<IAutocompleteResult>> = ref([]); | ||||||
|       required: false |   const keyboardNavigationIndex: Ref<number> = ref(0); | ||||||
|  |  | ||||||
|  |   watch( | ||||||
|  |     () => props.query, | ||||||
|  |     newQuery => { | ||||||
|  |       if (newQuery?.length > 0) | ||||||
|  |         fetchAutocompleteResults(); /* eslint-disable-line no-use-before-define */ | ||||||
|     } |     } | ||||||
|   }, |   ); | ||||||
|   watch: { |  | ||||||
|     query(newQuery) { |  | ||||||
|       if (newQuery && newQuery.length > 1) this.fetchAutocompleteResults(); |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       searchResults: [], |  | ||||||
|       keyboardNavigationIndex: 0, |  | ||||||
|       numberOfResults: 10 |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     ...mapActions("popup", ["open"]), |  | ||||||
|     openPopup(result) { |  | ||||||
|       const { id, type } = result; |  | ||||||
|       this.open({ id, type }); |  | ||||||
|     }, |  | ||||||
|     fetchAutocompleteResults() { |  | ||||||
|       this.keyboardNavigationIndex = 0; |  | ||||||
|       this.searchResults = []; |  | ||||||
|  |  | ||||||
|       elasticSearchMoviesAndShows(this.query, this.numberOfResults).then( |   function openPopup(result) { | ||||||
|         resp => { |     if (!result.id || !result.type) return; | ||||||
|           const data = resp.hits.hits; |  | ||||||
|  |  | ||||||
|           let results = data.map(item => { |     store.dispatch("popup/open", { ...result }); | ||||||
|             let index = null; |   } | ||||||
|             if (item._source.log.file.path.includes("movie")) index = "movie"; |  | ||||||
|             if (item._source.log.file.path.includes("series")) index = "show"; |  | ||||||
|  |  | ||||||
|             if (index === "movie" || index === "show") { |   function removeDuplicates(_searchResults) { | ||||||
|               return { |     const filteredResults = []; | ||||||
|                 title: |     _searchResults.forEach(result => { | ||||||
|                   item._source.original_name || item._source.original_title, |       if (result === undefined) return; | ||||||
|                 id: item._source.id, |       const numberOfDuplicates = filteredResults.filter( | ||||||
|                 adult: item._source.adult, |         filterItem => filterItem.id === result.id | ||||||
|                 type: index |  | ||||||
|               }; |  | ||||||
|             } |  | ||||||
|           }); |  | ||||||
|  |  | ||||||
|           results = this.removeDuplicates(results); |  | ||||||
|           results = results.map((el, index) => { |  | ||||||
|             return { ...el, index }; |  | ||||||
|           }); |  | ||||||
|  |  | ||||||
|           this.$emit("update:results", results); |  | ||||||
|           this.searchResults = results; |  | ||||||
|         } |  | ||||||
|       ); |       ); | ||||||
|     }, |       if (numberOfDuplicates.length >= 1) { | ||||||
|     removeDuplicates(searchResults) { |         return; | ||||||
|       let filteredResults = []; |  | ||||||
|       searchResults.map(result => { |  | ||||||
|         if (result === undefined) return; |  | ||||||
|         const numberOfDuplicates = filteredResults.filter( |  | ||||||
|           filterItem => filterItem.id == result.id |  | ||||||
|         ); |  | ||||||
|         if (numberOfDuplicates.length >= 1) { |  | ||||||
|           return null; |  | ||||||
|         } |  | ||||||
|         filteredResults.push(result); |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       if (this.adult == false) { |  | ||||||
|         filteredResults = filteredResults.filter( |  | ||||||
|           result => result.adult == false |  | ||||||
|         ); |  | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       return filteredResults; |       filteredResults.push(result); | ||||||
|     } |     }); | ||||||
|   }, |  | ||||||
|   created() { |     return filteredResults; | ||||||
|     if (this.query) this.fetchAutocompleteResults(); |  | ||||||
|   } |   } | ||||||
| }; |  | ||||||
|  |   function elasticIndexToMediaType(index: Index): MediaTypes { | ||||||
|  |     if (index === Index.Movies) return MediaTypes.Movie; | ||||||
|  |     if (index === Index.Shows) return MediaTypes.Show; | ||||||
|  |  | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function parseElasticResponse(elasticResponse: IAutocompleteSearchResults) { | ||||||
|  |     const data = elasticResponse.hits.hits; | ||||||
|  |  | ||||||
|  |     const results: Array<IAutocompleteResult> = []; | ||||||
|  |  | ||||||
|  |     data.forEach(item => { | ||||||
|  |       if (!Object.values(Index).includes(item._index)) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       results.push({ | ||||||
|  |         title: item._source?.original_name || item._source.original_title, | ||||||
|  |         id: item._source.id, | ||||||
|  |         adult: item._source.adult, | ||||||
|  |         type: elasticIndexToMediaType(item._index) | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return removeDuplicates(results).map((el, index) => { | ||||||
|  |       return { ...el, index }; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function fetchAutocompleteResults() { | ||||||
|  |     keyboardNavigationIndex.value = 0; | ||||||
|  |     searchResults.value = []; | ||||||
|  |  | ||||||
|  |     elasticSearchMoviesAndShows(props.query, numberOfResults) | ||||||
|  |       .then(elasticResponse => parseElasticResponse(elasticResponse)) | ||||||
|  |       .then(_searchResults => { | ||||||
|  |         emit("update:results", _searchResults); | ||||||
|  |         searchResults.value = _searchResults; | ||||||
|  |       }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // on load functions | ||||||
|  |   fetchAutocompleteResults(); | ||||||
|  |   // end on load functions | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "src/scss/variables"; |   @import "src/scss/variables"; | ||||||
| @import "src/scss/media-queries"; |   @import "src/scss/media-queries"; | ||||||
| @import "src/scss/main"; |   @import "src/scss/main"; | ||||||
| $sizes: 22; |   $sizes: 22; | ||||||
|  |  | ||||||
| @for $i from 0 through $sizes { |   @for $i from 0 through $sizes { | ||||||
|   .dropdown .di-#{$i} { |     .dropdown .di-#{$i} { | ||||||
|     visibility: visible; |       visibility: visible; | ||||||
|     transform-origin: top center; |       transform-origin: top center; | ||||||
|     animation: scaleZ 200ms calc(50ms * #{$i}) ease-in forwards; |       animation: scaleZ 200ms calc(50ms * #{$i}) ease-in forwards; | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @keyframes scaleZ { |  | ||||||
|   0% { |  | ||||||
|     opacity: 0; |  | ||||||
|     transform: scale(0); |  | ||||||
|   } |  | ||||||
|   80% { |  | ||||||
|     transform: scale(1.07); |  | ||||||
|   } |  | ||||||
|   100% { |  | ||||||
|     opacity: 1; |  | ||||||
|     transform: scale(1); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .dropdown { |  | ||||||
|   top: var(--header-size); |  | ||||||
|   position: relative; |  | ||||||
|   height: 100%; |  | ||||||
|   width: 100%; |  | ||||||
|   max-width: 720px; |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   flex-wrap: wrap; |  | ||||||
|   z-index: 5; |  | ||||||
|  |  | ||||||
|   margin-top: -1px; |  | ||||||
|   border-top: none; |  | ||||||
|   padding: 0; |  | ||||||
|  |  | ||||||
|   @include mobile { |  | ||||||
|     position: fixed; |  | ||||||
|     left: 0; |  | ||||||
|     max-width: 100vw; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @include tablet-min { |  | ||||||
|     top: unset; |  | ||||||
|     --gutter: 1.5rem; |  | ||||||
|     max-width: calc(100% - (2 * var(--gutter))); |  | ||||||
|     margin: -1px var(--gutter) 0 var(--gutter); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @include desktop { |  | ||||||
|     max-width: 720px; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| li.result { |  | ||||||
|   background-color: var(--background-95); |  | ||||||
|   color: var(--text-color-50); |  | ||||||
|   padding: 0.5rem 2rem; |  | ||||||
|   list-style: none; |  | ||||||
|  |  | ||||||
|   opacity: 0; |  | ||||||
|   height: 56px; |  | ||||||
|   width: 100%; |  | ||||||
|   visibility: hidden; |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   padding: 0.5rem 2rem; |  | ||||||
|  |  | ||||||
|   font-size: 1.4rem; |  | ||||||
|   text-transform: capitalize; |  | ||||||
|   list-style: none; |  | ||||||
|   cursor: pointer; |  | ||||||
|   white-space: nowrap; |  | ||||||
|  |  | ||||||
|   transition: color 0.1s ease, fill 0.4s ease; |  | ||||||
|  |  | ||||||
|   span { |  | ||||||
|     overflow-x: hidden; |  | ||||||
|     text-overflow: ellipsis; |  | ||||||
|     transition: inherit; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &.active, |  | ||||||
|   &:hover, |  | ||||||
|   &:active { |  | ||||||
|     color: var(--text-color); |  | ||||||
|     border-bottom: 2px solid var(--color-green); |  | ||||||
|  |  | ||||||
|     .type-icon { |  | ||||||
|       fill: var(--text-color); |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .type-icon { |   @keyframes scaleZ { | ||||||
|     width: 28px; |     0% { | ||||||
|     height: 28px; |       opacity: 0; | ||||||
|     margin-right: 1rem; |       transform: scale(0); | ||||||
|     transition: inherit; |     } | ||||||
|     fill: var(--text-color-50); |     80% { | ||||||
|  |       transform: scale(1.07); | ||||||
|  |     } | ||||||
|  |     100% { | ||||||
|  |       opacity: 1; | ||||||
|  |       transform: scale(1); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } |  | ||||||
|  |  | ||||||
| li.info { |   .dropdown { | ||||||
|   visibility: hidden; |     top: var(--header-size); | ||||||
|   opacity: 0; |     position: relative; | ||||||
|   display: flex; |     height: 100%; | ||||||
|   justify-content: center; |     width: 100%; | ||||||
|   padding: 0 1rem; |     max-width: 720px; | ||||||
|   color: var(--text-color-50); |     display: flex; | ||||||
|   background-color: var(--background-95); |     flex-direction: column; | ||||||
|   color: var(--text-color-50); |     flex-wrap: wrap; | ||||||
|   font-size: 0.6rem; |     z-index: 5; | ||||||
|   height: 16px; |  | ||||||
|   width: 100%; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .shut-leave-to { |     margin-top: -1px; | ||||||
|   height: 0px; |     border-top: none; | ||||||
|   transition: height 0.4s ease; |     padding: 0; | ||||||
|   flex-wrap: no-wrap; |  | ||||||
|   overflow: hidden; |     @include mobile { | ||||||
| } |       position: fixed; | ||||||
|  |       left: 0; | ||||||
|  |       max-width: 100vw; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @include tablet-min { | ||||||
|  |       top: unset; | ||||||
|  |       --gutter: 1.5rem; | ||||||
|  |       max-width: calc(100% - (2 * var(--gutter))); | ||||||
|  |       margin: -1px var(--gutter) 0 var(--gutter); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @include desktop { | ||||||
|  |       max-width: 720px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   li.result { | ||||||
|  |     background-color: var(--background-95); | ||||||
|  |     color: var(--text-color-50); | ||||||
|  |     padding: 0.5rem 2rem; | ||||||
|  |     list-style: none; | ||||||
|  |  | ||||||
|  |     opacity: 0; | ||||||
|  |     height: 56px; | ||||||
|  |     width: 100%; | ||||||
|  |     visibility: hidden; | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     padding: 0.5rem 2rem; | ||||||
|  |  | ||||||
|  |     font-size: 1.4rem; | ||||||
|  |     text-transform: capitalize; | ||||||
|  |     list-style: none; | ||||||
|  |     cursor: pointer; | ||||||
|  |     white-space: nowrap; | ||||||
|  |  | ||||||
|  |     transition: color 0.1s ease, fill 0.4s ease; | ||||||
|  |  | ||||||
|  |     span { | ||||||
|  |       overflow-x: hidden; | ||||||
|  |       text-overflow: ellipsis; | ||||||
|  |       transition: inherit; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &.active, | ||||||
|  |     &:hover, | ||||||
|  |     &:active { | ||||||
|  |       color: var(--text-color); | ||||||
|  |       border-bottom: 2px solid var(--color-green); | ||||||
|  |  | ||||||
|  |       .type-icon { | ||||||
|  |         fill: var(--text-color); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .type-icon { | ||||||
|  |       width: 28px; | ||||||
|  |       height: 28px; | ||||||
|  |       margin-right: 1rem; | ||||||
|  |       transition: inherit; | ||||||
|  |       fill: var(--text-color-50); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   li.info { | ||||||
|  |     visibility: hidden; | ||||||
|  |     opacity: 0; | ||||||
|  |     display: flex; | ||||||
|  |     justify-content: center; | ||||||
|  |     padding: 0 1rem; | ||||||
|  |     color: var(--text-color-50); | ||||||
|  |     background-color: var(--background-95); | ||||||
|  |     color: var(--text-color-50); | ||||||
|  |     font-size: 0.6rem; | ||||||
|  |     height: 16px; | ||||||
|  |     width: 100%; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .shut-leave-to { | ||||||
|  |     height: 0px; | ||||||
|  |     transition: height 0.4s ease; | ||||||
|  |     flex-wrap: no-wrap; | ||||||
|  |     overflow: hidden; | ||||||
|  |   } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,8 +1,10 @@ | |||||||
| <template> | <template> | ||||||
|   <nav> |   <nav> | ||||||
|  |     <!-- eslint-disable-next-line vuejs-accessibility/anchor-has-content --> | ||||||
|     <a v-if="isHome" class="nav__logo" href="/"> |     <a v-if="isHome" class="nav__logo" href="/"> | ||||||
|       <TmdbLogo class="logo" /> |       <TmdbLogo class="logo" /> | ||||||
|     </a> |     </a> | ||||||
|  |  | ||||||
|     <router-link v-else class="nav__logo" to="/" exact> |     <router-link v-else class="nav__logo" to="/" exact> | ||||||
|       <TmdbLogo class="logo" /> |       <TmdbLogo class="logo" /> | ||||||
|     </router-link> |     </router-link> | ||||||
| @@ -10,10 +12,10 @@ | |||||||
|     <SearchInput /> |     <SearchInput /> | ||||||
|  |  | ||||||
|     <Hamburger class="mobile-only" /> |     <Hamburger class="mobile-only" /> | ||||||
|  |  | ||||||
|     <NavigationIcon class="desktop-only" :route="profileRoute" /> |     <NavigationIcon class="desktop-only" :route="profileRoute" /> | ||||||
|  |  | ||||||
|     <div class="navigation-icons-grid mobile-only" :class="{ open: isOpen }"> |     <!-- <div class="navigation-icons-grid mobile-only" :class="{ open: isOpen }"> --> | ||||||
|  |     <div v-if="isOpen" class="navigation-icons-grid mobile-only"> | ||||||
|       <NavigationIcons> |       <NavigationIcons> | ||||||
|         <NavigationIcon :route="profileRoute" /> |         <NavigationIcon :route="profileRoute" /> | ||||||
|       </NavigationIcons> |       </NavigationIcons> | ||||||
| @@ -21,106 +23,103 @@ | |||||||
|   </nav> |   </nav> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| import { mapGetters, mapActions } from "vuex"; |   import { computed } from "vue"; | ||||||
| import TmdbLogo from "@/icons/tmdb-logo"; |   import { useStore } from "vuex"; | ||||||
| import IconProfile from "@/icons/IconProfile"; |   import { useRoute } from "vue-router"; | ||||||
| import IconProfileLock from "@/icons/IconProfileLock"; |   import SearchInput from "@/components/header/SearchInput.vue"; | ||||||
| import IconSettings from "@/icons/IconSettings"; |   import Hamburger from "@/components/ui/Hamburger.vue"; | ||||||
| import IconActivity from "@/icons/IconActivity"; |   import NavigationIcons from "@/components/header/NavigationIcons.vue"; | ||||||
| import SearchInput from "@/components/header/SearchInput"; |   import NavigationIcon from "@/components/header/NavigationIcon.vue"; | ||||||
| import NavigationIcons from "src/components/header/NavigationIcons"; |   import TmdbLogo from "@/icons/tmdb-logo.vue"; | ||||||
| import NavigationIcon from "src/components/header/NavigationIcon"; |   import IconProfile from "@/icons/IconProfile.vue"; | ||||||
| import Hamburger from "@/components/ui/Hamburger"; |   import IconProfileLock from "@/icons/IconProfileLock.vue"; | ||||||
|  |   import type INavigationIcon from "../../interfaces/INavigationIcon"; | ||||||
|  |  | ||||||
| export default { |   const route = useRoute(); | ||||||
|   components: { |   const store = useStore(); | ||||||
|     NavigationIcons, |  | ||||||
|     NavigationIcon, |   const signinNavigationIcon: INavigationIcon = { | ||||||
|     SearchInput, |     title: "Signin", | ||||||
|     TmdbLogo, |     route: "/signin", | ||||||
|     IconProfile, |     icon: IconProfileLock | ||||||
|     IconProfileLock, |   }; | ||||||
|     IconSettings, |  | ||||||
|     IconActivity, |   const profileNavigationIcon: INavigationIcon = { | ||||||
|     Hamburger |     title: "Profile", | ||||||
|   }, |     route: "/profile", | ||||||
|   computed: { |     icon: IconProfile | ||||||
|     ...mapGetters("user", ["loggedIn"]), |   }; | ||||||
|     ...mapGetters("hamburger", ["isOpen"]), |  | ||||||
|     isHome() { |   const isHome = computed(() => route.path === "/"); | ||||||
|       return this.$route.path === "/"; |   const isOpen = computed(() => store.getters["hamburger/isOpen"]); | ||||||
|     }, |   const loggedIn = computed(() => store.getters["user/loggedIn"]); | ||||||
|     profileRoute() { |  | ||||||
|       return { |   const profileRoute = computed(() => | ||||||
|         title: !this.loggedIn ? "Signin" : "Profile", |     !loggedIn.value ? signinNavigationIcon : profileNavigationIcon | ||||||
|         route: !this.loggedIn ? "/signin" : "/profile", |   ); | ||||||
|         icon: !this.loggedIn ? IconProfileLock : IconProfile |  | ||||||
|       }; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "src/scss/variables"; |   @import "src/scss/variables"; | ||||||
| @import "src/scss/media-queries"; |   @import "src/scss/media-queries"; | ||||||
|  |  | ||||||
| .spacer { |   .spacer { | ||||||
|   @include mobile-only { |     @include mobile-only { | ||||||
|  |       width: 100%; | ||||||
|  |       height: $header-size; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   nav { | ||||||
|  |     display: grid; | ||||||
|  |     grid-template-columns: var(--header-size) 1fr var(--header-size); | ||||||
|  |  | ||||||
|  |     > * { | ||||||
|  |       z-index: 10; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .nav__logo { | ||||||
|  |     overflow: hidden; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .logo { | ||||||
|  |     padding: 1rem; | ||||||
|  |     fill: var(--color-green); | ||||||
|  |     width: var(--header-size); | ||||||
|  |     height: var(--header-size); | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     justify-content: center; | ||||||
|  |     background: $background-nav-logo; | ||||||
|  |     transition: transform 0.3s ease; | ||||||
|  |  | ||||||
|  |     &:hover { | ||||||
|  |       transform: scale(1.08); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @include mobile { | ||||||
|  |       padding: 0.5rem; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .navigation-icons-grid { | ||||||
|  |     display: flex; | ||||||
|  |     flex-wrap: wrap; | ||||||
|  |     position: fixed; | ||||||
|  |     top: var(--header-size); | ||||||
|  |     left: 0; | ||||||
|     width: 100%; |     width: 100%; | ||||||
|     height: $header-size; |     background-color: $background-95; | ||||||
|   } |     visibility: hidden; | ||||||
| } |     opacity: 0; | ||||||
|  |     transition: opacity 0.4s ease; | ||||||
|  |  | ||||||
| nav { |  | ||||||
|   display: grid; |  | ||||||
|   grid-template-columns: var(--header-size) 1fr var(--header-size); |  | ||||||
|  |  | ||||||
|   > * { |  | ||||||
|     z-index: 10; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .nav__logo { |  | ||||||
|   overflow: hidden; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .logo { |  | ||||||
|   padding: 1rem; |  | ||||||
|   fill: var(--color-green); |  | ||||||
|   width: var(--header-size); |  | ||||||
|   height: var(--header-size); |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: center; |  | ||||||
|   background: $background-nav-logo; |  | ||||||
|   transition: transform 0.3s ease; |  | ||||||
|  |  | ||||||
|   &:hover { |  | ||||||
|     transform: scale(1.08); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @include mobile { |  | ||||||
|     padding: 0.5rem; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .navigation-icons-grid { |  | ||||||
|   display: flex; |  | ||||||
|   flex-wrap: wrap; |  | ||||||
|   position: fixed; |  | ||||||
|   top: var(--header-size); |  | ||||||
|   left: 0; |  | ||||||
|   width: 100%; |  | ||||||
|   background-color: $background-95; |  | ||||||
|   visibility: hidden; |  | ||||||
|   opacity: 0; |  | ||||||
|   transition: opacity 0.4s ease; |  | ||||||
|  |  | ||||||
|   &.open { |  | ||||||
|     opacity: 1; |     opacity: 1; | ||||||
|     visibility: visible; |     visibility: visible; | ||||||
|  |  | ||||||
|  |     &.open { | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,81 +1,97 @@ | |||||||
| <template> | <template> | ||||||
|   <router-link |   <router-link | ||||||
|     :to="{ path: route.route }" |     v-if="route?.requiresAuth == undefined || (route?.requiresAuth && loggedIn)" | ||||||
|     :key="route.title" |     :key="route?.title" | ||||||
|     v-if="route.requiresAuth == undefined || (route.requiresAuth && loggedIn)" |     :to="{ path: route?.route }" | ||||||
|   > |   > | ||||||
|     <li class="navigation-link" :class="{ active: route.route == active }"> |     <li | ||||||
|       <component class="navigation-icon" :is="route.icon"></component> |       class="navigation-link" | ||||||
|  |       :class="{ active: matchesActiveRoute(), 'nofill-stroke': useStroke }" | ||||||
|  |     > | ||||||
|  |       <component :is="route.icon" class="navigation-icon"></component> | ||||||
|       <span>{{ route.title }}</span> |       <span>{{ route.title }}</span> | ||||||
|     </li> |     </li> | ||||||
|   </router-link> |   </router-link> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| import { mapGetters, mapActions } from "vuex"; |   import { useStore } from "vuex"; | ||||||
|  |   import { computed, defineProps } from "vue"; | ||||||
|  |   import type INavigationIcon from "../../interfaces/INavigationIcon"; | ||||||
|  |  | ||||||
| export default { |   interface Props { | ||||||
|   name: "NavigationIcon", |     route: INavigationIcon; | ||||||
|   props: { |     active?: string; | ||||||
|     active: { |     useStroke?: boolean; | ||||||
|       type: String, |   } | ||||||
|       required: false |  | ||||||
|     }, |   const props = defineProps<Props>(); | ||||||
|     route: { |  | ||||||
|       type: Object, |   const store = useStore(); | ||||||
|       required: true |   const loggedIn = computed(() => store.getters["user/loggedIn"]); | ||||||
|     } |  | ||||||
|   }, |   function matchesActiveRoute() { | ||||||
|   computed: { |     const currentRoute = props.route.title.toLowerCase(); | ||||||
|     ...mapGetters("user", ["loggedIn"]) |     return props?.active?.includes(currentRoute); | ||||||
|   } |   } | ||||||
| }; |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "src/scss/media-queries"; |   @import "src/scss/media-queries"; | ||||||
|  |  | ||||||
| .navigation-link { |   .navigation-link { | ||||||
|   display: grid; |     display: grid; | ||||||
|   place-items: center; |     place-items: center; | ||||||
|   min-height: var(--header-size); |     min-height: var(--header-size); | ||||||
|   list-style: none; |     list-style: none; | ||||||
|   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: transform 0.3s ease, color 0.3s ease, stoke 0.3s ease, |     transition: transform 0.3s ease, color 0.3s ease, stoke 0.3s ease, | ||||||
|     fill 0.3s ease, background-color 0.5s ease; |       fill 0.3s ease, background-color 0.5s ease; | ||||||
|  |  | ||||||
|   &:hover { |     &:hover { | ||||||
|     transform: scale(1.05); |       transform: scale(1.05); | ||||||
|   } |     } | ||||||
|  |  | ||||||
|   &:hover, |     &:hover, | ||||||
|   &.active { |     &.active { | ||||||
|     background-color: var(--background-color); |       background-color: var(--background-color); | ||||||
|  |  | ||||||
|     span, |       span, | ||||||
|     .navigation-icon { |       .navigation-icon { | ||||||
|       color: var(--text-color); |         color: var(--text-color); | ||||||
|       fill: var(--text-color); |         fill: var(--text-color); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     span { | ||||||
|  |       text-transform: uppercase; | ||||||
|  |       font-size: 11px; | ||||||
|  |       margin-top: 0.25rem; | ||||||
|  |       color: var(--text-color-70); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &.nofill-stroke { | ||||||
|  |       .navigation-icon { | ||||||
|  |         stroke: var(--text-color-70); | ||||||
|  |         fill: none !important; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       &:hover .navigation-icon, | ||||||
|  |       &.active .navigation-icon { | ||||||
|  |         stroke: var(--text-color); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   span { |   a { | ||||||
|     text-transform: uppercase; |     text-decoration: none; | ||||||
|     font-size: 11px; |  | ||||||
|     margin-top: 0.25rem; |  | ||||||
|     color: var(--text-color-70); |  | ||||||
|   } |   } | ||||||
| } |  | ||||||
|  |  | ||||||
| a { |   .navigation-icon { | ||||||
|   text-decoration: none; |     width: 28px; | ||||||
| } |     fill: var(--text-color-70); | ||||||
|  |     transition: inherit; | ||||||
| .navigation-icon { |   } | ||||||
|   width: 28px; |  | ||||||
|   fill: var(--text-color-70); |  | ||||||
|   transition: inherit; |  | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,103 +1,99 @@ | |||||||
| <template> | <template> | ||||||
|   <ul class="navigation-icons"> |   <ul class="navigation-icons"> | ||||||
|     <NavigationIcon |     <NavigationIcon | ||||||
|       v-for="route in routes" |       v-for="_route in routes" | ||||||
|       :key="route.route" |       :key="_route.route" | ||||||
|       :route="route" |       :route="_route" | ||||||
|       :active="activeRoute" |       :active="activeRoute" | ||||||
|  |       :useStroke="_route?.useStroke" | ||||||
|     /> |     /> | ||||||
|     <slot></slot> |     <slot></slot> | ||||||
|   </ul> |   </ul> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| import NavigationIcon from "@/components/header/NavigationIcon"; |   import { ref, watch } from "vue"; | ||||||
| import IconInbox from "@/icons/IconInbox"; |   import { useRoute } from "vue-router"; | ||||||
| import IconNowPlaying from "@/icons/IconNowPlaying"; |   import NavigationIcon from "@/components/header/NavigationIcon.vue"; | ||||||
| import IconPopular from "@/icons/IconPopular"; |   import IconInbox from "@/icons/IconInbox.vue"; | ||||||
| import IconUpcoming from "@/icons/IconUpcoming"; |   import IconNowPlaying from "@/icons/IconNowPlaying.vue"; | ||||||
| import IconSettings from "@/icons/IconSettings"; |   import IconPopular from "@/icons/IconPopular.vue"; | ||||||
| import IconActivity from "@/icons/IconActivity"; |   import IconUpcoming from "@/icons/IconUpcoming.vue"; | ||||||
|  |   import IconSettings from "@/icons/IconSettings.vue"; | ||||||
|  |   import IconActivity from "@/icons/IconActivity.vue"; | ||||||
|  |   import IconBinoculars from "@/icons/IconBinoculars.vue"; | ||||||
|  |   import type INavigationIcon from "../../interfaces/INavigationIcon"; | ||||||
|  |  | ||||||
| export default { |   const route = useRoute(); | ||||||
|   name: "NavigationIcons", |   const activeRoute = ref(window?.location?.pathname); | ||||||
|   components: { |   const routes: INavigationIcon[] = [ | ||||||
|     NavigationIcon, |     { | ||||||
|     IconInbox, |       title: "Requests", | ||||||
|     IconPopular, |       route: "/list/requests", | ||||||
|     IconNowPlaying, |       icon: IconInbox | ||||||
|     IconUpcoming, |     }, | ||||||
|     IconSettings, |     { | ||||||
|     IconActivity |       title: "Now Playing", | ||||||
|   }, |       route: "/list/now_playing", | ||||||
|   data() { |       icon: IconNowPlaying | ||||||
|     return { |     }, | ||||||
|       routes: [ |     { | ||||||
|         { |       title: "Popular", | ||||||
|           title: "Requests", |       route: "/list/popular", | ||||||
|           route: "/list/requests", |       icon: IconPopular | ||||||
|           icon: IconInbox |     }, | ||||||
|         }, |     { | ||||||
|         { |       title: "Upcoming", | ||||||
|           title: "Now Playing", |       route: "/list/upcoming", | ||||||
|           route: "/list/now_playing", |       icon: IconUpcoming | ||||||
|           icon: IconNowPlaying |     }, | ||||||
|         }, |     { | ||||||
|         { |       title: "Activity", | ||||||
|           title: "Popular", |       route: "/activity", | ||||||
|           route: "/list/popular", |       requiresAuth: true, | ||||||
|           icon: IconPopular |       useStroke: true, | ||||||
|         }, |       icon: IconActivity | ||||||
|         { |     }, | ||||||
|           title: "Upcoming", |     { | ||||||
|           route: "/list/upcoming", |       title: "Torrents", | ||||||
|           icon: IconUpcoming |       route: "/torrents", | ||||||
|         }, |       requiresAuth: true, | ||||||
|         { |       icon: IconBinoculars | ||||||
|           title: "Activity", |     }, | ||||||
|           route: "/activity", |     { | ||||||
|           requiresAuth: true, |       title: "Settings", | ||||||
|           icon: IconActivity |       route: "/settings", | ||||||
|         }, |       requiresAuth: true, | ||||||
|         { |       useStroke: true, | ||||||
|           title: "Settings", |       icon: IconSettings | ||||||
|           route: "/profile?settings=true", |  | ||||||
|           requiresAuth: true, |  | ||||||
|           icon: IconSettings |  | ||||||
|         } |  | ||||||
|       ], |  | ||||||
|       activeRoute: null |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|   watch: { |  | ||||||
|     $route() { |  | ||||||
|       this.activeRoute = window.location.pathname; |  | ||||||
|     } |     } | ||||||
|   }, |   ]; | ||||||
|   created() { |  | ||||||
|     this.activeRoute = window.location.pathname; |   function setActiveRoute(_route: string) { | ||||||
|  |     activeRoute.value = _route; | ||||||
|   } |   } | ||||||
| }; |  | ||||||
|  |   watch(route, () => setActiveRoute(window?.location?.pathname || "")); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "src/scss/media-queries"; |   @import "src/scss/media-queries"; | ||||||
|  |  | ||||||
| .navigation-icons { |   .navigation-icons { | ||||||
|   display: grid; |     display: grid; | ||||||
|   grid-column: 1fr; |     grid-column: 1fr; | ||||||
|   padding-left: 0; |     padding-left: 0; | ||||||
|   margin: 0; |     margin: 0; | ||||||
|   background-color: var(--background-color-secondary); |     background-color: var(--background-color-secondary); | ||||||
|   z-index: 15; |     z-index: 15; | ||||||
|   width: 100%; |     width: 100%; | ||||||
|  |  | ||||||
|   @include desktop { |     @include desktop { | ||||||
|     grid-template-rows: var(--header-size); |       grid-template-rows: var(--header-size); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @include mobile { | ||||||
|  |       grid-template-columns: 1fr 1fr; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @include mobile { |  | ||||||
|     grid-template-columns: 1fr 1fr; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,281 +1,290 @@ | |||||||
| <template> | <template> | ||||||
|   <div> |   <div> | ||||||
|     <div class="search" :class="{ active: focusingInput }"> |     <div class="search" :class="{ active: inputIsActive }"> | ||||||
|       <IconSearch class="search-icon" tabindex="-1" /> |       <IconSearch class="search-icon" tabindex="-1" /> | ||||||
|  |  | ||||||
|  |       <!-- eslint-disable-next-line vuejs-accessibility/form-control-has-label --> | ||||||
|       <input |       <input | ||||||
|         ref="input" |         ref="inputElement" | ||||||
|  |         v-model="query" | ||||||
|         type="text" |         type="text" | ||||||
|         placeholder="Search for movie or show" |         placeholder="Search for movie or show" | ||||||
|         aria-label="Search input for finding a movie or show" |         aria-label="Search input for finding a movie or show" | ||||||
|         autocorrect="off" |         autocorrect="off" | ||||||
|         autocapitalize="off" |         autocapitalize="off" | ||||||
|         tabindex="0" |         tabindex="0" | ||||||
|         v-model="query" |  | ||||||
|         @input="handleInput" |         @input="handleInput" | ||||||
|         @click="focusingInput = true" |         @click="focus" | ||||||
|         @keydown.escape="handleEscape" |         @keydown.escape="handleEscape" | ||||||
|         @keyup.enter="handleSubmit" |         @keyup.enter="handleSubmit" | ||||||
|         @keydown.up="navigateUp" |         @keydown.up="navigateUp" | ||||||
|         @keydown.down="navigateDown" |         @keydown.down="navigateDown" | ||||||
|         @focus="focusingInput = true" |         @focus="focus" | ||||||
|         @blur="focusingInput = false" |         @blur="blur" | ||||||
|       /> |       /> | ||||||
|  |  | ||||||
|       <IconClose |       <IconClose | ||||||
|  |         v-if="query && query.length" | ||||||
|         tabindex="0" |         tabindex="0" | ||||||
|         aria-label="button" |         aria-label="button" | ||||||
|         v-if="query && query.length" |  | ||||||
|         @click="resetQuery" |  | ||||||
|         @keydown.enter.stop="resetQuery" |  | ||||||
|         class="close-icon" |         class="close-icon" | ||||||
|  |         @click="clearInput" | ||||||
|  |         @keydown.enter.stop="clearInput" | ||||||
|       /> |       /> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <AutocompleteDropdown |     <AutocompleteDropdown | ||||||
|       v-if="showAutocompleteResults" |       v-if="showAutocompleteResults" | ||||||
|  |       v-model:results="dropdownResults" | ||||||
|       :query="query" |       :query="query" | ||||||
|       :index="dropdownIndex" |       :index="dropdownIndex" | ||||||
|       :results.sync="dropdownResults" |  | ||||||
|     /> |     /> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| import { mapActions, mapGetters } from "vuex"; |   import { ref, computed } from "vue"; | ||||||
| import SeasonedButton from "@/components/ui/SeasonedButton"; |   import { useStore } from "vuex"; | ||||||
| import AutocompleteDropdown from "@/components/header/AutocompleteDropdown"; |   import { useRouter, useRoute } from "vue-router"; | ||||||
|  |   import AutocompleteDropdown from "@/components/header/AutocompleteDropdown.vue"; | ||||||
|  |   import IconSearch from "@/icons/IconSearch.vue"; | ||||||
|  |   import IconClose from "@/icons/IconClose.vue"; | ||||||
|  |   import type { Ref } from "vue"; | ||||||
|  |   import type { MediaTypes } from "../../interfaces/IList"; | ||||||
|  |  | ||||||
| import IconSearch from "src/icons/IconSearch"; |   interface ISearchResult { | ||||||
| import IconClose from "src/icons/IconClose"; |     title: string; | ||||||
| import config from "@/config"; |     id: number; | ||||||
|  |     adult: boolean; | ||||||
|  |     type: MediaTypes; | ||||||
|  |   } | ||||||
|  |  | ||||||
| export default { |   const store = useStore(); | ||||||
|   name: "SearchInput", |   const router = useRouter(); | ||||||
|   components: { |   const route = useRoute(); | ||||||
|     SeasonedButton, |  | ||||||
|     AutocompleteDropdown, |  | ||||||
|     IconClose, |  | ||||||
|     IconSearch |  | ||||||
|   }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       query: null, |  | ||||||
|       disabled: false, |  | ||||||
|       dropdownIndex: -1, |  | ||||||
|       dropdownResults: [], |  | ||||||
|       focusingInput: false, |  | ||||||
|       showAutocomplete: false |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|   computed: { |  | ||||||
|     ...mapGetters("popup", ["isOpen"]), |  | ||||||
|     showAutocompleteResults() { |  | ||||||
|       return ( |  | ||||||
|         !this.disabled && |  | ||||||
|         this.focusingInput && |  | ||||||
|         this.query && |  | ||||||
|         this.query.length > 0 |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   created() { |  | ||||||
|     const params = new URLSearchParams(window.location.search); |  | ||||||
|     if (params && params.has("query")) { |  | ||||||
|       this.query = decodeURIComponent(params.get("query")); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const elasticUrl = config.ELASTIC_URL; |   const query: Ref<string> = ref(null); | ||||||
|     if (elasticUrl === undefined || elasticUrl === false || elasticUrl === "") { |   const disabled: Ref<boolean> = ref(false); | ||||||
|       this.disabled = true; |   const dropdownIndex: Ref<number> = ref(-1); | ||||||
|     } |   const dropdownResults: Ref<ISearchResult[]> = ref([]); | ||||||
|   }, |   const inputIsActive: Ref<boolean> = ref(false); | ||||||
|   methods: { |   const inputElement: Ref<HTMLInputElement> = ref(null); | ||||||
|     ...mapActions("popup", ["open"]), |  | ||||||
|     navigateDown() { |  | ||||||
|       if (this.dropdownIndex < this.dropdownResults.length - 1) { |  | ||||||
|         this.dropdownIndex++; |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     navigateUp() { |  | ||||||
|       if (this.dropdownIndex > -1) this.dropdownIndex--; |  | ||||||
|  |  | ||||||
|       const input = this.$refs.input; |   const isOpen = computed(() => store.getters["popup/isOpen"]); | ||||||
|       const textLength = input.value.length; |   const showAutocompleteResults = computed(() => { | ||||||
|  |     return ( | ||||||
|  |       !disabled.value && | ||||||
|  |       inputIsActive.value && | ||||||
|  |       query.value && | ||||||
|  |       query.value.length > 0 | ||||||
|  |     ); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|       setTimeout(() => { |   const params = new URLSearchParams(window.location.search); | ||||||
|         input.focus(); |   if (params && params.has("query")) { | ||||||
|         input.setSelectionRange(textLength, textLength + 1); |     query.value = decodeURIComponent(params.get("query")); | ||||||
|       }, 1); |   } | ||||||
|     }, |  | ||||||
|     search() { |  | ||||||
|       const encodedQuery = encodeURI(this.query.replace('/ /g, "+"')); |  | ||||||
|  |  | ||||||
|       this.$router.push({ |   const { ELASTIC } = process.env; | ||||||
|         name: "search", |   if (ELASTIC === undefined || ELASTIC === "") { | ||||||
|         query: { |     disabled.value = true; | ||||||
|           ...this.$route.query, |   } | ||||||
|           query: encodedQuery |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     }, |  | ||||||
|     resetQuery(event) { |  | ||||||
|       this.query = ""; |  | ||||||
|       this.$refs.input.focus(); |  | ||||||
|     }, |  | ||||||
|     handleInput(e) { |  | ||||||
|       this.$emit("input", this.query); |  | ||||||
|       this.dropdownIndex = -1; |  | ||||||
|     }, |  | ||||||
|     handleSubmit() { |  | ||||||
|       if (!this.query || this.query.length == 0) return; |  | ||||||
|  |  | ||||||
|       if (this.dropdownIndex >= 0) { |   function navigateDown() { | ||||||
|         const resultItem = this.dropdownResults[this.dropdownIndex]; |     if (dropdownIndex.value < dropdownResults.value.length - 1) { | ||||||
|  |       dropdownIndex.value += 1; | ||||||
|         console.log("resultItem:", resultItem); |  | ||||||
|         this.open({ |  | ||||||
|           id: resultItem.id, |  | ||||||
|           type: resultItem.type |  | ||||||
|         }); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       this.search(); |  | ||||||
|       this.$refs.input.blur(); |  | ||||||
|       this.dropdownIndex = -1; |  | ||||||
|     }, |  | ||||||
|     handleEscape() { |  | ||||||
|       if (!this.isOpen) { |  | ||||||
|         this.$refs.input.blur(); |  | ||||||
|         this.dropdownIndex = -1; |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| }; |  | ||||||
|  |   function navigateUp() { | ||||||
|  |     if (dropdownIndex.value > -1) dropdownIndex.value -= 1; | ||||||
|  |  | ||||||
|  |     const textLength = inputElement.value.value.length; | ||||||
|  |  | ||||||
|  |     setTimeout(() => { | ||||||
|  |       inputElement.value.focus(); | ||||||
|  |       inputElement.value.setSelectionRange(textLength, textLength + 1); | ||||||
|  |     }, 1); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function search() { | ||||||
|  |     const encodedQuery = encodeURI(query.value.replace("/ /g", "+")); | ||||||
|  |  | ||||||
|  |     router.push({ | ||||||
|  |       name: "search", | ||||||
|  |       query: { | ||||||
|  |         ...route.query, | ||||||
|  |         query: encodedQuery | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function handleInput() { | ||||||
|  |     dropdownIndex.value = -1; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function focus() { | ||||||
|  |     inputIsActive.value = true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function reset() { | ||||||
|  |     inputElement.value.blur(); | ||||||
|  |     dropdownIndex.value = -1; | ||||||
|  |     inputIsActive.value = false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function blur() { | ||||||
|  |     return setTimeout(reset, 150); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function clearInput() { | ||||||
|  |     query.value = ""; | ||||||
|  |     inputElement.value.focus(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function handleSubmit() { | ||||||
|  |     if (!query.value || query.value.length === 0) return; | ||||||
|  |  | ||||||
|  |     if (dropdownIndex.value >= 0) { | ||||||
|  |       const resultItem = dropdownResults.value[dropdownIndex.value]; | ||||||
|  |  | ||||||
|  |       store.dispatch("popup/open", { | ||||||
|  |         id: resultItem?.id, | ||||||
|  |         type: resultItem?.type | ||||||
|  |       }); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     search(); | ||||||
|  |     reset(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function handleEscape() { | ||||||
|  |     if (!isOpen.value) reset(); | ||||||
|  |   } | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "src/scss/variables"; |   @import "src/scss/variables"; | ||||||
| @import "src/scss/media-queries"; |   @import "src/scss/media-queries"; | ||||||
| @import "src/scss/main"; |   @import "src/scss/main"; | ||||||
|  |  | ||||||
| .close-icon { |   .close-icon { | ||||||
|   position: absolute; |     position: absolute; | ||||||
|   top: calc(50% - 12px); |     top: calc(50% - 12px); | ||||||
|   right: 0; |     right: 0; | ||||||
|   cursor: pointer; |     cursor: pointer; | ||||||
|   fill: var(--text-color); |     fill: var(--text-color); | ||||||
|   height: 24px; |     height: 24px; | ||||||
|   width: 24px; |     width: 24px; | ||||||
|  |  | ||||||
|   @include tablet-min { |     @include tablet-min { | ||||||
|     right: 6px; |       right: 6px; | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .filter { |  | ||||||
|   width: 100%; |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   margin: 1rem 2rem; |  | ||||||
|  |  | ||||||
|   h2 { |  | ||||||
|     margin-top: 0.5rem; |  | ||||||
|     margin-bottom: 0.5rem; |  | ||||||
|     font-weight: 400; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &-items { |  | ||||||
|     display: flex; |  | ||||||
|     flex-direction: row; |  | ||||||
|     align-items: center; |  | ||||||
|  |  | ||||||
|     > :not(:first-child) { |  | ||||||
|       margin-left: 1rem; |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } |  | ||||||
|  |  | ||||||
| hr { |   .filter { | ||||||
|   display: block; |  | ||||||
|   height: 1px; |  | ||||||
|   border: 0; |  | ||||||
|   border-bottom: 1px solid $text-color-50; |  | ||||||
|   margin-top: 10px; |  | ||||||
|   margin-bottom: 10px; |  | ||||||
|   width: 90%; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .search.active { |  | ||||||
|   input { |  | ||||||
|     border-color: var(--color-green); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .search-icon { |  | ||||||
|     fill: var(--color-green); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .search { |  | ||||||
|   height: $header-size; |  | ||||||
|   display: flex; |  | ||||||
|   position: fixed; |  | ||||||
|   flex-wrap: wrap; |  | ||||||
|   z-index: 5; |  | ||||||
|   border: 0; |  | ||||||
|   background-color: $background-color-secondary; |  | ||||||
|  |  | ||||||
|   // TODO check if this is for mobile |  | ||||||
|   width: calc(100% - 110px); |  | ||||||
|   top: 0; |  | ||||||
|   right: 55px; |  | ||||||
|  |  | ||||||
|   @include tablet-min { |  | ||||||
|     position: relative; |  | ||||||
|     width: 100%; |     width: 100%; | ||||||
|     right: 0px; |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  |     margin: 1rem 2rem; | ||||||
|  |  | ||||||
|  |     h2 { | ||||||
|  |       margin-top: 0.5rem; | ||||||
|  |       margin-bottom: 0.5rem; | ||||||
|  |       font-weight: 400; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &-items { | ||||||
|  |       display: flex; | ||||||
|  |       flex-direction: row; | ||||||
|  |       align-items: center; | ||||||
|  |  | ||||||
|  |       > :not(:first-child) { | ||||||
|  |         margin-left: 1rem; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   input { |   hr { | ||||||
|     display: block; |     display: block; | ||||||
|     width: 100%; |     height: 1px; | ||||||
|     padding: 13px 28px 13px 45px; |  | ||||||
|     outline: none; |  | ||||||
|     margin: 0; |  | ||||||
|     border: 0; |     border: 0; | ||||||
|     background-color: $background-color-secondary; |     border-bottom: 1px solid $text-color-50; | ||||||
|     font-weight: 300; |     margin-top: 10px; | ||||||
|     font-size: 18px; |     margin-bottom: 10px; | ||||||
|     color: $text-color; |     width: 90%; | ||||||
|     border-bottom: 1px solid transparent; |   } | ||||||
|  |  | ||||||
|     &:focus { |   .search.active { | ||||||
|       // border-bottom: 1px solid var(--color-green); |     input { | ||||||
|       border-color: var(--color-green); |       border-color: var(--color-green); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @include tablet-min { |     .search-icon { | ||||||
|       font-size: 24px; |       fill: var(--color-green); | ||||||
|       padding: 13px 40px 13px 60px; |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &-icon { |   .search { | ||||||
|     width: 20px; |     height: $header-size; | ||||||
|     height: 20px; |     display: flex; | ||||||
|     fill: var(--text-color-50); |     position: fixed; | ||||||
|     pointer-events: none; |     flex-wrap: wrap; | ||||||
|     position: absolute; |     z-index: 5; | ||||||
|     left: 15px; |     border: 0; | ||||||
|     top: calc(50% - 10px); |     background-color: $background-color-secondary; | ||||||
|  |  | ||||||
|  |     // TODO check if this is for mobile | ||||||
|  |     width: calc(100% - 110px); | ||||||
|  |     top: 0; | ||||||
|  |     right: 55px; | ||||||
|  |  | ||||||
|     @include tablet-min { |     @include tablet-min { | ||||||
|       width: 24px; |       position: relative; | ||||||
|       height: 24px; |       width: 100%; | ||||||
|       top: calc(50% - 12px); |       right: 0px; | ||||||
|       left: 22px; |     } | ||||||
|  |  | ||||||
|  |     input { | ||||||
|  |       display: block; | ||||||
|  |       width: 100%; | ||||||
|  |       padding: 13px 28px 13px 45px; | ||||||
|  |       outline: none; | ||||||
|  |       margin: 0; | ||||||
|  |       border: 0; | ||||||
|  |       background-color: $background-color-secondary; | ||||||
|  |       font-weight: 300; | ||||||
|  |       font-size: 18px; | ||||||
|  |       color: $text-color; | ||||||
|  |       border-bottom: 1px solid transparent; | ||||||
|  |  | ||||||
|  |       &:focus { | ||||||
|  |         // border-bottom: 1px solid var(--color-green); | ||||||
|  |         border-color: var(--color-green); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @include tablet-min { | ||||||
|  |         font-size: 24px; | ||||||
|  |         padding: 13px 40px 13px 60px; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &-icon { | ||||||
|  |       width: 20px; | ||||||
|  |       height: 20px; | ||||||
|  |       fill: var(--text-color-50); | ||||||
|  |       pointer-events: none; | ||||||
|  |       position: absolute; | ||||||
|  |       left: 15px; | ||||||
|  |       top: calc(50% - 10px); | ||||||
|  |  | ||||||
|  |       @include tablet-min { | ||||||
|  |         width: 24px; | ||||||
|  |         height: 24px; | ||||||
|  |         top: calc(50% - 12px); | ||||||
|  |         left: 22px; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,82 +1,86 @@ | |||||||
| <template> | <template> | ||||||
|   <li |   <li | ||||||
|     class="sidebar-list-element" |     class="sidebar-list-element" | ||||||
|     @click="event => $emit('click', event)" |  | ||||||
|     :class="{ active, disabled }" |     :class="{ active, disabled }" | ||||||
|  |     @click="emit('click')" | ||||||
|  |     @keydown.enter="emit('click')" | ||||||
|   > |   > | ||||||
|     <slot></slot> |     <slot></slot> | ||||||
|   </li> |   </li> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| export default { |   import { defineProps, defineEmits } from "vue"; | ||||||
|   props: { |  | ||||||
|     active: { |   interface Props { | ||||||
|       type: Boolean, |     active?: boolean; | ||||||
|       default: false |     disabled?: boolean; | ||||||
|     }, |  | ||||||
|     disabled: { |  | ||||||
|       type: Boolean, |  | ||||||
|       default: false |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| }; |  | ||||||
|  |   interface Emit { | ||||||
|  |     (e: "click"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const emit = defineEmits<Emit>(); | ||||||
|  |   defineProps<Props>(); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss"> | <style lang="scss"> | ||||||
| @import "src/scss/media-queries"; |   @import "src/scss/media-queries"; | ||||||
|  |  | ||||||
| li.sidebar-list-element { |   li.sidebar-list-element { | ||||||
|   display: flex; |     display: flex; | ||||||
|   align-items: center; |     align-items: center; | ||||||
|   text-decoration: none; |     text-decoration: none; | ||||||
|   text-transform: uppercase; |     text-transform: uppercase; | ||||||
|   color: var(--text-color-50); |     color: var(--text-color-50); | ||||||
|   font-size: 12px; |     font-size: 12px; | ||||||
|   padding: 10px 0; |     padding: 10px 0; | ||||||
|   border-bottom: 1px solid var(--text-color-5); |     border-bottom: 1px solid var(--text-color-5); | ||||||
|   cursor: pointer; |     cursor: pointer; | ||||||
|  |     user-select: none; | ||||||
|  |     -webkit-user-select: none; | ||||||
|  |  | ||||||
|   &:first-of-type { |     &:first-of-type { | ||||||
|     padding-top: 0; |       padding-top: 0; | ||||||
|   } |     } | ||||||
|  |  | ||||||
|   div > svg, |  | ||||||
|   svg { |  | ||||||
|     width: 26px; |  | ||||||
|     height: 26px; |  | ||||||
|     margin-right: 1rem; |  | ||||||
|     transition: all 0.3s ease; |  | ||||||
|     fill: var(--text-color-70); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &:hover, |  | ||||||
|   &.active { |  | ||||||
|     color: var(--text-color); |  | ||||||
|  |  | ||||||
|     div > svg, |     div > svg, | ||||||
|     svg { |     svg { | ||||||
|       fill: var(--text-color); |       width: 26px; | ||||||
|       transform: scale(1.1, 1.1); |       height: 26px; | ||||||
|  |       margin-right: 1rem; | ||||||
|  |       transition: all 0.3s ease; | ||||||
|  |       fill: var(--text-color-70); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &:hover, | ||||||
|  |     &.active { | ||||||
|  |       color: var(--text-color); | ||||||
|  |  | ||||||
|  |       div > svg, | ||||||
|  |       svg { | ||||||
|  |         fill: var(--text-color); | ||||||
|  |         transform: scale(1.1, 1.1); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &.active > div > svg, | ||||||
|  |     &.active > svg { | ||||||
|  |       fill: var(--color-green); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &.disabled { | ||||||
|  |       cursor: default; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .pending { | ||||||
|  |       color: #f8bd2d; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .meta { | ||||||
|  |       margin-left: auto; | ||||||
|  |       text-align: right; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &.active > div > svg, |  | ||||||
|   &.active > svg { |  | ||||||
|     fill: var(--color-green); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &.disabled { |  | ||||||
|     cursor: default; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .pending { |  | ||||||
|     color: #f8bd2d; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .meta { |  | ||||||
|     margin-left: auto; |  | ||||||
|     text-align: right; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,10 +1,11 @@ | |||||||
| <template> | <template> | ||||||
|   <div |   <div | ||||||
|     id="description" |     ref="descriptionElement" | ||||||
|     class="movie-description noselect" |     class="movie-description noselect" | ||||||
|     @click="overflow ? (truncated = !truncated) : null" |     @click="overflow ? (truncated = !truncated) : null" | ||||||
|  |     @keydown.enter="overflow ? (truncated = !truncated) : null" | ||||||
|   > |   > | ||||||
|     <span ref="description" :class="{ truncated }">{{ description }}</span> |     <span :class="{ truncated }">{{ description }}</span> | ||||||
|  |  | ||||||
|     <button v-if="description && overflow" class="truncate-toggle"> |     <button v-if="description && overflow" class="truncate-toggle"> | ||||||
|       <IconArrowDown :class="{ rotate: !truncated }" /> |       <IconArrowDown :class="{ rotate: !truncated }" /> | ||||||
| @@ -12,113 +13,115 @@ | |||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| import IconArrowDown from "../../icons/IconArrowDown"; |   import { ref, defineProps, onMounted } from "vue"; | ||||||
| export default { |   import type { Ref } from "vue"; | ||||||
|   components: { IconArrowDown }, |   import IconArrowDown from "../../icons/IconArrowDown.vue"; | ||||||
|   props: { |  | ||||||
|     description: { |  | ||||||
|       type: String, |  | ||||||
|       required: true |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       truncated: true, |  | ||||||
|       overflow: false |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|   mounted() { |  | ||||||
|     this.checkDescriptionOverflowing(); |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     checkDescriptionOverflowing() { |  | ||||||
|       const descriptionEl = document.getElementById("description"); |  | ||||||
|       if (!descriptionEl) return; |  | ||||||
|  |  | ||||||
|       const { height, width } = descriptionEl.getBoundingClientRect(); |   interface Props { | ||||||
|       const { fontSize, lineHeight } = getComputedStyle(descriptionEl); |     description: string; | ||||||
|  |  | ||||||
|       const elementWithoutOverflow = document.createElement("div"); |  | ||||||
|       elementWithoutOverflow.setAttribute( |  | ||||||
|         "style", |  | ||||||
|         `max-width: ${Math.ceil( |  | ||||||
|           width + 10 |  | ||||||
|         )}px; display: block; font-size: ${fontSize}; line-height: ${lineHeight};` |  | ||||||
|       ); |  | ||||||
|       // Don't know why need to add 10px to width, but works out perfectly |  | ||||||
|  |  | ||||||
|       elementWithoutOverflow.classList.add("dummy-non-overflow"); |  | ||||||
|       elementWithoutOverflow.innerText = this.description; |  | ||||||
|  |  | ||||||
|       document.body.appendChild(elementWithoutOverflow); |  | ||||||
|       const elemWithoutOverflowHeight = |  | ||||||
|         elementWithoutOverflow.getBoundingClientRect()["height"]; |  | ||||||
|  |  | ||||||
|       this.overflow = elemWithoutOverflowHeight > height; |  | ||||||
|       this.removeElements(document.querySelectorAll(".dummy-non-overflow")); |  | ||||||
|     }, |  | ||||||
|     removeElements: elems => elems.forEach(el => el.remove()) |  | ||||||
|   } |   } | ||||||
| }; |  | ||||||
|  |   const props = defineProps<Props>(); | ||||||
|  |   const truncated: Ref<boolean> = ref(true); | ||||||
|  |   const overflow: Ref<boolean> = ref(false); | ||||||
|  |   const descriptionElement: Ref<HTMLElement> = ref(null); | ||||||
|  |  | ||||||
|  |   // eslint-disable-next-line no-undef | ||||||
|  |   function removeElements(elems: NodeListOf<Element>) { | ||||||
|  |     elems.forEach(el => el.remove()); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // The description element overflows text after 4 rows with css | ||||||
|  |   // line-clamp this takes the same text and adds to a temporary | ||||||
|  |   // element without css overflow. If the temp element is | ||||||
|  |   // higher then description element, we display expand button | ||||||
|  |   function checkDescriptionOverflowing() { | ||||||
|  |     const element = descriptionElement?.value; | ||||||
|  |     if (!element) return; | ||||||
|  |  | ||||||
|  |     const { height, width } = element.getBoundingClientRect(); | ||||||
|  |     const { fontSize, lineHeight } = getComputedStyle(element); | ||||||
|  |  | ||||||
|  |     const descriptionComparisonElement = document.createElement("div"); | ||||||
|  |     descriptionComparisonElement.setAttribute( | ||||||
|  |       "style", | ||||||
|  |       `max-width: ${Math.ceil( | ||||||
|  |         width + 10 | ||||||
|  |       )}px; display: block; font-size: ${fontSize}; line-height: ${lineHeight};` | ||||||
|  |     ); | ||||||
|  |     // Don't know why need to add 10px to width, but works out perfectly | ||||||
|  |  | ||||||
|  |     descriptionComparisonElement.classList.add("dummy-non-overflow"); | ||||||
|  |     descriptionComparisonElement.innerText = props.description; | ||||||
|  |  | ||||||
|  |     document.body.appendChild(descriptionComparisonElement); | ||||||
|  |     const elemWithoutOverflowHeight = | ||||||
|  |       descriptionComparisonElement.getBoundingClientRect().height; | ||||||
|  |  | ||||||
|  |     overflow.value = elemWithoutOverflowHeight > height; | ||||||
|  |     removeElements(document.querySelectorAll(".dummy-non-overflow")); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   onMounted(checkDescriptionOverflowing); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "src/scss/media-queries"; |   @import "src/scss/media-queries"; | ||||||
|  |  | ||||||
| .movie-description { |   .movie-description { | ||||||
|   font-weight: 300; |     font-weight: 300; | ||||||
|   font-size: 13px; |     font-size: 13px; | ||||||
|   line-height: 1.8; |     line-height: 1.8; | ||||||
|   margin-bottom: 20px; |     margin-bottom: 20px; | ||||||
|   transition: all 1s ease; |     transition: all 1s ease; | ||||||
|  |  | ||||||
|   @include tablet-min { |     @include tablet-min { | ||||||
|     margin-bottom: 30px; |       margin-bottom: 30px; | ||||||
|     font-size: 14px; |       font-size: 14px; | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| span.truncated { |  | ||||||
|   display: -webkit-box; |  | ||||||
|   overflow: hidden; |  | ||||||
|   -webkit-line-clamp: 4; |  | ||||||
|   -webkit-box-orient: vertical; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .truncate-toggle { |  | ||||||
|   border: none; |  | ||||||
|   background: none; |  | ||||||
|   width: 100%; |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   text-align: center; |  | ||||||
|   color: var(--text-color); |  | ||||||
|   margin-top: 1rem; |  | ||||||
|   cursor: pointer; |  | ||||||
|  |  | ||||||
|   svg { |  | ||||||
|     transition: 0.4s ease all; |  | ||||||
|     height: 22px; |  | ||||||
|     width: 22px; |  | ||||||
|     fill: var(--text-color); |  | ||||||
|  |  | ||||||
|     &.rotate { |  | ||||||
|       transform: rotateX(180deg); |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &::before, |   span.truncated { | ||||||
|   &::after { |     display: -webkit-box; | ||||||
|     content: ""; |     overflow: hidden; | ||||||
|     flex: 1; |     -webkit-line-clamp: 4; | ||||||
|     border-bottom: 1px solid var(--text-color-50); |     -webkit-box-orient: vertical; | ||||||
|   } |   } | ||||||
|   &::before { |  | ||||||
|     margin-right: 1rem; |   .truncate-toggle { | ||||||
|  |     border: none; | ||||||
|  |     background: none; | ||||||
|  |     width: 100%; | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     text-align: center; | ||||||
|  |     color: var(--text-color); | ||||||
|  |     margin-top: 1rem; | ||||||
|  |     cursor: pointer; | ||||||
|  |  | ||||||
|  |     svg { | ||||||
|  |       transition: 0.4s ease all; | ||||||
|  |       height: 22px; | ||||||
|  |       width: 22px; | ||||||
|  |       fill: var(--text-color); | ||||||
|  |  | ||||||
|  |       &.rotate { | ||||||
|  |         transform: rotateX(180deg); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &::before, | ||||||
|  |     &::after { | ||||||
|  |       content: ""; | ||||||
|  |       flex: 1; | ||||||
|  |       border-bottom: 1px solid var(--text-color-50); | ||||||
|  |     } | ||||||
|  |     &::before { | ||||||
|  |       margin-right: 1rem; | ||||||
|  |     } | ||||||
|  |     &::after { | ||||||
|  |       margin-left: 1rem; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|   &::after { |  | ||||||
|     margin-left: 1rem; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -7,52 +7,48 @@ | |||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| export default { |   import { defineProps } from "vue"; | ||||||
|   props: { |  | ||||||
|     title: { |   interface Props { | ||||||
|       type: String, |     title: string; | ||||||
|       required: true |     detail?: string | number; | ||||||
|     }, |  | ||||||
|     detail: { |  | ||||||
|       required: false, |  | ||||||
|       default: null |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| }; |  | ||||||
|  |   defineProps<Props>(); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "src/scss/media-queries"; |   @import "src/scss/media-queries"; | ||||||
|  |  | ||||||
| .movie-detail { |   .movie-detail { | ||||||
|   margin-bottom: 20px; |     margin-bottom: 20px; | ||||||
|  |  | ||||||
|   &:last-of-type { |     &:last-of-type { | ||||||
|     margin-bottom: 0px; |       margin-bottom: 0px; | ||||||
|   } |     } | ||||||
|  |  | ||||||
|   @include tablet-min { |     @include tablet-min { | ||||||
|     margin-bottom: 30px; |       margin-bottom: 30px; | ||||||
|   } |     } | ||||||
|  |  | ||||||
|   h2.title { |     h2.title { | ||||||
|     margin: 0; |       margin: 0; | ||||||
|     font-weight: 400; |       font-weight: 400; | ||||||
|     text-transform: uppercase; |       text-transform: uppercase; | ||||||
|     font-size: 1.2rem; |       font-size: 1.2rem; | ||||||
|     color: var(--color-green); |       color: var(--color-green); | ||||||
|  |  | ||||||
|     @include mobile { |       @include mobile { | ||||||
|       font-size: 1.1rem; |         font-size: 1.1rem; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     span.info { | ||||||
|  |       font-weight: 300; | ||||||
|  |       font-size: 1rem; | ||||||
|  |       letter-spacing: 0.8px; | ||||||
|  |       margin-top: 5px; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   span.info { |  | ||||||
|     font-weight: 300; |  | ||||||
|     font-size: 1rem; |  | ||||||
|     letter-spacing: 0.8px; |  | ||||||
|     margin-top: 5px; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,22 +1,24 @@ | |||||||
| <template> | <template> | ||||||
|   <section class="movie"> |   <section class="movie"> | ||||||
|     <!-- HEADER w/ POSTER --> |     <!-- HEADER w/ POSTER --> | ||||||
|  |     <!-- eslint-disable-next-line vuejs-accessibility/click-events-have-key-events --> | ||||||
|     <header |     <header | ||||||
|       ref="header" |       ref="backdropElement" | ||||||
|       :class="compact ? 'compact' : ''" |       :class="compact ? 'compact' : ''" | ||||||
|       @click="compact = !compact" |       @click="compact = !compact" | ||||||
|     > |     > | ||||||
|       <figure class="movie__poster"> |       <figure class="movie__poster"> | ||||||
|         <img |         <img | ||||||
|           class="movie-item__img is-loaded" |  | ||||||
|           ref="poster-image" |           ref="poster-image" | ||||||
|           src="/assets/placeholder.png" |           class="movie-item__img is-loaded" | ||||||
|  |           alt="Movie poster" | ||||||
|  |           :src="poster" | ||||||
|         /> |         /> | ||||||
|       </figure> |       </figure> | ||||||
|  |  | ||||||
|       <div v-if="movie" class="movie__title"> |       <div v-if="media" class="movie__title"> | ||||||
|         <h1>{{ movie.title || movie.name }}</h1> |         <h1>{{ media.title }}</h1> | ||||||
|         <i>{{ movie.tagline }}</i> |         <i>{{ media.tagline }}</i> | ||||||
|       </div> |       </div> | ||||||
|       <loading-placeholder v-else :count="2" /> |       <loading-placeholder v-else :count="2" /> | ||||||
|     </header> |     </header> | ||||||
| @@ -25,28 +27,35 @@ | |||||||
|     <div class="movie__main"> |     <div class="movie__main"> | ||||||
|       <div class="movie__wrap movie__wrap--main"> |       <div class="movie__wrap movie__wrap--main"> | ||||||
|         <!-- SIDEBAR ACTIONS --> |         <!-- SIDEBAR ACTIONS --> | ||||||
|         <div class="movie__actions" v-if="movie"> |         <div v-if="media" class="movie__actions"> | ||||||
|           <action-button :active="matched" :disabled="true"> |           <action-button :active="media?.exists_in_plex" :disabled="true"> | ||||||
|             <IconThumbsUp v-if="matched" /> |             <IconThumbsUp v-if="media?.exists_in_plex" /> | ||||||
|             <IconThumbsDown v-else /> |             <IconThumbsDown v-else /> | ||||||
|             {{ !matched ? "Not yet available" : "Already available 🎉" }} |             {{ | ||||||
|  |               !media?.exists_in_plex | ||||||
|  |                 ? "Not yet available" | ||||||
|  |                 : "Already available 🎉" | ||||||
|  |             }} | ||||||
|           </action-button> |           </action-button> | ||||||
|  |  | ||||||
|           <action-button @click="sendRequest" :active="requested"> |           <action-button :active="requested" @click="sendRequest"> | ||||||
|             <transition name="fade" mode="out-in"> |             <transition name="fade" mode="out-in"> | ||||||
|               <div v-if="!requested" key="request"><IconRequest /></div> |               <div v-if="!requested" key="request"><IconRequest /></div> | ||||||
|               <div v-else key="requested"><IconRequested /></div> |               <div v-else key="requested"><IconRequested /></div> | ||||||
|             </transition> |             </transition> | ||||||
|             {{ !requested ? `Request ${this.type}?` : "Already requested" }} |             {{ !requested ? `Request ${type}?` : "Already requested" }} | ||||||
|           </action-button> |           </action-button> | ||||||
|  |  | ||||||
|           <action-button v-if="plexId && matched" @click="openInPlex"> |           <action-button | ||||||
|  |             v-if="plexId && media?.exists_in_plex" | ||||||
|  |             @click="openInPlex" | ||||||
|  |           > | ||||||
|             <IconPlay /> |             <IconPlay /> | ||||||
|             Open and watch in plex now! |             Open and watch in plex now! | ||||||
|           </action-button> |           </action-button> | ||||||
|  |  | ||||||
|           <action-button |           <action-button | ||||||
|             v-if="credits && credits.cast && credits.cast.length" |             v-if="cast?.length" | ||||||
|             :active="showCast" |             :active="showCast" | ||||||
|             @click="() => (showCast = !showCast)" |             @click="() => (showCast = !showCast)" | ||||||
|           > |           > | ||||||
| @@ -56,8 +65,8 @@ | |||||||
|  |  | ||||||
|           <action-button |           <action-button | ||||||
|             v-if="admin === true" |             v-if="admin === true" | ||||||
|             @click="showTorrents = !showTorrents" |  | ||||||
|             :active="showTorrents" |             :active="showTorrents" | ||||||
|  |             @click="showTorrents = !showTorrents" | ||||||
|           > |           > | ||||||
|             <IconBinoculars /> |             <IconBinoculars /> | ||||||
|             Search for torrents |             Search for torrents | ||||||
| @@ -73,11 +82,11 @@ | |||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|         <!-- Loading placeholder --> |         <!-- Loading placeholder --> | ||||||
|         <div class="movie__actions text-input__loading" v-else> |         <div v-else class="movie__actions text-input__loading"> | ||||||
|           <div |           <div | ||||||
|             v-for="index in admin ? Array(4) : Array(3)" |             v-for="index in admin ? Array(4) : Array(3)" | ||||||
|             class="movie__actions-link" |  | ||||||
|             :key="index" |             :key="index" | ||||||
|  |             class="movie__actions-link" | ||||||
|           > |           > | ||||||
|             <div |             <div | ||||||
|               class="movie__actions-text text-input__loading--line" |               class="movie__actions-text text-input__loading--line" | ||||||
| @@ -94,451 +103,433 @@ | |||||||
|           </div> |           </div> | ||||||
|  |  | ||||||
|           <Description |           <Description | ||||||
|             v-if="!loading && movie && movie.overview" |             v-if="!loading && media && media.overview" | ||||||
|             :description="movie.overview" |             :description="media.overview" | ||||||
|           /> |           /> | ||||||
|  |  | ||||||
|           <div class="movie__details" v-if="movie"> |           <div v-if="media" class="movie__details"> | ||||||
|             <Detail |             <Detail | ||||||
|               v-if="movie.year" |               v-if="media.year" | ||||||
|               title="Release date" |               title="Release date" | ||||||
|               :detail="movie.year" |               :detail="media.year" | ||||||
|             /> |             /> | ||||||
|             <Detail v-if="movie.rating" title="Rating" :detail="movie.rating" /> |  | ||||||
|             <Detail |             <Detail | ||||||
|               v-if="movie.type == 'show'" |               v-if="media.type === MediaTypes.Movie && media.rating" | ||||||
|  |               title="Rating" | ||||||
|  |               :detail="media.rating" | ||||||
|  |             /> | ||||||
|  |             <Detail | ||||||
|  |               v-if="media.type == MediaTypes.Show" | ||||||
|               title="Seasons" |               title="Seasons" | ||||||
|               :detail="movie.seasons" |               :detail="media.seasons" | ||||||
|             /> |             /> | ||||||
|             <Detail |             <Detail | ||||||
|               v-if="movie.genres && movie.genres.length" |               v-if="media.genres && media.genres.length" | ||||||
|               title="Genres" |               title="Genres" | ||||||
|               :detail="movie.genres.join(', ')" |               :detail="media.genres.join(', ')" | ||||||
|             /> |             /> | ||||||
|             <Detail |             <Detail | ||||||
|               v-if=" |               v-if=" | ||||||
|                 movie.production_status && |                 media.production_status && | ||||||
|                 movie.production_status !== 'Released' |                 media.production_status !== 'Released' | ||||||
|               " |               " | ||||||
|               title="Production status" |               title="Production status" | ||||||
|               :detail="movie.production_status" |               :detail="media.production_status" | ||||||
|             /> |             /> | ||||||
|             <Detail |             <Detail | ||||||
|               v-if="movie.runtime" |               v-if="media.runtime" | ||||||
|               title="Runtime" |               title="Runtime" | ||||||
|               :detail="humanMinutes(movie.runtime)" |               :detail="humanMinutes(media.runtime)" | ||||||
|             /> |             /> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|         <!-- TODO: change this classname, this is general  --> |         <!-- TODO: change this classname, this is general  --> | ||||||
|  |  | ||||||
|         <div |         <div v-if="showCast && cast?.length" class="movie__admin"> | ||||||
|           class="movie__admin" |  | ||||||
|           v-if="showCast && credits && credits.cast && credits.cast.length" |  | ||||||
|         > |  | ||||||
|           <Detail title="cast"> |           <Detail title="cast"> | ||||||
|             <CastList :cast="credits.cast" /> |             <CastList :cast="cast" /> | ||||||
|           </Detail> |           </Detail> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <!-- TORRENT LIST --> |       <!-- TORRENT LIST --> | ||||||
|       <TorrentList |       <TorrentList | ||||||
|         v-if="movie && admin" |         v-if="media && admin && showTorrents" | ||||||
|         :show="showTorrents" |         class="torrents" | ||||||
|         :query="title" |         :query="media?.title" | ||||||
|         :tmdb_id="id" |         :tmdb-id="id" | ||||||
|         :admin="admin" |  | ||||||
|       ></TorrentList> |       ></TorrentList> | ||||||
|     </div> |     </div> | ||||||
|   </section> |   </section> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| import { mapGetters } from "vuex"; |   import { ref, computed, defineProps } from "vue"; | ||||||
| import img from "@/directives/v-image"; |   import { useStore } from "vuex"; | ||||||
| import IconProfile from "@/icons/IconProfile"; |  | ||||||
| import IconThumbsUp from "@/icons/IconThumbsUp"; |  | ||||||
| import IconThumbsDown from "@/icons/IconThumbsDown"; |  | ||||||
| import IconInfo from "@/icons/IconInfo"; |  | ||||||
| import IconRequest from "@/icons/IconRequest"; |  | ||||||
| import IconRequested from "@/icons/IconRequested"; |  | ||||||
| import IconBinoculars from "@/icons/IconBinoculars"; |  | ||||||
| import IconPlay from "@/icons/IconPlay"; |  | ||||||
| import TorrentList from "@/components/TorrentList"; |  | ||||||
| import CastList from "@/components/CastList"; |  | ||||||
| import Detail from "@/components/popup/Detail"; |  | ||||||
| import ActionButton from "@/components/popup/ActionButton"; |  | ||||||
| import Description from "@/components/popup/Description"; |  | ||||||
| import store from "@/store"; |  | ||||||
| import LoadingPlaceholder from "@/components/ui/LoadingPlaceholder"; |  | ||||||
|  |  | ||||||
| import { |   // import img from "@/directives/v-image"; | ||||||
|   getMovie, |   import IconProfile from "@/icons/IconProfile.vue"; | ||||||
|   getShow, |   import IconThumbsUp from "@/icons/IconThumbsUp.vue"; | ||||||
|   getPerson, |   import IconThumbsDown from "@/icons/IconThumbsDown.vue"; | ||||||
|   getCredits, |   import IconInfo from "@/icons/IconInfo.vue"; | ||||||
|   request, |   import IconRequest from "@/icons/IconRequest.vue"; | ||||||
|   getRequestStatus, |   import IconRequested from "@/icons/IconRequested.vue"; | ||||||
|   watchLink |   import IconBinoculars from "@/icons/IconBinoculars.vue"; | ||||||
| } from "@/api"; |   import IconPlay from "@/icons/IconPlay.vue"; | ||||||
|  |   import TorrentList from "@/components/torrent/TruncatedTorrentResults.vue"; | ||||||
|  |   import CastList from "@/components/CastList.vue"; | ||||||
|  |   import Detail from "@/components/popup/Detail.vue"; | ||||||
|  |   import ActionButton from "@/components/popup/ActionButton.vue"; | ||||||
|  |   import Description from "@/components/popup/Description.vue"; | ||||||
|  |   import LoadingPlaceholder from "@/components/ui/LoadingPlaceholder.vue"; | ||||||
|  |   import type { Ref } from "vue"; | ||||||
|  |   import type { | ||||||
|  |     IMovie, | ||||||
|  |     IShow, | ||||||
|  |     IMediaCredits, | ||||||
|  |     ICast | ||||||
|  |   } from "../../interfaces/IList"; | ||||||
|  |   import { MediaTypes } from "../../interfaces/IList"; | ||||||
|  |  | ||||||
| export default { |   import { humanMinutes } from "../../utils"; | ||||||
|   // props: ['id', 'type'], |   import { | ||||||
|   props: { |     getMovie, | ||||||
|     id: { |     getShow, | ||||||
|       required: true, |     getMovieCredits, | ||||||
|       type: Number |     getShowCredits, | ||||||
|     }, |     request, | ||||||
|     type: { |     getRequestStatus | ||||||
|       required: false, |     // watchLink | ||||||
|       type: String |   } from "../../api"; | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   components: { |  | ||||||
|     Description, |  | ||||||
|     Detail, |  | ||||||
|     ActionButton, |  | ||||||
|     IconProfile, |  | ||||||
|     IconThumbsUp, |  | ||||||
|     IconThumbsDown, |  | ||||||
|     IconRequest, |  | ||||||
|     IconRequested, |  | ||||||
|     IconInfo, |  | ||||||
|     IconBinoculars, |  | ||||||
|     IconPlay, |  | ||||||
|     TorrentList, |  | ||||||
|     CastList, |  | ||||||
|     LoadingPlaceholder |  | ||||||
|   }, |  | ||||||
|   directives: { img: img }, // TODO decide to remove or use |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       ASSET_URL: "https://image.tmdb.org/t/p/", |  | ||||||
|       ASSET_SIZES: ["w500", "w780", "original"], |  | ||||||
|       movie: undefined, |  | ||||||
|       title: undefined, |  | ||||||
|       poster: undefined, |  | ||||||
|       backdrop: undefined, |  | ||||||
|       matched: false, |  | ||||||
|       requested: false, |  | ||||||
|       showTorrents: false, |  | ||||||
|       showCast: false, |  | ||||||
|       credits: [], |  | ||||||
|       compact: false, |  | ||||||
|       loading: true |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|   watch: { |  | ||||||
|     id: function (val) { |  | ||||||
|       this.fetchByType(); |  | ||||||
|     }, |  | ||||||
|     backdrop: function (backdrop) { |  | ||||||
|       if (backdrop != null) { |  | ||||||
|         const style = { |  | ||||||
|           backgroundImage: |  | ||||||
|             "url(" + this.ASSET_URL + this.ASSET_SIZES[1] + backdrop + ")" |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         Object.assign(this.$refs.header.style, style); |   interface Props { | ||||||
|       } |     id: number; | ||||||
|     } |     type: MediaTypes.Movie | MediaTypes.Show; | ||||||
|   }, |  | ||||||
|   computed: { |  | ||||||
|     ...mapGetters("user", ["loggedIn", "admin", "plexId"]), |  | ||||||
|     numberOfTorrentResults: () => { |  | ||||||
|       let numTorrents = store.getters["torrentModule/resultCount"]; |  | ||||||
|       return numTorrents !== null ? numTorrents + " results" : null; |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     async fetchByType() { |  | ||||||
|       try { |  | ||||||
|         let response; |  | ||||||
|         if (this.type === "movie") { |  | ||||||
|           response = await getMovie(this.id, true, false); |  | ||||||
|         } else if (this.type === "show") { |  | ||||||
|           response = await getShow(this.id, false, false); |  | ||||||
|         } else { |  | ||||||
|           this.$router.push({ name: "404" }); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         this.parseResponse(response); |  | ||||||
|       } catch (error) { |  | ||||||
|         this.$router.push({ name: "404" }); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // async get credits |  | ||||||
|       getCredits(this.type, this.id).then(credits => (this.credits = credits)); |  | ||||||
|     }, |  | ||||||
|     parseResponse(movie) { |  | ||||||
|       this.loading = false; |  | ||||||
|       this.movie = { ...movie }; |  | ||||||
|       this.title = movie.title; |  | ||||||
|       this.poster = movie.poster; |  | ||||||
|       this.backdrop = movie.backdrop; |  | ||||||
|       this.matched = movie.exists_in_plex || false; |  | ||||||
|       this.checkIfRequested(movie).then(status => (this.requested = status)); |  | ||||||
|  |  | ||||||
|       store.dispatch("documentTitle/updateTitle", movie.title); |  | ||||||
|       this.setPosterSrc(); |  | ||||||
|     }, |  | ||||||
|     async checkIfRequested(movie) { |  | ||||||
|       return await getRequestStatus(movie.id, movie.type); |  | ||||||
|     }, |  | ||||||
|     setPosterSrc() { |  | ||||||
|       const poster = this.$refs["poster-image"]; |  | ||||||
|       if (this.poster == null) { |  | ||||||
|         poster.src = "/assets/no-image.svg"; |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       poster.src = `${this.ASSET_URL}${this.ASSET_SIZES[0]}${this.poster}`; |  | ||||||
|     }, |  | ||||||
|     humanMinutes(minutes) { |  | ||||||
|       if (minutes instanceof Array) { |  | ||||||
|         minutes = minutes[0]; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       const hours = Math.floor(minutes / 60); |  | ||||||
|       const minutesLeft = minutes - hours * 60; |  | ||||||
|  |  | ||||||
|       if (minutesLeft == 0) { |  | ||||||
|         return hours > 1 ? `${hours} hours` : `${hours} hour`; |  | ||||||
|       } else if (hours == 0) { |  | ||||||
|         return `${minutesLeft} min`; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       return `${hours}h ${minutesLeft}m`; |  | ||||||
|     }, |  | ||||||
|     sendRequest() { |  | ||||||
|       request(this.id, this.type).then(resp => { |  | ||||||
|         if (resp.success) { |  | ||||||
|           this.requested = true; |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     }, |  | ||||||
|     openInPlex() { |  | ||||||
|       watchLink(this.title, this.movie.year).then( |  | ||||||
|         watchLink => (window.location = watchLink) |  | ||||||
|       ); |  | ||||||
|     }, |  | ||||||
|     openTmdb() { |  | ||||||
|       const tmdbType = this.type === "show" ? "tv" : this.type; |  | ||||||
|       window.location.href = |  | ||||||
|         "https://www.themoviedb.org/" + tmdbType + "/" + this.id; |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   created() { |  | ||||||
|     store.dispatch("torrentModule/setResultCount", null); |  | ||||||
|     this.prevDocumentTitle = store.getters["documentTitle/title"]; |  | ||||||
|     this.fetchByType(); |  | ||||||
|   }, |  | ||||||
|   beforeDestroy() { |  | ||||||
|     store.dispatch("documentTitle/updateTitle", this.prevDocumentTitle); |  | ||||||
|   } |   } | ||||||
| }; |  | ||||||
|  |   const props = defineProps<Props>(); | ||||||
|  |   const ASSET_URL = "https://image.tmdb.org/t/p/"; | ||||||
|  |   const ASSET_SIZES = ["w500", "w780", "original"]; | ||||||
|  |  | ||||||
|  |   const media: Ref<IMovie | IShow> = ref(); | ||||||
|  |   const requested: Ref<boolean> = ref(); | ||||||
|  |   const showTorrents: Ref<boolean> = ref(); | ||||||
|  |   const showCast: Ref<boolean> = ref(); | ||||||
|  |   const cast: Ref<ICast[]> = ref([]); | ||||||
|  |   const compact: Ref<boolean> = ref(); | ||||||
|  |   const loading: Ref<boolean> = ref(); | ||||||
|  |   const backdropElement: Ref<HTMLElement> = ref(); | ||||||
|  |  | ||||||
|  |   const store = useStore(); | ||||||
|  |  | ||||||
|  |   const admin = computed(() => store.getters["user/admin"]); | ||||||
|  |   const plexId = computed(() => store.getters["user/plexId"]); | ||||||
|  |  | ||||||
|  |   const poster = computed(() => { | ||||||
|  |     if (!media.value) return "/assets/placeholder.png"; | ||||||
|  |     if (!media.value?.poster) return "/assets/no-image.svg"; | ||||||
|  |  | ||||||
|  |     return `${ASSET_URL}${ASSET_SIZES[0]}${media.value.poster}`; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const numberOfTorrentResults = computed(() => { | ||||||
|  |     const count = store.getters["torrentModule/resultCount"]; | ||||||
|  |     return count ? `${count} results` : null; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   function setCast(_cast: ICast[]) { | ||||||
|  |     cast.value = _cast; | ||||||
|  |   } | ||||||
|  |   function setRequested(status: boolean) { | ||||||
|  |     requested.value = status; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function getCredits( | ||||||
|  |     type: MediaTypes.Movie | MediaTypes.Show | ||||||
|  |   ): Promise<IMediaCredits> { | ||||||
|  |     if (type === MediaTypes.Movie) { | ||||||
|  |       return getMovieCredits(props.id); | ||||||
|  |     } | ||||||
|  |     if (type === MediaTypes.Show) { | ||||||
|  |       return getShowCredits(props.id); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return Promise.reject(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function setAndReturnMedia(_media: IMovie | IShow) { | ||||||
|  |     media.value = _media; | ||||||
|  |     return _media; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function fetchMedia() { | ||||||
|  |     if (!props.id || !props.type) { | ||||||
|  |       console.error("Unable to fetch media, requires id & type"); // eslint-disable-line no-console | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let apiFunction: typeof getMovie; | ||||||
|  |     let parameters: { | ||||||
|  |       checkExistance: boolean; | ||||||
|  |       credits: boolean; | ||||||
|  |       releaseDates?: boolean; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     if (props.type === MediaTypes.Movie) { | ||||||
|  |       apiFunction = getMovie; | ||||||
|  |       parameters = { checkExistance: true, credits: false }; | ||||||
|  |     } else if (props.type === MediaTypes.Show) { | ||||||
|  |       apiFunction = getShow; | ||||||
|  |       parameters = { checkExistance: true, credits: false }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     apiFunction(props.id, { ...parameters }) | ||||||
|  |       .then(setAndReturnMedia) | ||||||
|  |       .then(() => getCredits(props.type)) | ||||||
|  |       .then(credits => setCast(credits?.cast || [])) | ||||||
|  |       .then(() => getRequestStatus(props.id, props.type)) | ||||||
|  |       .then(requestStatus => setRequested(requestStatus || false)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function setBackdrop(): void { | ||||||
|  |     if (!media.value?.backdrop || !backdropElement.value?.style) return; | ||||||
|  |  | ||||||
|  |     const backdropURL = `${ASSET_URL}${ASSET_SIZES[1]}${media.value.backdrop}`; | ||||||
|  |     backdropElement.value.style.backgroundImage = `url(${backdropURL})`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function sendRequest() { | ||||||
|  |     request(props.id, props.type).then(resp => | ||||||
|  |       setRequested(resp?.success || false) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function openInPlex(): boolean { | ||||||
|  |     // watchLink() | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function openTmdb() { | ||||||
|  |     const tmdbType = props.type === MediaTypes.Show ? "tv" : props.type; | ||||||
|  |     const tmdbURL = `https://www.themoviedb.org/${tmdbType}/${props.id}`; | ||||||
|  |     window.location.href = tmdbURL; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // On created functions | ||||||
|  |   fetchMedia(); | ||||||
|  |   setBackdrop(); | ||||||
|  |   store.dispatch("torrentModule/setResultCount", null); | ||||||
|  |   // End on create functions | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "src/scss/loading-placeholder"; |   @import "src/scss/loading-placeholder"; | ||||||
| @import "src/scss/variables"; |   @import "src/scss/variables"; | ||||||
| @import "src/scss/media-queries"; |   @import "src/scss/media-queries"; | ||||||
| @import "src/scss/main"; |   @import "src/scss/main"; | ||||||
|  |  | ||||||
| header { |   header { | ||||||
|   $duration: 0.2s; |     $duration: 0.2s; | ||||||
|   transform: scaleY(1); |     transform: scaleY(1); | ||||||
|   transition: height $duration ease; |     transition: height $duration ease; | ||||||
|   transform-origin: top; |     transform-origin: top; | ||||||
|   position: relative; |     position: relative; | ||||||
|   background-size: cover; |     background-size: cover; | ||||||
|   background-repeat: no-repeat; |     background-repeat: no-repeat; | ||||||
|   background-position: 50% 50%; |     background-position: 50% 50%; | ||||||
|   background-color: $background-color; |     background-color: $background-color; | ||||||
|   display: grid; |     display: grid; | ||||||
|   grid-template-columns: 1fr 1fr; |     grid-template-columns: 1fr 1fr; | ||||||
|   height: 350px; |     height: 350px; | ||||||
|  |  | ||||||
|   @include mobile { |     @include mobile { | ||||||
|     grid-template-columns: 1fr; |       grid-template-columns: 1fr; | ||||||
|     height: 250px; |       height: 250px; | ||||||
|     place-items: center; |       place-items: center; | ||||||
|   } |  | ||||||
|  |  | ||||||
|   * { |  | ||||||
|     z-index: 2; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &::before { |  | ||||||
|     content: ""; |  | ||||||
|     display: block; |  | ||||||
|     position: absolute; |  | ||||||
|     top: 0; |  | ||||||
|     left: 0; |  | ||||||
|     z-index: 1; |  | ||||||
|     width: 100%; |  | ||||||
|     height: 100%; |  | ||||||
|     background: $background-dark-85; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @include mobile { |  | ||||||
|     &.compact { |  | ||||||
|       height: 100px; |  | ||||||
|     } |     } | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .movie__poster { |     * { | ||||||
|   display: none; |       z-index: 2; | ||||||
|  |     } | ||||||
|  |  | ||||||
|   @include desktop { |     &::before { | ||||||
|     background: var(--background-color); |       content: ""; | ||||||
|     height: auto; |       display: block; | ||||||
|     display: block; |       position: absolute; | ||||||
|     width: calc(100% - 80px); |       top: 0; | ||||||
|     margin: 40px; |       left: 0; | ||||||
|  |       z-index: 1; | ||||||
|     > img { |  | ||||||
|       width: 100%; |       width: 100%; | ||||||
|       border-radius: 10px; |       height: 100%; | ||||||
|  |       background: $background-dark-85; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @include mobile { | ||||||
|  |       &.compact { | ||||||
|  |         height: 100px; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } |  | ||||||
|  |  | ||||||
| .movie { |   .movie__poster { | ||||||
|   &__wrap { |     display: none; | ||||||
|     &--header { |  | ||||||
|       align-items: center; |     @include desktop { | ||||||
|       height: 100%; |       background: var(--background-color); | ||||||
|  |       height: auto; | ||||||
|  |       display: block; | ||||||
|  |       width: calc(100% - 80px); | ||||||
|  |       margin: 40px; | ||||||
|  |  | ||||||
|  |       > img { | ||||||
|  |         width: 100%; | ||||||
|  |         border-radius: 10px; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|     &--main { |   } | ||||||
|  |  | ||||||
|  |   .movie { | ||||||
|  |     &__wrap { | ||||||
|  |       &--header { | ||||||
|  |         align-items: center; | ||||||
|  |         height: 100%; | ||||||
|  |       } | ||||||
|  |       &--main { | ||||||
|  |         display: flex; | ||||||
|  |         flex-wrap: wrap; | ||||||
|  |         flex-direction: column; | ||||||
|  |         @include tablet-min { | ||||||
|  |           flex-direction: row; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         background-color: $background-color; | ||||||
|  |         color: $text-color; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &__img { | ||||||
|  |       display: block; | ||||||
|  |       width: 100%; | ||||||
|  |       opacity: 0; | ||||||
|  |       transform: scale(0.97) translateZ(0); | ||||||
|  |       transition: opacity 0.5s ease, transform 0.5s ease; | ||||||
|  |  | ||||||
|  |       &.is-loaded { | ||||||
|  |         opacity: 1; | ||||||
|  |         transform: scale(1); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &__title { | ||||||
|  |       position: relative; | ||||||
|  |       padding: 20px; | ||||||
|  |       text-align: center; | ||||||
|  |       width: 100%; | ||||||
|  |       height: fit-content; | ||||||
|  |  | ||||||
|  |       @include tablet-min { | ||||||
|  |         text-align: left; | ||||||
|  |         padding: 140px 30px 0 40px; | ||||||
|  |       } | ||||||
|  |       h1 { | ||||||
|  |         color: var(--color-green); | ||||||
|  |         font-weight: 500; | ||||||
|  |         line-height: 1.4; | ||||||
|  |         font-size: 24px; | ||||||
|  |         margin-bottom: 0; | ||||||
|  |  | ||||||
|  |         @include tablet-min { | ||||||
|  |           font-size: 30px; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       i { | ||||||
|  |         display: block; | ||||||
|  |         color: rgba(255, 255, 255, 0.8); | ||||||
|  |         margin-top: 1rem; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &__actions { | ||||||
|  |       text-align: center; | ||||||
|  |       width: 100%; | ||||||
|  |       order: 2; | ||||||
|  |       padding: 20px; | ||||||
|  |       border-top: 1px solid $text-color-5; | ||||||
|  |       @include tablet-min { | ||||||
|  |         order: 1; | ||||||
|  |         width: 45%; | ||||||
|  |         padding: 185px 0 40px 40px; | ||||||
|  |         border-top: 0; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     &__info { | ||||||
|  |       width: 100%; | ||||||
|  |       padding: 20px; | ||||||
|  |       order: 1; | ||||||
|  |       @include tablet-min { | ||||||
|  |         order: 2; | ||||||
|  |         padding: 40px; | ||||||
|  |         width: 55%; | ||||||
|  |         margin-left: 45%; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     &__info { | ||||||
|  |       margin-left: 0; | ||||||
|  |     } | ||||||
|  |     &__details { | ||||||
|       display: flex; |       display: flex; | ||||||
|       flex-wrap: wrap; |       flex-wrap: wrap; | ||||||
|       flex-direction: column; |  | ||||||
|       @include tablet-min { |  | ||||||
|         flex-direction: row; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       background-color: $background-color; |       > * { | ||||||
|       color: $text-color; |         margin-right: 30px; | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &__img { |         @include mobile { | ||||||
|     display: block; |           margin-right: 20px; | ||||||
|     width: 100%; |         } | ||||||
|     opacity: 0; |  | ||||||
|     transform: scale(0.97) translateZ(0); |  | ||||||
|     transition: opacity 0.5s ease, transform 0.5s ease; |  | ||||||
|  |  | ||||||
|     &.is-loaded { |  | ||||||
|       opacity: 1; |  | ||||||
|       transform: scale(1); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &__title { |  | ||||||
|     position: relative; |  | ||||||
|     padding: 20px; |  | ||||||
|     text-align: center; |  | ||||||
|     width: 100%; |  | ||||||
|     height: fit-content; |  | ||||||
|  |  | ||||||
|     @include tablet-min { |  | ||||||
|       text-align: left; |  | ||||||
|       padding: 140px 30px 0 40px; |  | ||||||
|     } |  | ||||||
|     h1 { |  | ||||||
|       color: var(--color-green); |  | ||||||
|       font-weight: 500; |  | ||||||
|       line-height: 1.4; |  | ||||||
|       font-size: 24px; |  | ||||||
|       margin-bottom: 0; |  | ||||||
|  |  | ||||||
|       @include tablet-min { |  | ||||||
|         font-size: 30px; |  | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |     &__admin { | ||||||
|     i { |       width: 100%; | ||||||
|       display: block; |       padding: 20px; | ||||||
|       color: rgba(255, 255, 255, 0.8); |  | ||||||
|       margin-top: 1rem; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &__actions { |  | ||||||
|     text-align: center; |  | ||||||
|     width: 100%; |  | ||||||
|     order: 2; |  | ||||||
|     padding: 20px; |  | ||||||
|     border-top: 1px solid $text-color-5; |  | ||||||
|     @include tablet-min { |  | ||||||
|       order: 1; |  | ||||||
|       width: 45%; |  | ||||||
|       padding: 185px 0 40px 40px; |  | ||||||
|       border-top: 0; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   &__info { |  | ||||||
|     width: 100%; |  | ||||||
|     padding: 20px; |  | ||||||
|     order: 1; |  | ||||||
|     @include tablet-min { |  | ||||||
|       order: 2; |       order: 2; | ||||||
|       padding: 40px; |       @include tablet-min { | ||||||
|       width: 55%; |         order: 3; | ||||||
|       margin-left: 45%; |         padding: 40px; | ||||||
|  |         padding-top: 0px; | ||||||
|  |         width: 100%; | ||||||
|  |       } | ||||||
|  |       &-title { | ||||||
|  |         margin: 0; | ||||||
|  |         font-weight: 400; | ||||||
|  |         text-transform: uppercase; | ||||||
|  |         text-align: center; | ||||||
|  |         font-size: 14px; | ||||||
|  |         color: $green; | ||||||
|  |         padding-bottom: 20px; | ||||||
|  |         @include tablet-min { | ||||||
|  |           font-size: 16px; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |  | ||||||
|   &__info { |  | ||||||
|     margin-left: 0; |  | ||||||
|   } |  | ||||||
|   &__details { |  | ||||||
|     display: flex; |  | ||||||
|     flex-wrap: wrap; |  | ||||||
|  |  | ||||||
|     > * { |     .torrents { | ||||||
|       margin-right: 30px; |       background-color: var(--background-color); | ||||||
|  |       padding: 0 1rem; | ||||||
|  |  | ||||||
|       @include mobile { |       @include mobile { | ||||||
|         margin-right: 20px; |         padding: 0 0.5rem; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   &__admin { |  | ||||||
|     width: 100%; |  | ||||||
|     padding: 20px; |  | ||||||
|     order: 2; |  | ||||||
|     @include tablet-min { |  | ||||||
|       order: 3; |  | ||||||
|       padding: 40px; |  | ||||||
|       padding-top: 0px; |  | ||||||
|       width: 100%; |  | ||||||
|     } |  | ||||||
|     &-title { |  | ||||||
|       margin: 0; |  | ||||||
|       font-weight: 400; |  | ||||||
|       text-transform: uppercase; |  | ||||||
|       text-align: center; |  | ||||||
|       font-size: 14px; |  | ||||||
|       color: $green; |  | ||||||
|       padding-bottom: 20px; |  | ||||||
|       @include tablet-min { |  | ||||||
|         font-size: 16px; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .fade-enter-active, |   .fade-enter-active, | ||||||
| .fade-leave-active { |   .fade-leave-active { | ||||||
|   transition: opacity 0.4s; |     transition: opacity 0.4s; | ||||||
| } |   } | ||||||
| .fade-enter, |   .fade-enter, | ||||||
| .fade-leave-to { |   .fade-leave-to { | ||||||
|   opacity: 0; |     opacity: 0; | ||||||
| } |   } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -3,14 +3,14 @@ | |||||||
|     <header ref="header"> |     <header ref="header"> | ||||||
|       <div class="info"> |       <div class="info"> | ||||||
|         <h1 v-if="person"> |         <h1 v-if="person"> | ||||||
|           {{ person.title || person.name }} |           {{ person.name }} | ||||||
|         </h1> |         </h1> | ||||||
|         <div v-else> |         <div v-else> | ||||||
|           <loading-placeholder :count="1" /> |           <loading-placeholder :count="1" /> | ||||||
|           <loading-placeholder :count="1" lineClass="short" :top="3.5" /> |           <loading-placeholder :count="1" line-class="short" :top="3.5" /> | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|         <span class="known-for" v-if="person && person['known_for_department']"> |         <span v-if="person && person['known_for_department']" class="known-for"> | ||||||
|           {{ |           {{ | ||||||
|             person.known_for_department === "Acting" |             person.known_for_department === "Acting" | ||||||
|               ? "Actor" |               ? "Actor" | ||||||
| @@ -21,18 +21,19 @@ | |||||||
|  |  | ||||||
|       <figure class="person__poster"> |       <figure class="person__poster"> | ||||||
|         <img |         <img | ||||||
|           class="person-item__img is-loaded" |  | ||||||
|           ref="poster-image" |           ref="poster-image" | ||||||
|           src="/assets/placeholder.png" |           class="person-item__img is-loaded" | ||||||
|  |           alt="Image of person" | ||||||
|  |           :src="poster" | ||||||
|         /> |         /> | ||||||
|       </figure> |       </figure> | ||||||
|     </header> |     </header> | ||||||
|  |  | ||||||
|     <div v-if="loading"> |     <div v-if="loading"> | ||||||
|       <loading-placeholder :count="6" /> |       <loading-placeholder :count="6" /> | ||||||
|       <loading-placeholder lineClass="short" :top="3" /> |       <loading-placeholder line-class="short" :top="3" /> | ||||||
|       <loading-placeholder :count="6" lineClass="fullwidth" /> |       <loading-placeholder :count="6" line-class="fullwidth" /> | ||||||
|       <loading-placeholder lineClass="short" :top="4.5" /> |       <loading-placeholder line-class="short" :top="4.5" /> | ||||||
|       <loading-placeholder /> |       <loading-placeholder /> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
| @@ -50,251 +51,228 @@ | |||||||
|       </Detail> |       </Detail> | ||||||
|  |  | ||||||
|       <Detail |       <Detail | ||||||
|  |         v-if="creditedShows.length" | ||||||
|         title="movies" |         title="movies" | ||||||
|         :detail="`Credited in ${movieCredits.length} movies`" |         :detail="`Credited in ${creditedMovies.length} movies`" | ||||||
|         v-if="credits" |  | ||||||
|       > |       > | ||||||
|         <CastList :cast="movieCredits" /> |         <CastList :cast="creditedMovies" /> | ||||||
|       </Detail> |       </Detail> | ||||||
|  |  | ||||||
|       <Detail |       <Detail | ||||||
|  |         v-if="creditedShows.length" | ||||||
|         title="shows" |         title="shows" | ||||||
|         :detail="`Credited in ${showCredits.length} shows`" |         :detail="`Credited in ${creditedShows.length} shows`" | ||||||
|         v-if="credits" |  | ||||||
|       > |       > | ||||||
|         <CastList :cast="showCredits" /> |         <CastList :cast="creditedShows" /> | ||||||
|       </Detail> |       </Detail> | ||||||
|     </div> |     </div> | ||||||
|   </section> |   </section> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| import img from "@/directives/v-image"; |   import { ref, computed, defineProps } from "vue"; | ||||||
| import CastList from "@/components/CastList"; |   import CastList from "@/components/CastList.vue"; | ||||||
| import Detail from "@/components/popup/Detail"; |   import Detail from "@/components/popup/Detail.vue"; | ||||||
| import Description from "@/components/popup/Description"; |   import Description from "@/components/popup/Description.vue"; | ||||||
| import LoadingPlaceholder from "@/components/ui/LoadingPlaceholder"; |   import LoadingPlaceholder from "@/components/ui/LoadingPlaceholder.vue"; | ||||||
|  |   import type { Ref, ComputedRef } from "vue"; | ||||||
|  |   import { getPerson, getPersonCredits } from "../../api"; | ||||||
|  |   import type { | ||||||
|  |     IPerson, | ||||||
|  |     IPersonCredits, | ||||||
|  |     IMovie, | ||||||
|  |     IShow | ||||||
|  |   } from "../../interfaces/IList"; | ||||||
|  |   import { MediaTypes } from "../../interfaces/IList"; | ||||||
|  |  | ||||||
| import { getPerson, getPersonCredits } from "@/api"; |   interface Props { | ||||||
|  |     id: number; | ||||||
| export default { |  | ||||||
|   props: { |  | ||||||
|     id: { |  | ||||||
|       required: true, |  | ||||||
|       type: Number |  | ||||||
|     }, |  | ||||||
|     type: { |  | ||||||
|       required: false, |  | ||||||
|       type: String, |  | ||||||
|       default: "person" |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   components: { |  | ||||||
|     Detail, |  | ||||||
|     Description, |  | ||||||
|     CastList, |  | ||||||
|     LoadingPlaceholder |  | ||||||
|   }, |  | ||||||
|   directives: { img: img }, // TODO decide to remove or use |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       ASSET_URL: "https://image.tmdb.org/t/p/", |  | ||||||
|       ASSET_SIZES: ["w500", "w780", "original"], |  | ||||||
|       person: undefined, |  | ||||||
|       loading: true, |  | ||||||
|       credits: undefined |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|   watch: { |  | ||||||
|     backdrop: function (backdrop) { |  | ||||||
|       if (backdrop != null) { |  | ||||||
|         const style = { |  | ||||||
|           backgroundImage: |  | ||||||
|             "url(" + this.ASSET_URL + this.ASSET_SIZES[1] + backdrop + ")" |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         Object.assign(this.$refs.header.style, style); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   computed: { |  | ||||||
|     age: function () { |  | ||||||
|       if (!this.person || !this.person.birthday) { |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       const today = new Date().getFullYear(); |  | ||||||
|       const birthYear = new Date(this.person.birthday).getFullYear(); |  | ||||||
|       return `${today - birthYear} years old`; |  | ||||||
|     }, |  | ||||||
|     movieCredits: function () { |  | ||||||
|       const { cast } = this.credits; |  | ||||||
|       if (!cast) return; |  | ||||||
|  |  | ||||||
|       return cast |  | ||||||
|         .filter(l => l.type === "movie") |  | ||||||
|         .filter((item, pos, self) => self.indexOf(item) == pos) |  | ||||||
|         .sort((a, b) => a.popularity < b.popularity); |  | ||||||
|     }, |  | ||||||
|     showCredits: function () { |  | ||||||
|       const { cast } = this.credits; |  | ||||||
|       if (!cast) return; |  | ||||||
|  |  | ||||||
|       const alreadyExists = (item, pos, self) => { |  | ||||||
|         const names = self.map(item => item.title); |  | ||||||
|         return names.indexOf(item.title) == pos; |  | ||||||
|       }; |  | ||||||
|  |  | ||||||
|       return cast |  | ||||||
|         .filter(item => item.type === "show") |  | ||||||
|         .filter(alreadyExists) |  | ||||||
|         .sort((a, b) => a.popularity < b.popularity); |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     parseResponse(person) { |  | ||||||
|       this.loading = false; |  | ||||||
|       this.person = { ...person }; |  | ||||||
|       this.title = person.title; |  | ||||||
|       this.poster = person.poster; |  | ||||||
|       if (person.credits) this.credits = person.credits; |  | ||||||
|  |  | ||||||
|       this.setPosterSrc(); |  | ||||||
|     }, |  | ||||||
|     setPosterSrc() { |  | ||||||
|       const poster = this.$refs["poster-image"]; |  | ||||||
|       if (this.poster == null) { |  | ||||||
|         poster.src = "/assets/no-image.svg"; |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       poster.src = `${this.ASSET_URL}${this.ASSET_SIZES[0]}${this.poster}`; |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   created() { |  | ||||||
|     getPerson(this.id, false) |  | ||||||
|       .then(this.parseResponse) |  | ||||||
|       .catch(error => { |  | ||||||
|         console.error(error); |  | ||||||
|         this.$router.push({ name: "404" }); |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|     getPersonCredits(this.id) |  | ||||||
|       .then(credits => (this.credits = credits)) |  | ||||||
|       .catch(error => { |  | ||||||
|         console.error(error); |  | ||||||
|       }); |  | ||||||
|   } |   } | ||||||
| }; |  | ||||||
|  |   const props = defineProps<Props>(); | ||||||
|  |   const ASSET_URL = "https://image.tmdb.org/t/p/"; | ||||||
|  |   const ASSET_SIZES = ["w500", "w780", "original"]; | ||||||
|  |  | ||||||
|  |   const person: Ref<IPerson> = ref(); | ||||||
|  |   const credits: Ref<IPersonCredits> = ref(); | ||||||
|  |   const loading: Ref<boolean> = ref(false); | ||||||
|  |   const creditedMovies: Ref<Array<IMovie | IShow>> = ref([]); | ||||||
|  |   const creditedShows: Ref<Array<IMovie | IShow>> = ref([]); | ||||||
|  |  | ||||||
|  |   const poster: ComputedRef<string> = computed(() => { | ||||||
|  |     if (!person.value) return "/assets/placeholder.png"; | ||||||
|  |     if (!person.value?.poster) return "/assets/no-image.svg"; | ||||||
|  |  | ||||||
|  |     return `${ASSET_URL}${ASSET_SIZES[0]}${person.value.poster}`; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const age: ComputedRef<string> = computed(() => { | ||||||
|  |     if (!person.value?.birthday) return ""; | ||||||
|  |  | ||||||
|  |     const today = new Date().getFullYear(); | ||||||
|  |     const birthYear = new Date(person.value.birthday).getFullYear(); | ||||||
|  |     return `${today - birthYear} years old`; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   function setCredits(_credits: IPersonCredits) { | ||||||
|  |     credits.value = _credits; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function setPerson(_person: IPerson) { | ||||||
|  |     person.value = _person; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function sortPopularity(a: IMovie | IShow, b: IMovie | IShow): number { | ||||||
|  |     return a.popularity < b.popularity ? 1 : -1; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function alreadyExists( | ||||||
|  |     item: IMovie | IShow, | ||||||
|  |     pos: number, | ||||||
|  |     self: Array<IMovie | IShow> | ||||||
|  |   ) { | ||||||
|  |     const names = self.map(_item => _item.title); | ||||||
|  |     return names.indexOf(item.title) === pos; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function personCreditedFrom(cast: Array<IMovie | IShow>): void { | ||||||
|  |     creditedMovies.value = cast | ||||||
|  |       .filter(credit => credit.type === MediaTypes.Movie) | ||||||
|  |       .filter(alreadyExists) | ||||||
|  |       .sort(sortPopularity); | ||||||
|  |  | ||||||
|  |     creditedShows.value = cast | ||||||
|  |       .filter(credit => credit.type === MediaTypes.Show) | ||||||
|  |       .filter(alreadyExists) | ||||||
|  |       .sort(sortPopularity); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function fetchPerson() { | ||||||
|  |     if (!props.id) { | ||||||
|  |       console.error("Unable to fetch person, missing id!"); // eslint-disable-line no-console | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     getPerson(props.id) | ||||||
|  |       .then(setPerson) | ||||||
|  |       .then(() => getPersonCredits(person.value?.id)) | ||||||
|  |       .then(setCredits) | ||||||
|  |       .then(() => personCreditedFrom(credits.value?.cast)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // On create functions | ||||||
|  |   fetchPerson(); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "src/scss/loading-placeholder"; |   @import "src/scss/loading-placeholder"; | ||||||
| @import "src/scss/variables"; |   @import "src/scss/variables"; | ||||||
| @import "src/scss/media-queries"; |   @import "src/scss/media-queries"; | ||||||
| @import "src/scss/main"; |   @import "src/scss/main"; | ||||||
|  |  | ||||||
| section.person { |   section.person { | ||||||
|   overflow: hidden; |     overflow: hidden; | ||||||
|   position: relative; |     position: relative; | ||||||
|   padding: 40px; |     padding: 40px; | ||||||
|   background-color: var(--background-color); |     background-color: var(--background-color); | ||||||
|  |  | ||||||
|   @include mobile { |     @include mobile { | ||||||
|     padding: 50px 20px 10px; |       padding: 50px 20px 10px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &:before { | ||||||
|  |       content: ""; | ||||||
|  |       display: block; | ||||||
|  |       position: absolute; | ||||||
|  |       top: -130px; | ||||||
|  |       left: -100px; | ||||||
|  |       z-index: 1; | ||||||
|  |       width: 1000px; | ||||||
|  |       height: 500px; | ||||||
|  |       transform: rotate(21deg); | ||||||
|  |       background-color: #062541; | ||||||
|  |  | ||||||
|  |       @include mobile { | ||||||
|  |         // top: -52vw; | ||||||
|  |         top: -215px; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   &:before { |   header { | ||||||
|     content: ""; |     $duration: 0.2s; | ||||||
|  |     transition: height $duration ease; | ||||||
|  |     position: relative; | ||||||
|  |     background-color: transparent; | ||||||
|  |     display: grid; | ||||||
|  |     grid-template-columns: 1fr 1fr; | ||||||
|  |     height: 350px; | ||||||
|  |     z-index: 2; | ||||||
|  |  | ||||||
|  |     @include mobile { | ||||||
|  |       height: 180px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .info { | ||||||
|  |       display: flex; | ||||||
|  |       flex-direction: column; | ||||||
|  |       padding: 30px; | ||||||
|  |       padding-left: 0; | ||||||
|  |       text-align: left; | ||||||
|  |  | ||||||
|  |       @include mobile { | ||||||
|  |         padding: 0; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     h1 { | ||||||
|  |       color: $green; | ||||||
|  |       width: 100%; | ||||||
|  |       font-weight: 500; | ||||||
|  |       line-height: 1.4; | ||||||
|  |       font-size: 30px; | ||||||
|  |       margin-top: 0; | ||||||
|  |  | ||||||
|  |       @include mobile { | ||||||
|  |         font-size: 24px; | ||||||
|  |         margin: 10px 0; | ||||||
|  |         // padding: 30px 30px 30px 40px; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .known-for { | ||||||
|  |       color: rgba(255, 255, 255, 0.8); | ||||||
|  |       font-size: 1.2rem; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .person__poster { | ||||||
|     display: block; |     display: block; | ||||||
|     position: absolute; |     margin: auto; | ||||||
|     top: -130px; |     width: fit-content; | ||||||
|     left: -100px; |  | ||||||
|     z-index: 1; |  | ||||||
|     width: 1000px; |  | ||||||
|     height: 500px; |  | ||||||
|     transform: rotate(21deg); |  | ||||||
|     background-color: #062541; |  | ||||||
|  |  | ||||||
|     @include mobile { |  | ||||||
|       // top: -52vw; |  | ||||||
|       top: -215px; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| header { |  | ||||||
|   $duration: 0.2s; |  | ||||||
|   transition: height $duration ease; |  | ||||||
|   position: relative; |  | ||||||
|   background-color: transparent; |  | ||||||
|   display: grid; |  | ||||||
|   grid-template-columns: 1fr 1fr; |  | ||||||
|   height: 350px; |  | ||||||
|   z-index: 2; |  | ||||||
|  |  | ||||||
|   @include mobile { |  | ||||||
|     height: 180px; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .info { |  | ||||||
|     display: flex; |  | ||||||
|     flex-direction: column; |  | ||||||
|     padding: 30px; |  | ||||||
|     padding-left: 0; |  | ||||||
|     text-align: left; |  | ||||||
|  |  | ||||||
|     @include mobile { |  | ||||||
|       padding: 0; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   h1 { |  | ||||||
|     color: $green; |  | ||||||
|     width: 100%; |  | ||||||
|     font-weight: 500; |  | ||||||
|     line-height: 1.4; |  | ||||||
|     font-size: 30px; |  | ||||||
|     margin-top: 0; |  | ||||||
|  |  | ||||||
|     @include mobile { |  | ||||||
|       font-size: 24px; |  | ||||||
|       margin: 10px 0; |  | ||||||
|       // padding: 30px 30px 30px 40px; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .known-for { |  | ||||||
|     color: rgba(255, 255, 255, 0.8); |  | ||||||
|     font-size: 1.2rem; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .person__poster { |  | ||||||
|   display: block; |  | ||||||
|   border-radius: 10px; |  | ||||||
|   background-color: grey; |  | ||||||
|   animation: pulse 1s infinite ease-in-out; |  | ||||||
|  |  | ||||||
|   @keyframes pulse { |  | ||||||
|     0% { |  | ||||||
|       background-color: rgba(165, 165, 165, 0.1); |  | ||||||
|     } |  | ||||||
|     50% { |  | ||||||
|       background-color: rgba(165, 165, 165, 0.3); |  | ||||||
|     } |  | ||||||
|     100% { |  | ||||||
|       background-color: rgba(165, 165, 165, 0.1); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   > img { |  | ||||||
|     border-radius: 10px; |     border-radius: 10px; | ||||||
|     width: 100%; |     background-color: grey; | ||||||
|  |     animation: pulse 1s infinite ease-in-out; | ||||||
|  |  | ||||||
|     @include mobile { |     @keyframes pulse { | ||||||
|       max-width: 225px; |       0% { | ||||||
|  |         background-color: rgba(165, 165, 165, 0.1); | ||||||
|  |       } | ||||||
|  |       50% { | ||||||
|  |         background-color: rgba(165, 165, 165, 0.3); | ||||||
|  |       } | ||||||
|  |       100% { | ||||||
|  |         background-color: rgba(165, 165, 165, 0.1); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     > img { | ||||||
|  |       border-radius: 10px; | ||||||
|  |       width: 100%; | ||||||
|  |  | ||||||
|  |       @include mobile { | ||||||
|  |         max-width: 225px; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
							
								
								
									
										98
									
								
								src/components/profile/ChangePassword.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,98 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <h3 class="settings__header">Change password</h3> | ||||||
|  |     <form class="form"> | ||||||
|  |       <seasoned-input | ||||||
|  |         v-model="oldPassword" | ||||||
|  |         placeholder="old password" | ||||||
|  |         icon="Keyhole" | ||||||
|  |         type="password" | ||||||
|  |       /> | ||||||
|  |  | ||||||
|  |       <seasoned-input | ||||||
|  |         v-model="newPassword" | ||||||
|  |         placeholder="new password" | ||||||
|  |         icon="Keyhole" | ||||||
|  |         type="password" | ||||||
|  |       /> | ||||||
|  |  | ||||||
|  |       <seasoned-input | ||||||
|  |         v-model="newPasswordRepeat" | ||||||
|  |         placeholder="repeat new password" | ||||||
|  |         icon="Keyhole" | ||||||
|  |         type="password" | ||||||
|  |       /> | ||||||
|  |  | ||||||
|  |       <seasoned-button @click="changePassword">change password</seasoned-button> | ||||||
|  |     </form> | ||||||
|  |     <seasoned-messages v-model:messages="messages" /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { ref } from "vue"; | ||||||
|  |   import SeasonedInput from "@/components/ui/SeasonedInput.vue"; | ||||||
|  |   import SeasonedButton from "@/components/ui/SeasonedButton.vue"; | ||||||
|  |   import SeasonedMessages from "@/components/ui/SeasonedMessages.vue"; | ||||||
|  |   import type { Ref } from "vue"; | ||||||
|  |   import { ErrorMessageTypes } from "../../interfaces/IErrorMessage"; | ||||||
|  |   import type { IErrorMessage } from "../../interfaces/IErrorMessage"; | ||||||
|  |  | ||||||
|  |   // interface ResetPasswordPayload { | ||||||
|  |   //   old_password: string; | ||||||
|  |   //   new_password: string; | ||||||
|  |   // } | ||||||
|  |  | ||||||
|  |   const oldPassword: Ref<string> = ref(""); | ||||||
|  |   const newPassword: Ref<string> = ref(""); | ||||||
|  |   const newPasswordRepeat: Ref<string> = ref(""); | ||||||
|  |   const messages: Ref<IErrorMessage[]> = ref([]); | ||||||
|  |  | ||||||
|  |   function addWarningMessage(message: string, title?: string) { | ||||||
|  |     messages.value.push({ | ||||||
|  |       message, | ||||||
|  |       title, | ||||||
|  |       type: ErrorMessageTypes.Warning | ||||||
|  |     } as IErrorMessage); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function validate() { | ||||||
|  |     return new Promise((resolve, reject) => { | ||||||
|  |       if (!oldPassword.value || oldPassword?.value?.length === 0) { | ||||||
|  |         addWarningMessage("Missing old password!", "Validation error"); | ||||||
|  |         reject(); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (!newPassword.value || newPassword?.value?.length === 0) { | ||||||
|  |         addWarningMessage("Missing new password!", "Validation error"); | ||||||
|  |         reject(); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (newPassword.value !== newPasswordRepeat.value) { | ||||||
|  |         addWarningMessage( | ||||||
|  |           "Password and password repeat do not match!", | ||||||
|  |           "Validation error" | ||||||
|  |         ); | ||||||
|  |         reject(); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       resolve(true); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // TODO seasoned-api /user/password-reset | ||||||
|  |   async function changePassword() { | ||||||
|  |     try { | ||||||
|  |       validate(); | ||||||
|  |     } catch (error) { | ||||||
|  |       console.log("not valid!"); // eslint-disable-line no-console | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // const body: ResetPasswordPayload = { | ||||||
|  |     //   old_password: oldPassword.value, | ||||||
|  |     //   new_password: newPassword.value | ||||||
|  |     // }; | ||||||
|  |     // const options = {}; | ||||||
|  |     // fetch() | ||||||
|  |   } | ||||||
|  | </script> | ||||||
							
								
								
									
										114
									
								
								src/components/profile/LinkPlexAccount.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,114 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <h3 class="settings__header">Plex account</h3> | ||||||
|  |  | ||||||
|  |     <div v-if="!plexId"> | ||||||
|  |       <span class="info" | ||||||
|  |         >Sign in to your plex account to get information about recently added | ||||||
|  |         movies and to see your watch history</span | ||||||
|  |       > | ||||||
|  |  | ||||||
|  |       <form class="form"> | ||||||
|  |         <seasoned-input | ||||||
|  |           v-model="username" | ||||||
|  |           placeholder="plex username" | ||||||
|  |           type="email" | ||||||
|  |         /> | ||||||
|  |         <seasoned-input | ||||||
|  |           v-model="password" | ||||||
|  |           placeholder="plex password" | ||||||
|  |           type="password" | ||||||
|  |           @enter="authenticatePlex" | ||||||
|  |         > | ||||||
|  |         </seasoned-input> | ||||||
|  |  | ||||||
|  |         <seasoned-button @click="authenticatePlex" | ||||||
|  |           >link plex account</seasoned-button | ||||||
|  |         > | ||||||
|  |       </form> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div v-else> | ||||||
|  |       <span class="info" | ||||||
|  |         >Awesome, your account is already authenticated with plex! Enjoy viewing | ||||||
|  |         your seasoned search history, plex watch history and real-time torrent | ||||||
|  |         download progress.</span | ||||||
|  |       > | ||||||
|  |       <seasoned-button @click="unauthenticatePlex" | ||||||
|  |         >un-link plex account</seasoned-button | ||||||
|  |       > | ||||||
|  |     </div> | ||||||
|  |     <seasoned-messages v-model:messages="messages" /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { ref, computed, defineEmits } from "vue"; | ||||||
|  |   import { useStore } from "vuex"; | ||||||
|  |   import seasonedInput from "@/components/ui/SeasonedInput.vue"; | ||||||
|  |   import SeasonedButton from "@/components/ui/SeasonedButton.vue"; | ||||||
|  |   import SeasonedMessages from "@/components/ui/SeasonedMessages.vue"; | ||||||
|  |   import type { Ref, ComputedRef } from "vue"; | ||||||
|  |   import { linkPlexAccount, unlinkPlexAccount } from "../../api"; | ||||||
|  |   import { ErrorMessageTypes } from "../../interfaces/IErrorMessage"; | ||||||
|  |   import type { IErrorMessage } from "../../interfaces/IErrorMessage"; | ||||||
|  |  | ||||||
|  |   interface Emit { | ||||||
|  |     (e: "reload"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const username: Ref<string> = ref(""); | ||||||
|  |   const password: Ref<string> = ref(""); | ||||||
|  |   const messages: Ref<IErrorMessage[]> = ref([]); | ||||||
|  |  | ||||||
|  |   const store = useStore(); | ||||||
|  |   const emit = defineEmits<Emit>(); | ||||||
|  |  | ||||||
|  |   const plexId: ComputedRef<boolean> = computed( | ||||||
|  |     () => store.getters["user/plexId"] | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   async function authenticatePlex() { | ||||||
|  |     const { success, message } = await linkPlexAccount( | ||||||
|  |       username.value, | ||||||
|  |       password.value | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     if (success) { | ||||||
|  |       username.value = ""; | ||||||
|  |       password.value = ""; | ||||||
|  |       emit("reload"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     messages.value.push({ | ||||||
|  |       type: success ? ErrorMessageTypes.Success : ErrorMessageTypes.Error, | ||||||
|  |       title: success ? "Authenticated with plex" : "Something went wrong", | ||||||
|  |       message | ||||||
|  |     } as IErrorMessage); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async function unauthenticatePlex() { | ||||||
|  |     const response = await unlinkPlexAccount(); | ||||||
|  |  | ||||||
|  |     if (response?.success) { | ||||||
|  |       emit("reload"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     messages.value.push({ | ||||||
|  |       type: response.success | ||||||
|  |         ? ErrorMessageTypes.Success | ||||||
|  |         : ErrorMessageTypes.Error, | ||||||
|  |       title: response.success | ||||||
|  |         ? "Unlinked plex account " | ||||||
|  |         : "Something went wrong", | ||||||
|  |       message: response.message | ||||||
|  |     } as IErrorMessage); | ||||||
|  |   } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  |   .info { | ||||||
|  |     display: block; | ||||||
|  |     margin-bottom: 25px; | ||||||
|  |   } | ||||||
|  | </style> | ||||||
							
								
								
									
										6
									
								
								src/components/torrent/ActiveTorrents.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | |||||||
|  | <template> | ||||||
|  |   <code | ||||||
|  |     >Monitor active torrents requested. Requires authentication with owners plex | ||||||
|  |     library!</code | ||||||
|  |   > | ||||||
|  | </template> | ||||||
							
								
								
									
										162
									
								
								src/components/torrent/TorrentSearchResults.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,162 @@ | |||||||
|  | <template> | ||||||
|  |   <div v-if="query?.length" class="container"> | ||||||
|  |     <h2 class="torrent-header-text"> | ||||||
|  |       Searching for: <span class="query">{{ query }}</span> | ||||||
|  |     </h2> | ||||||
|  |  | ||||||
|  |     <loader v-if="loading" type="section" /> | ||||||
|  |     <div v-else> | ||||||
|  |       <div v-if="torrents.length > 0" class="torrent-table"> | ||||||
|  |         <torrent-table :torrents="torrents" @magnet="addTorrent" /> | ||||||
|  |  | ||||||
|  |         <slot /> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div v-else class="no-results"> | ||||||
|  |         <h2>No results found</h2> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { ref, inject, defineProps } from "vue"; | ||||||
|  |   import { useStore } from "vuex"; | ||||||
|  |   import Loader from "@/components/ui/Loader.vue"; | ||||||
|  |   import TorrentTable from "@/components/torrent/TorrentTable.vue"; | ||||||
|  |   import type { Ref } from "vue"; | ||||||
|  |   import { searchTorrents, addMagnet } from "../../api"; | ||||||
|  |   import type ITorrent from "../../interfaces/ITorrent"; | ||||||
|  |  | ||||||
|  |   interface Props { | ||||||
|  |     query: string; | ||||||
|  |     tmdbId?: number; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const loading: Ref<boolean> = ref(true); | ||||||
|  |   const torrents: Ref<ITorrent[]> = ref([]); | ||||||
|  |  | ||||||
|  |   const props = defineProps<Props>(); | ||||||
|  |   const store = useStore(); | ||||||
|  |   const notifications: { | ||||||
|  |     info; | ||||||
|  |     success; | ||||||
|  |     error; | ||||||
|  |   } = inject("notifications"); | ||||||
|  |  | ||||||
|  |   function setTorrents(_torrents: ITorrent[]) { | ||||||
|  |     torrents.value = _torrents || []; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function setLoading(state: boolean) { | ||||||
|  |     loading.value = state; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function updateResultCountDisplay() { | ||||||
|  |     store.dispatch("torrentModule/setResults", torrents.value); | ||||||
|  |     store.dispatch( | ||||||
|  |       "torrentModule/setResultCount", | ||||||
|  |       torrents.value?.length || -1 | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function fetchTorrents() { | ||||||
|  |     if (!props.query?.length) return; | ||||||
|  |  | ||||||
|  |     loading.value = true; | ||||||
|  |     searchTorrents(props.query) | ||||||
|  |       .then(torrentResponse => setTorrents(torrentResponse?.results)) | ||||||
|  |       .then(() => updateResultCountDisplay()) | ||||||
|  |       .finally(() => setLoading(false)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function addTorrent(torrent: ITorrent) { | ||||||
|  |     const { name, magnet } = torrent; | ||||||
|  |  | ||||||
|  |     notifications.info({ | ||||||
|  |       title: "Adding torrent 🧲", | ||||||
|  |       description: props.query, | ||||||
|  |       timeout: 3000 | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     addMagnet(magnet, name, props.tmdbId) | ||||||
|  |       .then(() => { | ||||||
|  |         notifications.success({ | ||||||
|  |           title: "Torrent added 🎉", | ||||||
|  |           description: props.query, | ||||||
|  |           timeout: 3000 | ||||||
|  |         }); | ||||||
|  |       }) | ||||||
|  |       .catch(() => { | ||||||
|  |         notifications.error({ | ||||||
|  |           title: "Failed to add torrent 🙅♀️", | ||||||
|  |           description: "Check console for more info", | ||||||
|  |           timeout: 3000 | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   fetchTorrents(); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  |   @import "src/scss/variables"; | ||||||
|  |   @import "src/scss/media-queries"; | ||||||
|  |   @import "src/scss/elements"; | ||||||
|  |  | ||||||
|  |   h2 { | ||||||
|  |     font-size: 20px; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .toggle { | ||||||
|  |     max-width: unset !important; | ||||||
|  |     margin: 1rem 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .container { | ||||||
|  |     background-color: $background-color; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .no-results { | ||||||
|  |     display: flex; | ||||||
|  |     padding-bottom: 2rem; | ||||||
|  |     justify-content: center; | ||||||
|  |     flex-direction: column; | ||||||
|  |     width: 100%; | ||||||
|  |     align-items: center; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .torrent-header-text { | ||||||
|  |     font-weight: 300; | ||||||
|  |     text-transform: uppercase; | ||||||
|  |     font-size: 20px; | ||||||
|  |     color: var(--text-color); | ||||||
|  |     text-align: center; | ||||||
|  |     margin: 0; | ||||||
|  |  | ||||||
|  |     .query { | ||||||
|  |       font-weight: 500; | ||||||
|  |       white-space: pre; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @include mobile { | ||||||
|  |       text-align: left; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .download { | ||||||
|  |     &__icon { | ||||||
|  |       fill: $text-color-70; | ||||||
|  |       height: 1.2rem; | ||||||
|  |  | ||||||
|  |       &:hover { | ||||||
|  |         fill: $text-color; | ||||||
|  |         cursor: pointer; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &.active &__icon { | ||||||
|  |       fill: $green; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | </style> | ||||||
							
								
								
									
										269
									
								
								src/components/torrent/TorrentTable.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,269 @@ | |||||||
|  | <template> | ||||||
|  |   <table> | ||||||
|  |     <thead class="table__header noselect"> | ||||||
|  |       <tr> | ||||||
|  |         <th | ||||||
|  |           v-for="column in columns" | ||||||
|  |           :key="column" | ||||||
|  |           :class="column === selectedColumn ? 'active' : null" | ||||||
|  |           @click="sortTable(column)" | ||||||
|  |         > | ||||||
|  |           {{ column }} | ||||||
|  |           <span v-if="prevCol === column && direction">↑</span> | ||||||
|  |           <span v-if="prevCol === column && !direction">↓</span> | ||||||
|  |         </th> | ||||||
|  |       </tr> | ||||||
|  |     </thead> | ||||||
|  |  | ||||||
|  |     <tbody> | ||||||
|  |       <tr | ||||||
|  |         v-for="torrent in torrents" | ||||||
|  |         :key="torrent.magnet" | ||||||
|  |         class="table__content" | ||||||
|  |       > | ||||||
|  |         <td | ||||||
|  |           @click="expand($event, torrent.name)" | ||||||
|  |           @keydown.enter="expand($event, torrent.name)" | ||||||
|  |         > | ||||||
|  |           {{ torrent.name }} | ||||||
|  |         </td> | ||||||
|  |         <td | ||||||
|  |           @click="expand($event, torrent.name)" | ||||||
|  |           @keydown.enter="expand($event, torrent.name)" | ||||||
|  |         > | ||||||
|  |           {{ torrent.seed }} | ||||||
|  |         </td> | ||||||
|  |         <td | ||||||
|  |           @click="expand($event, torrent.name)" | ||||||
|  |           @keydown.enter="expand($event, torrent.name)" | ||||||
|  |         > | ||||||
|  |           {{ torrent.size }} | ||||||
|  |         </td> | ||||||
|  |         <td | ||||||
|  |           class="download" | ||||||
|  |           @click="() => emit('magnet', torrent)" | ||||||
|  |           @keydown.enter="() => emit('magnet', torrent)" | ||||||
|  |         > | ||||||
|  |           <IconMagnet /> | ||||||
|  |         </td> | ||||||
|  |       </tr> | ||||||
|  |     </tbody> | ||||||
|  |   </table> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { ref, defineProps, defineEmits } from "vue"; | ||||||
|  |   import IconMagnet from "@/icons/IconMagnet.vue"; | ||||||
|  |   import type { Ref } from "vue"; | ||||||
|  |   import { sortableSize } from "../../utils"; | ||||||
|  |   import type ITorrent from "../../interfaces/ITorrent"; | ||||||
|  |  | ||||||
|  |   interface Props { | ||||||
|  |     torrents: Array<ITorrent>; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   interface Emit { | ||||||
|  |     (e: "magnet", torrent: ITorrent): void; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const props = defineProps<Props>(); | ||||||
|  |   const emit = defineEmits<Emit>(); | ||||||
|  |  | ||||||
|  |   const columns: string[] = ["name", "seed", "size", "add"]; | ||||||
|  |  | ||||||
|  |   const torrents: Ref<ITorrent[]> = ref(props.torrents); | ||||||
|  |   const direction: Ref<boolean> = ref(false); | ||||||
|  |   const selectedColumn: Ref<string> = ref(columns[0]); | ||||||
|  |   const prevCol: Ref<string> = ref(""); | ||||||
|  |  | ||||||
|  |   function expand(event: MouseEvent, text: string) { | ||||||
|  |     const elementClicked = event.target as HTMLElement; | ||||||
|  |     const tableRow = elementClicked.parentElement; | ||||||
|  |     const scopedStyleDataVariable = Object.keys(tableRow.dataset)[0]; | ||||||
|  |  | ||||||
|  |     const existingExpandedElement = | ||||||
|  |       document.getElementsByClassName("expanded")[0]; | ||||||
|  |     const clickedSameTwice = | ||||||
|  |       existingExpandedElement?.previousSibling?.isEqualNode(tableRow); | ||||||
|  |  | ||||||
|  |     if (existingExpandedElement) { | ||||||
|  |       existingExpandedElement.remove(); | ||||||
|  |  | ||||||
|  |       // Clicked the same element twice, remove and return | ||||||
|  |       // not recreate and collapse | ||||||
|  |       if (clickedSameTwice) return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const expandedRow = document.createElement("tr"); | ||||||
|  |     const expandedCol = document.createElement("td"); | ||||||
|  |     expandedRow.dataset[scopedStyleDataVariable] = ""; | ||||||
|  |     expandedCol.dataset[scopedStyleDataVariable] = ""; | ||||||
|  |     expandedRow.className = "expanded"; | ||||||
|  |     expandedCol.innerText = text; | ||||||
|  |     expandedCol.colSpan = 4; | ||||||
|  |  | ||||||
|  |     expandedRow.appendChild(expandedCol); | ||||||
|  |     tableRow.insertAdjacentElement("afterend", expandedRow); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function sortName() { | ||||||
|  |     const torrentsCopy = [...torrents.value]; | ||||||
|  |     if (direction.value) { | ||||||
|  |       torrents.value = torrentsCopy.sort((a, b) => (a.name < b.name ? 1 : -1)); | ||||||
|  |     } else { | ||||||
|  |       torrents.value = torrentsCopy.sort((a, b) => (a.name > b.name ? 1 : -1)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function sortSeed() { | ||||||
|  |     const torrentsCopy = [...torrents.value]; | ||||||
|  |     if (direction.value) { | ||||||
|  |       torrents.value = torrentsCopy.sort( | ||||||
|  |         (a, b) => parseInt(a.seed, 10) - parseInt(b.seed, 10) | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       torrents.value = torrentsCopy.sort( | ||||||
|  |         (a, b) => parseInt(b.seed, 10) - parseInt(a.seed, 10) | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function sortSize() { | ||||||
|  |     const torrentsCopy = [...torrents.value]; | ||||||
|  |     if (direction.value) { | ||||||
|  |       torrents.value = torrentsCopy.sort( | ||||||
|  |         (a, b) => sortableSize(a.size) - sortableSize(b.size) | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       torrents.value = torrentsCopy.sort( | ||||||
|  |         (a, b) => sortableSize(b.size) - sortableSize(a.size) | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function sortTable(col, sameDirection = false) { | ||||||
|  |     if (prevCol.value === col && sameDirection === false) { | ||||||
|  |       direction.value = !direction.value; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (col === "name") sortName(); | ||||||
|  |     else if (col === "seed") sortSeed(); | ||||||
|  |     else if (col === "size") sortSize(); | ||||||
|  |  | ||||||
|  |     prevCol.value = col; | ||||||
|  |   } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  |   @import "src/scss/variables"; | ||||||
|  |   @import "src/scss/media-queries"; | ||||||
|  |   @import "src/scss/elements"; | ||||||
|  |  | ||||||
|  |   table { | ||||||
|  |     border-spacing: 0; | ||||||
|  |     margin-top: 0.5rem; | ||||||
|  |     width: 100%; | ||||||
|  |     // border-collapse: collapse; | ||||||
|  |     border-radius: 0.5rem; | ||||||
|  |     overflow: hidden; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   th, | ||||||
|  |   td { | ||||||
|  |     border: 0.5px solid var(--background-color-40); | ||||||
|  |     @include mobile { | ||||||
|  |       white-space: nowrap; | ||||||
|  |       padding: 0; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   thead { | ||||||
|  |     position: relative; | ||||||
|  |     user-select: none; | ||||||
|  |     -webkit-user-select: none; | ||||||
|  |     color: var(--table-header-text-color); | ||||||
|  |     text-transform: uppercase; | ||||||
|  |     cursor: pointer; | ||||||
|  |     background-color: var(--table-background-color); | ||||||
|  |     // background-color: black; | ||||||
|  |     // color: var(--color-green); | ||||||
|  |     letter-spacing: 0.8px; | ||||||
|  |     font-size: 1rem; | ||||||
|  |  | ||||||
|  |     th:last-of-type { | ||||||
|  |       padding-right: 0.4rem; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   tbody { | ||||||
|  |     // first column | ||||||
|  |     tr td:first-of-type { | ||||||
|  |       position: relative; | ||||||
|  |       padding: 0 0.3rem; | ||||||
|  |       cursor: default; | ||||||
|  |       word-break: break-all; | ||||||
|  |       border-left: 1px solid var(--table-background-color); | ||||||
|  |  | ||||||
|  |       @include mobile { | ||||||
|  |         max-width: 40vw; | ||||||
|  |         overflow-x: hidden; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // all columns except first | ||||||
|  |     tr td:not(td:first-of-type) { | ||||||
|  |       text-align: center; | ||||||
|  |       white-space: nowrap; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // last column | ||||||
|  |     tr td:last-of-type { | ||||||
|  |       vertical-align: middle; | ||||||
|  |       cursor: pointer; | ||||||
|  |       border-right: 1px solid var(--table-background-color); | ||||||
|  |  | ||||||
|  |       svg { | ||||||
|  |         width: 21px; | ||||||
|  |         display: block; | ||||||
|  |         margin: auto; | ||||||
|  |         padding: 0.3rem 0; | ||||||
|  |         fill: var(--text-color); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // alternate background color per row | ||||||
|  |     tr:nth-child(even) { | ||||||
|  |       background-color: var(--background-70); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // last element rounded corner border | ||||||
|  |     tr:last-of-type { | ||||||
|  |       td { | ||||||
|  |         border-bottom: 1px solid var(--table-background-color); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       td:first-of-type { | ||||||
|  |         border-bottom-left-radius: 0.5rem; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       td:last-of-type { | ||||||
|  |         border-bottom-right-radius: 0.5rem; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .expanded { | ||||||
|  |     padding: 0.25rem 1rem; | ||||||
|  |     max-width: 100%; | ||||||
|  |     border-left: 1px solid $text-color; | ||||||
|  |     border-right: 1px solid $text-color; | ||||||
|  |     border-bottom: 1px solid $text-color; | ||||||
|  |  | ||||||
|  |     td { | ||||||
|  |       white-space: normal; | ||||||
|  |       word-break: break-all; | ||||||
|  |       padding: 0.5rem 0.15rem; | ||||||
|  |       width: 100%; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | </style> | ||||||
							
								
								
									
										88
									
								
								src/components/torrent/TruncatedTorrentResults.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,88 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <torrent-search-results | ||||||
|  |       :query="query" | ||||||
|  |       :tmdb-id="tmdbId" | ||||||
|  |       :class="{ truncated: truncated }" | ||||||
|  |       ><div | ||||||
|  |         v-if="truncated" | ||||||
|  |         class="load-more" | ||||||
|  |         tabindex="0" | ||||||
|  |         role="button" | ||||||
|  |         @click="truncated = false" | ||||||
|  |         @keydown.enter="truncated = false" | ||||||
|  |       > | ||||||
|  |         <icon-arrow-down /> | ||||||
|  |       </div> | ||||||
|  |     </torrent-search-results> | ||||||
|  |  | ||||||
|  |     <div class="edit-query-btn-container"> | ||||||
|  |       <seasonedButton @click="openInTorrentPage" | ||||||
|  |         >View on torrent page</seasonedButton | ||||||
|  |       > | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { ref, defineProps } from "vue"; | ||||||
|  |   import { useRouter } from "vue-router"; | ||||||
|  |   import TorrentSearchResults from "@/components/torrent/TorrentSearchResults.vue"; | ||||||
|  |   import SeasonedButton from "@/components/ui/SeasonedButton.vue"; | ||||||
|  |   import IconArrowDown from "@/icons/IconArrowDown.vue"; | ||||||
|  |   import type { Ref } from "vue"; | ||||||
|  |  | ||||||
|  |   interface Props { | ||||||
|  |     query: string; | ||||||
|  |     tmdbId?: number; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const props = defineProps<Props>(); | ||||||
|  |   const router = useRouter(); | ||||||
|  |  | ||||||
|  |   const truncated: Ref<boolean> = ref(true); | ||||||
|  |  | ||||||
|  |   function openInTorrentPage() { | ||||||
|  |     if (!props.query?.length) { | ||||||
|  |       router.push("/torrents"); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     router.push({ path: "/torrents", query: { query: props.query } }); | ||||||
|  |   } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  |   :global(.truncated .torrent-table) { | ||||||
|  |     position: relative; | ||||||
|  |     max-height: 500px; | ||||||
|  |     overflow-y: hidden; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .load-more { | ||||||
|  |     position: absolute; | ||||||
|  |     display: flex; | ||||||
|  |     align-items: flex-end; | ||||||
|  |     justify-content: center; | ||||||
|  |     bottom: 0rem; | ||||||
|  |     width: 100%; | ||||||
|  |     height: 3rem; | ||||||
|  |     cursor: pointer; | ||||||
|  |     background: linear-gradient( | ||||||
|  |       to top, | ||||||
|  |       var(--background-color) 20%, | ||||||
|  |       var(--background-0) 100% | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   svg { | ||||||
|  |     height: 30px; | ||||||
|  |     fill: var(--text-color); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .edit-query-btn-container { | ||||||
|  |     display: flex; | ||||||
|  |     justify-content: center; | ||||||
|  |     padding: 1rem; | ||||||
|  |   } | ||||||
|  | </style> | ||||||
| @@ -1,52 +1,48 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="darkToggle"> |   <div class="darkToggle"> | ||||||
|     <span @click="toggleDarkmode">{{ darkmodeToggleIcon }}</span> |     <span @click="toggleDarkmode" @keydown.enter="toggleDarkmode">{{ | ||||||
|  |       darkmodeToggleIcon | ||||||
|  |     }}</span> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| export default { |   import { ref, computed } from "vue"; | ||||||
|   data() { |  | ||||||
|     return { |   function systemDarkModeEnabled() { | ||||||
|       darkmode: this.systemDarkModeEnabled() |     const computedStyle = window.getComputedStyle(document.body); | ||||||
|     }; |     if (computedStyle?.colorScheme != null) { | ||||||
|   }, |       return computedStyle.colorScheme.includes("dark"); | ||||||
|   methods: { |  | ||||||
|     toggleDarkmode() { |  | ||||||
|       this.darkmode = !this.darkmode; |  | ||||||
|       document.body.className = this.darkmode ? "dark" : "light"; |  | ||||||
|     }, |  | ||||||
|     systemDarkModeEnabled() { |  | ||||||
|       const computedStyle = window.getComputedStyle(document.body); |  | ||||||
|       if (computedStyle["colorScheme"] != null) { |  | ||||||
|         return computedStyle.colorScheme.includes("dark"); |  | ||||||
|       } |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   computed: { |  | ||||||
|     darkmodeToggleIcon() { |  | ||||||
|       return this.darkmode ? "🌝" : "🌚"; |  | ||||||
|     } |     } | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const darkmode = ref(systemDarkModeEnabled()); | ||||||
|  |   const darkmodeToggleIcon = computed(() => { | ||||||
|  |     return darkmode.value ? "🌝" : "🌚"; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   function toggleDarkmode() { | ||||||
|  |     darkmode.value = !darkmode.value; | ||||||
|  |     document.body.className = darkmode.value ? "dark" : "light"; | ||||||
|   } |   } | ||||||
| }; |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .darkToggle { |   .darkToggle { | ||||||
|   height: 25px; |     height: 25px; | ||||||
|   width: 25px; |     width: 25px; | ||||||
|   cursor: pointer; |     cursor: pointer; | ||||||
|   position: fixed; |     position: fixed; | ||||||
|   margin-bottom: 1.5rem; |     margin-bottom: 1.5rem; | ||||||
|   margin-right: 2px; |     margin-right: 2px; | ||||||
|   bottom: 0; |     bottom: 0; | ||||||
|   right: 0; |     right: 0; | ||||||
|   z-index: 10; |     z-index: 10; | ||||||
|  |  | ||||||
|   -webkit-user-select: none; |     -webkit-user-select: none; | ||||||
|   -moz-user-select: none; |     -moz-user-select: none; | ||||||
|   -ms-user-select: none; |     -ms-user-select: none; | ||||||
|   user-select: none; |     user-select: none; | ||||||
| } |   } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -2,81 +2,84 @@ | |||||||
|   <div |   <div | ||||||
|     class="nav__hamburger" |     class="nav__hamburger" | ||||||
|     :class="{ open: isOpen }" |     :class="{ open: isOpen }" | ||||||
|  |     tabindex="0" | ||||||
|     @click="toggle" |     @click="toggle" | ||||||
|     @keydown.enter="toggle" |     @keydown.enter="toggle" | ||||||
|     tabindex="0" |  | ||||||
|   > |   > | ||||||
|     <div v-for="(_, index) in 3" :key="index" class="bar"></div> |     <div v-for="(_, index) in 3" :key="index" class="bar"></div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| import { mapGetters, mapActions } from "vuex"; |   import { computed } from "vue"; | ||||||
|  |   import { useStore } from "vuex"; | ||||||
|  |  | ||||||
| export default { |   const store = useStore(); | ||||||
|   computed: { ...mapGetters("hamburger", ["isOpen"]) }, |  | ||||||
|   methods: { ...mapActions("hamburger", ["toggle"]) } |   const isOpen = computed(() => store.getters["hamburger/isOpen"]); | ||||||
| }; |   const toggle = () => { | ||||||
|  |     store.dispatch("hamburger/toggle"); | ||||||
|  |   }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "src/scss/media-queries"; |   @import "src/scss/media-queries"; | ||||||
|  |  | ||||||
| .nav__hamburger { |   .nav__hamburger { | ||||||
|   display: block; |     display: block; | ||||||
|   position: relative; |     position: relative; | ||||||
|   width: var(--header-size); |     width: var(--header-size); | ||||||
|   height: var(--header-size); |     height: var(--header-size); | ||||||
|   cursor: pointer; |     cursor: pointer; | ||||||
|   border-left: 1px solid var(--background-color); |     border-left: 1px solid var(--background-color); | ||||||
|   background-color: var(--background-color-secondary); |     background-color: var(--background-color-secondary); | ||||||
|  |  | ||||||
|   @include tablet-min { |     @include tablet-min { | ||||||
|     display: none; |       display: none; | ||||||
|   } |     } | ||||||
|  |  | ||||||
|   .bar { |  | ||||||
|     position: absolute; |  | ||||||
|     width: 23px; |  | ||||||
|     height: 1px; |  | ||||||
|     background-color: var(--text-color-70); |  | ||||||
|     transition: all 300ms ease; |  | ||||||
|     &:nth-child(1) { |  | ||||||
|       left: 16px; |  | ||||||
|       top: 17px; |  | ||||||
|     } |  | ||||||
|     &:nth-child(2) { |  | ||||||
|       left: 16px; |  | ||||||
|       top: 25px; |  | ||||||
|       &:after { |  | ||||||
|         content: ""; |  | ||||||
|         position: absolute; |  | ||||||
|         left: 0px; |  | ||||||
|         top: 0px; |  | ||||||
|         width: 23px; |  | ||||||
|         height: 1px; |  | ||||||
|         transition: all 300ms ease; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     &:nth-child(3) { |  | ||||||
|       right: 15px; |  | ||||||
|       top: 33px; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   &.open { |  | ||||||
|     .bar { |     .bar { | ||||||
|       &:nth-child(1), |       position: absolute; | ||||||
|       &:nth-child(3) { |       width: 23px; | ||||||
|         width: 0; |       height: 1px; | ||||||
|  |       background-color: var(--text-color-70); | ||||||
|  |       transition: all 300ms ease; | ||||||
|  |       &:nth-child(1) { | ||||||
|  |         left: 16px; | ||||||
|  |         top: 17px; | ||||||
|       } |       } | ||||||
|       &:nth-child(2) { |       &:nth-child(2) { | ||||||
|         transform: rotate(-45deg); |         left: 16px; | ||||||
|  |         top: 25px; | ||||||
|  |         &:after { | ||||||
|  |           content: ""; | ||||||
|  |           position: absolute; | ||||||
|  |           left: 0px; | ||||||
|  |           top: 0px; | ||||||
|  |           width: 23px; | ||||||
|  |           height: 1px; | ||||||
|  |           transition: all 300ms ease; | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|       &:nth-child(2):after { |       &:nth-child(3) { | ||||||
|         transform: rotate(-90deg); |         right: 15px; | ||||||
|         background-color: var(--text-color-70); |         top: 33px; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     &.open { | ||||||
|  |       .bar { | ||||||
|  |         &:nth-child(1), | ||||||
|  |         &:nth-child(3) { | ||||||
|  |           width: 0; | ||||||
|  |         } | ||||||
|  |         &:nth-child(2) { | ||||||
|  |           transform: rotate(-45deg); | ||||||
|  |         } | ||||||
|  |         &:nth-child(2):after { | ||||||
|  |           transform: rotate(-90deg); | ||||||
|  |           background-color: var(--text-color-70); | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,48 +1,68 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="loader"> |   <div :class="`loader type-${type || LoaderHeightType.Page}`"> | ||||||
|     <i class="loader--icon"> |     <i class="loader--icon"> | ||||||
|       <i class="loader--icon-spinner" /> |       <i class="loader--icon-spinner" /> | ||||||
|     </i> |     </i> | ||||||
|   </div> |   </div> | ||||||
| </template> |  | ||||||
|  |   <!-- | ||||||
|  |   TODO: fetch and display movie facts after 1.5 seconds while loading? | ||||||
|  |    | ||||||
|  |  | ||||||
|  | --></template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  |   import { defineProps } from "vue"; | ||||||
|  |   import LoaderHeightType from "../../interfaces/ILoader"; | ||||||
|  |  | ||||||
|  |   interface Props { | ||||||
|  |     type?: LoaderHeightType; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   defineProps<Props>(); | ||||||
|  | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "src/scss/variables"; |   @import "src/scss/variables"; | ||||||
|  |  | ||||||
| .loader { |   .loader { | ||||||
|   display: flex; |     display: flex; | ||||||
|   width: 100%; |     width: 100%; | ||||||
|   height: 30vh; |     height: 30vh; | ||||||
|   justify-content: center; |     justify-content: center; | ||||||
|   align-items: center; |     align-items: center; | ||||||
|  |  | ||||||
|   &--icon { |     &.type-section { | ||||||
|     border: 2px solid $text-color-70; |       height: 15vh; | ||||||
|     border-radius: 50%; |     } | ||||||
|     display: block; |  | ||||||
|     height: 40px; |  | ||||||
|     position: absolute; |  | ||||||
|     width: 40px; |  | ||||||
|  |  | ||||||
|     &-spinner { |     &--icon { | ||||||
|  |       border: 2px solid $text-color-70; | ||||||
|  |       border-radius: 50%; | ||||||
|       display: block; |       display: block; | ||||||
|       animation: load 1s linear infinite; |       height: 40px; | ||||||
|       height: 35px; |       position: absolute; | ||||||
|       width: 35px; |       width: 40px; | ||||||
|       &:after { |  | ||||||
|         border: 7px solid $green-90; |       &-spinner { | ||||||
|         border-radius: 50%; |         display: block; | ||||||
|         content: ""; |         animation: load 1s linear infinite; | ||||||
|         left: 8px; |         height: 35px; | ||||||
|         position: absolute; |         width: 35px; | ||||||
|         top: 22px; |         &:after { | ||||||
|  |           border: 7px solid $green-90; | ||||||
|  |           border-radius: 50%; | ||||||
|  |           content: ""; | ||||||
|  |           left: 8px; | ||||||
|  |           position: absolute; | ||||||
|  |           top: 22px; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     @keyframes load { | ||||||
|  |       100% { | ||||||
|  |         transform: rotate(360deg); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   @keyframes load { |  | ||||||
|     100% { |  | ||||||
|       transform: rotate(360deg); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,34 +1,26 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="text-input__loading" :style="`margin-top: ${top}rem`"> |   <div class="text-input__loading" :style="`margin-top: ${top || 0}rem`"> | ||||||
|     <div |     <div | ||||||
|       class="text-input__loading--line" |       v-for="l in Array(count || 1)" | ||||||
|       :class="lineClass" |  | ||||||
|       v-for="l in Array(count)" |  | ||||||
|       :key="l" |       :key="l" | ||||||
|  |       class="text-input__loading--line" | ||||||
|  |       :class="lineClass || ''" | ||||||
|     ></div> |     ></div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| export default { |   import { defineProps } from "vue"; | ||||||
|   props: { |  | ||||||
|     count: { |   interface Props { | ||||||
|       type: Number, |     count?: number; | ||||||
|       default: 1, |     lineClass?: string; | ||||||
|       require: false |     top?: number; | ||||||
|     }, |  | ||||||
|     lineClass: { |  | ||||||
|       type: String, |  | ||||||
|       default: "" |  | ||||||
|     }, |  | ||||||
|     top: { |  | ||||||
|       type: Number, |  | ||||||
|       default: 0 |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| }; |  | ||||||
|  |   defineProps<Props>(); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "src/scss/loading-placeholder"; |   @import "src/scss/loading-placeholder"; | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,84 +1,77 @@ | |||||||
| <template> | <template> | ||||||
|   <button |   <button | ||||||
|     type="button" |     type="button" | ||||||
|     @click="emit('click')" |  | ||||||
|     :class="{ active: active, fullwidth: fullWidth }" |     :class="{ active: active, fullwidth: fullWidth }" | ||||||
|  |     @click="emit('click')" | ||||||
|   > |   > | ||||||
|     <slot></slot> |     <slot></slot> | ||||||
|   </button> |   </button> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| export default { |   import { defineProps, defineEmits } from "vue"; | ||||||
|   name: "seasonedButton", |  | ||||||
|   props: { |   interface Props { | ||||||
|     active: { |     active?: boolean; | ||||||
|       type: Boolean, |     fullWidth?: boolean; | ||||||
|       default: false, |  | ||||||
|       required: false |  | ||||||
|     }, |  | ||||||
|     fullWidth: { |  | ||||||
|       type: Boolean, |  | ||||||
|       default: false, |  | ||||||
|       required: false |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     emit() { |  | ||||||
|       this.$emit("click"); |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| }; |  | ||||||
|  |   interface Emit { | ||||||
|  |     (e: "click"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   defineProps<Props>(); | ||||||
|  |   const emit = defineEmits<Emit>(); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "src/scss/variables"; |   @import "src/scss/variables"; | ||||||
| @import "src/scss/media-queries"; |   @import "src/scss/media-queries"; | ||||||
|  |  | ||||||
| button { |   button { | ||||||
|   display: inline-block; |     display: inline-block; | ||||||
|   border: 1px solid $text-color; |     border: 1px solid $text-color; | ||||||
|   font-size: 11px; |     font-size: 11px; | ||||||
|   font-weight: 300; |     font-weight: 300; | ||||||
|   line-height: 1.5; |     line-height: 1.5; | ||||||
|   letter-spacing: 0.5px; |     letter-spacing: 0.5px; | ||||||
|   text-transform: uppercase; |     text-transform: uppercase; | ||||||
|   min-height: 45px; |     min-height: 45px; | ||||||
|   padding: 5px 10px 4px 10px; |     padding: 5px 10px 4px 10px; | ||||||
|   margin: 0; |     margin: 0; | ||||||
|   margin-right: 0.3rem; |     margin-right: 0.3rem; | ||||||
|   color: $text-color; |     color: $text-color; | ||||||
|   background: $background-color-secondary; |     background: $background-color-secondary; | ||||||
|   cursor: pointer; |     cursor: pointer; | ||||||
|   outline: none; |     outline: none; | ||||||
|   transition: background 0.5s ease, color 0.5s ease, border-color 0.5s ease; |     transition: 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; | ||||||
|     padding: 6px 20px 5px 20px; |       padding: 6px 20px 5px 20px; | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &.fullwidth { |  | ||||||
|     font-size: 14px; |  | ||||||
|     width: 40%; |  | ||||||
|  |  | ||||||
|     @include mobile { |  | ||||||
|       width: 60%; |  | ||||||
|     } |     } | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &:focus, |     &.fullwidth { | ||||||
|   &:active, |       font-size: 14px; | ||||||
|   &.active { |       width: 40%; | ||||||
|     background: $text-color; |  | ||||||
|     color: $background-color; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @media (hover: hover) { |       @include mobile { | ||||||
|     &:hover { |         width: 60%; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &:focus, | ||||||
|  |     &:active, | ||||||
|  |     &.active { | ||||||
|       background: $text-color; |       background: $text-color; | ||||||
|       color: $background-color; |       color: $background-color; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @media (hover: hover) { | ||||||
|  |       &:hover { | ||||||
|  |         background: $text-color; | ||||||
|  |         color: $background-color; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,134 +1,140 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="group" :class="{ completed: value, focus }"> |   <div class="group" :class="{ completed: modelValue, focus }"> | ||||||
|     <component :is="inputIcon" v-if="inputIcon" /> |     <component :is="inputIcon" v-if="inputIcon" /> | ||||||
|  |  | ||||||
|  |     <!-- eslint-disable-next-line vuejs-accessibility/form-control-has-label --> | ||||||
|     <input |     <input | ||||||
|       class="input" |       class="input" | ||||||
|       :type="tempType || type" |       :type="toggledType || type || 'text'" | ||||||
|       @input="handleInput" |  | ||||||
|       v-model="inputValue" |  | ||||||
|       :placeholder="placeholder" |       :placeholder="placeholder" | ||||||
|       @keyup.enter="event => $emit('enter', event)" |       :value="modelValue" | ||||||
|  |       @input="handleInput" | ||||||
|  |       @keyup.enter="event => emit('enter', event)" | ||||||
|       @focus="focus = true" |       @focus="focus = true" | ||||||
|       @blur="focus = false" |       @blur="focus = false" | ||||||
|     /> |     /> | ||||||
|  |  | ||||||
|     <i |     <i | ||||||
|       v-if="value && type === 'password'" |       v-if="modelValue && type === 'password'" | ||||||
|       @click="toggleShowPassword" |  | ||||||
|       @keydown.enter="toggleShowPassword" |  | ||||||
|       class="show noselect" |       class="show noselect" | ||||||
|       tabindex="0" |       tabindex="0" | ||||||
|       >{{ tempType == "password" ? "show" : "hide" }}</i |       @click="toggleShowPassword" | ||||||
|  |       @keydown.enter="toggleShowPassword" | ||||||
|  |       >{{ toggledType == "password" ? "show" : "hide" }}</i | ||||||
|     > |     > | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| import IconKey from "../../icons/IconKey"; |   import { ref, computed, defineProps, defineEmits } from "vue"; | ||||||
| import IconEmail from "../../icons/IconEmail"; |   import IconKey from "@/icons/IconKey.vue"; | ||||||
|  |   import IconEmail from "@/icons/IconEmail.vue"; | ||||||
|  |   import IconBinoculars from "@/icons/IconBinoculars.vue"; | ||||||
|  |   import type { Ref } from "vue"; | ||||||
|  |  | ||||||
| export default { |   interface Props { | ||||||
|   components: { IconKey, IconEmail }, |     modelValue: string; | ||||||
|   props: { |     placeholder: string; | ||||||
|     placeholder: { type: String }, |     type?: string; | ||||||
|     type: { type: String, default: "text" }, |   } | ||||||
|     value: { type: String, default: undefined } |  | ||||||
|   }, |   interface Emit { | ||||||
|   data() { |     (e: "change"); | ||||||
|     return { |     (e: "enter", event?: KeyboardEvent); | ||||||
|       inputValue: this.value || undefined, |     (e: "update:modelValue", value: string); | ||||||
|       tempType: this.type, |   } | ||||||
|       focus: false |  | ||||||
|     }; |   const props = defineProps<Props>(); | ||||||
|   }, |   const emit = defineEmits<Emit>(); | ||||||
|   computed: { |  | ||||||
|     inputIcon() { |   const toggledType: Ref<string> = ref(props.type); | ||||||
|       if (this.type === "password") return IconKey; |   const focus: Ref<boolean> = ref(false); | ||||||
|       if (this.type === "email") return IconEmail; |  | ||||||
|       return false; |   const inputIcon = computed(() => { | ||||||
|     } |     if (props.type === "password") return IconKey; | ||||||
|   }, |     if (props.type === "email") return IconEmail; | ||||||
|   methods: { |     if (props.type === "torrents") return IconBinoculars; | ||||||
|     handleInput(event) { |     return false; | ||||||
|       if (this.value !== undefined) { |   }); | ||||||
|         this.$emit("update:value", this.inputValue); |  | ||||||
|       } else { |   function handleInput(event: KeyboardEvent) { | ||||||
|         this.$emit("change", this.inputValue, event); |     const target = event?.target as HTMLInputElement; | ||||||
|       } |     if (!target) return; | ||||||
|     }, |  | ||||||
|     toggleShowPassword() { |     emit("update:modelValue", target?.value); | ||||||
|       if (this.tempType === "text") { |   } | ||||||
|         this.tempType = "password"; |  | ||||||
|       } else { |   // Could we move this to component that injects ?? | ||||||
|         this.tempType = "text"; |   function toggleShowPassword() { | ||||||
|       } |     if (toggledType.value === "text") { | ||||||
|  |       toggledType.value = "password"; | ||||||
|  |     } else { | ||||||
|  |       toggledType.value = "text"; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| }; |  | ||||||
| </script> | </script> | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "src/scss/variables"; |   @import "src/scss/variables"; | ||||||
| @import "src/scss/media-queries"; |   @import "src/scss/media-queries"; | ||||||
|  |  | ||||||
| .group { |   .group { | ||||||
|   display: flex; |     display: flex; | ||||||
|   width: 100%; |     width: 100%; | ||||||
|   position: relative; |     position: relative; | ||||||
|   max-width: 35rem; |     max-width: 35rem; | ||||||
|   border: 1px solid var(--text-color-50); |     border: 1px solid var(--text-color-50); | ||||||
|   background-color: var(--background-color-secondary); |     background-color: var(--background-color-secondary); | ||||||
|  |  | ||||||
|   &.completed, |     &.completed, | ||||||
|   &.focus, |     &.focus, | ||||||
|   &:hover, |     &:hover, | ||||||
|   &:focus { |     &:focus { | ||||||
|     border-color: var(--text-color); |       border-color: var(--text-color); | ||||||
|  |  | ||||||
|  |       svg { | ||||||
|  |         fill: var(--text-color); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     svg { |     svg { | ||||||
|       fill: var(--text-color); |       width: 24px; | ||||||
|  |       height: 24px; | ||||||
|  |       fill: var(--text-color-50); | ||||||
|  |       pointer-events: none; | ||||||
|  |       margin-top: 10px; | ||||||
|  |       margin-left: 10px; | ||||||
|  |       z-index: 8; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     input { | ||||||
|  |       width: 100%; | ||||||
|  |       padding: 10px; | ||||||
|  |       outline: none; | ||||||
|  |       background-color: var(--background-color-secondary); | ||||||
|  |       color: var(--text-color); | ||||||
|  |       font-weight: 100; | ||||||
|  |       font-size: 1.2rem; | ||||||
|  |       margin: 0; | ||||||
|  |       z-index: 3; | ||||||
|  |       border: none; | ||||||
|  |  | ||||||
|  |       border-radius: 0; | ||||||
|  |       -webkit-appearance: none; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .show { | ||||||
|  |       position: absolute; | ||||||
|  |       display: grid; | ||||||
|  |       place-items: center; | ||||||
|  |       right: 20px; | ||||||
|  |       z-index: 11; | ||||||
|  |       margin: auto 0; | ||||||
|  |       height: 100%; | ||||||
|  |       font-size: 0.9rem; | ||||||
|  |       cursor: pointer; | ||||||
|  |       color: var(--text-color-50); | ||||||
|  |       -webkit-user-select: none; | ||||||
|  |       user-select: none; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   svg { |  | ||||||
|     width: 24px; |  | ||||||
|     height: 24px; |  | ||||||
|     fill: var(--text-color-50); |  | ||||||
|     pointer-events: none; |  | ||||||
|     margin-top: 10px; |  | ||||||
|     margin-left: 10px; |  | ||||||
|     z-index: 8; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   input { |  | ||||||
|     width: 100%; |  | ||||||
|     padding: 10px; |  | ||||||
|     outline: none; |  | ||||||
|     background-color: var(--background-color-secondary); |  | ||||||
|     color: var(--text-color); |  | ||||||
|     font-weight: 100; |  | ||||||
|     font-size: 1.2rem; |  | ||||||
|     margin: 0; |  | ||||||
|     z-index: 3; |  | ||||||
|     border: none; |  | ||||||
|  |  | ||||||
|     border-radius: 0; |  | ||||||
|     -webkit-appearance: none; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .show { |  | ||||||
|     position: absolute; |  | ||||||
|     display: grid; |  | ||||||
|     place-items: center; |  | ||||||
|     right: 20px; |  | ||||||
|     z-index: 11; |  | ||||||
|     margin: auto 0; |  | ||||||
|     height: 100%; |  | ||||||
|     font-size: 0.9rem; |  | ||||||
|     cursor: pointer; |  | ||||||
|     color: var(--text-color-50); |  | ||||||
|     -webkit-user-select: none; |  | ||||||
|     user-select: none; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,165 +1,173 @@ | |||||||
| <template> | <template> | ||||||
|   <transition-group name="fade"> |   <transition-group name="fade"> | ||||||
|     <div |     <div | ||||||
|       class="message" |       v-for="(message, index) in messages" | ||||||
|       v-for="(message, index) in reversedMessages" |       :key="generateMessageKey(index, message)" | ||||||
|       :key="`${index}-${message.title}-${message.type}}`" |       class="card" | ||||||
|       :class="message.type || 'warning'" |       :class="message.type || 'warning'" | ||||||
|     > |     > | ||||||
|       <span class="pinstripe"></span> |       <span class="pinstripe"></span> | ||||||
|       <div> |       <div class="content"> | ||||||
|         <h2 class="title"> |         <h2 class="title"> | ||||||
|           {{ message.title || defaultTitles[message.type] }} |           {{ message.title || titleFromType(message.type) }} | ||||||
|         </h2> |         </h2> | ||||||
|         <span v-if="message.message" class="message">{{ |         <span v-if="message.message" class="message">{{ | ||||||
|           message.message |           message.message | ||||||
|         }}</span> |         }}</span> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <button class="dismiss" @click="clicked(message)">X</button> |       <button class="dismiss" @click="dismiss(Number(index))">X</button> | ||||||
|     </div> |     </div> | ||||||
|   </transition-group> |   </transition-group> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| export default { |   import { defineProps, defineEmits } from "vue"; | ||||||
|   props: { |   import type { | ||||||
|     messages: { |     ErrorMessageTypes, | ||||||
|       required: true, |     IErrorMessage | ||||||
|       type: Array |   } from "../../interfaces/IErrorMessage"; | ||||||
|     } |  | ||||||
|   }, |   interface Props { | ||||||
|   data() { |     messages: IErrorMessage[]; | ||||||
|     return { |   } | ||||||
|       defaultTitles: { |  | ||||||
|         error: "Unexpected error", |   interface Emit { | ||||||
|         warning: "Something went wrong", |     (e: "update:messages", messages: IErrorMessage[]); | ||||||
|         undefined: "Something went wrong" |   } | ||||||
|       }, |  | ||||||
|       localMessages: [...this.messages] |   const props = defineProps<Props>(); | ||||||
|     }; |   const emit = defineEmits<Emit>(); | ||||||
|   }, |  | ||||||
|   computed: { |   const defaultTitles = { | ||||||
|     reversedMessages() { |     error: "Unexpected error", | ||||||
|       return [...this.messages].reverse(); |     warning: "Something went wrong", | ||||||
|     } |     success: "Success!", | ||||||
|   }, |     undefined: "Something went wrong" | ||||||
|   methods: { |   }; | ||||||
|     clicked(e) { |  | ||||||
|       const removedMessage = [...this.messages].filter(mes => mes !== e); |   function titleFromType(type: ErrorMessageTypes) { | ||||||
|       this.$emit("update:messages", removedMessage); |     return defaultTitles[type]; | ||||||
|     } |   } | ||||||
|  |  | ||||||
|  |   function dismiss(index: number) { | ||||||
|  |     const _messages = [...props.messages]; | ||||||
|  |     _messages.splice(index, 1); | ||||||
|  |     emit("update:messages", _messages); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function generateMessageKey( | ||||||
|  |     index: string | number | symbol, | ||||||
|  |     errorMessage: IErrorMessage | ||||||
|  |   ) { | ||||||
|  |     return `${String(index)}-${errorMessage.title}-${errorMessage.type}`; | ||||||
|   } |   } | ||||||
| }; |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "src/scss/variables"; |   @import "src/scss/variables"; | ||||||
| @import "src/scss/media-queries"; |   @import "src/scss/media-queries"; | ||||||
|  |  | ||||||
| .fade-enter-active { |   .fade-active { | ||||||
|   transition: opacity 0.4s; |     transition: opacity 0.4s; | ||||||
| } |   } | ||||||
| .fade-leave-active { |   .fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ { | ||||||
|   transition: opacity 0.1s; |     opacity: 0; | ||||||
| } |   } | ||||||
| .fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ { |  | ||||||
|   opacity: 0; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .message { |   .card { | ||||||
|   width: 100%; |  | ||||||
|   max-width: 35rem; |  | ||||||
|  |  | ||||||
|   display: flex; |  | ||||||
|   margin-top: 1rem; |  | ||||||
|   margin-bottom: 1rem; |  | ||||||
|   color: $text-color-70; |  | ||||||
|  |  | ||||||
|   > div { |  | ||||||
|     margin: 10px 24px; |  | ||||||
|     width: 100%; |     width: 100%; | ||||||
|   } |     max-width: 35rem; | ||||||
|  |  | ||||||
|   .title { |     display: flex; | ||||||
|     font-weight: 300; |     margin-top: 0.8rem; | ||||||
|     letter-spacing: 0.25px; |  | ||||||
|     margin: 0; |  | ||||||
|     font-size: 1.3rem; |  | ||||||
|     color: $text-color; |  | ||||||
|     transition: color 0.5s ease; |  | ||||||
|   } |  | ||||||
|   .message { |  | ||||||
|     font-weight: 300; |  | ||||||
|     color: $text-color-70; |     color: $text-color-70; | ||||||
|     transition: color 0.5s ease; |  | ||||||
|     margin: 0.2rem 0 0.5rem; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @include mobile-only { |     .content { | ||||||
|     > div { |       margin: 0.4rem 1.2rem; | ||||||
|       margin: 6px 6px; |       width: 100%; | ||||||
|       line-height: 1.3rem; |  | ||||||
|  |       .title { | ||||||
|  |         font-weight: 300; | ||||||
|  |         letter-spacing: 0.25px; | ||||||
|  |         margin: 0; | ||||||
|  |         font-size: 1.3rem; | ||||||
|  |         color: $text-color; | ||||||
|  |         transition: color 0.5s ease; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .message { | ||||||
|  |         font-weight: 400; | ||||||
|  |         font-size: 1.2rem; | ||||||
|  |         color: $text-color-70; | ||||||
|  |         transition: color 0.5s ease; | ||||||
|  |         margin-bottom: 0.2rem; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       @include mobile-only { | ||||||
|  |         margin: 6px 6px; | ||||||
|  |         line-height: 1.3rem; | ||||||
|  |  | ||||||
|  |         h2 { | ||||||
|  |           font-size: 1.1rem; | ||||||
|  |         } | ||||||
|  |         span { | ||||||
|  |           font-size: 0.9rem; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|     h2 { |  | ||||||
|       font-size: 1.1rem; |  | ||||||
|     } |  | ||||||
|     span { |  | ||||||
|       font-size: 0.9rem; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .pinstripe { |  | ||||||
|     width: 0.5rem; |  | ||||||
|     background-color: $color-error-highlight; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .dismiss { |  | ||||||
|     position: relative; |  | ||||||
|     -webkit-appearance: none; |  | ||||||
|     -moz-appearance: none; |  | ||||||
|     background-color: transparent; |  | ||||||
|     border: unset; |  | ||||||
|     font-size: 18px; |  | ||||||
|     cursor: pointer; |  | ||||||
|  |  | ||||||
|     top: 0; |  | ||||||
|     float: right; |  | ||||||
|     height: 1.5rem; |  | ||||||
|     width: 1.5rem; |  | ||||||
|     padding: 0; |  | ||||||
|     margin-top: 0.5rem; |  | ||||||
|     margin-right: 0.5rem; |  | ||||||
|     color: $text-color-70; |  | ||||||
|     transition: color 0.5s ease; |  | ||||||
|  |  | ||||||
|     &:hover { |  | ||||||
|       color: $text-color; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &.success { |  | ||||||
|     background-color: $color-success; |  | ||||||
|  |  | ||||||
|     .pinstripe { |  | ||||||
|       background-color: $color-success-highlight; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &.error { |  | ||||||
|     background-color: $color-error; |  | ||||||
|  |  | ||||||
|     .pinstripe { |     .pinstripe { | ||||||
|  |       width: 0.5rem; | ||||||
|       background-color: $color-error-highlight; |       background-color: $color-error-highlight; | ||||||
|     } |     } | ||||||
|   } |  | ||||||
|  |  | ||||||
|   &.warning { |     .dismiss { | ||||||
|     background-color: $color-warning; |       position: relative; | ||||||
|  |       -webkit-appearance: none; | ||||||
|  |       -moz-appearance: none; | ||||||
|  |       background-color: transparent; | ||||||
|  |       border: unset; | ||||||
|  |       font-size: 18px; | ||||||
|  |       cursor: pointer; | ||||||
|  |  | ||||||
|     .pinstripe { |       top: 0; | ||||||
|       background-color: $color-warning-highlight; |       float: right; | ||||||
|  |       height: 1.5rem; | ||||||
|  |       width: 1.5rem; | ||||||
|  |       padding: 0; | ||||||
|  |       margin-top: 0.5rem; | ||||||
|  |       margin-right: 0.5rem; | ||||||
|  |       color: $text-color-70; | ||||||
|  |       transition: color 0.5s ease; | ||||||
|  |  | ||||||
|  |       &:hover { | ||||||
|  |         color: $text-color; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &.success { | ||||||
|  |       background-color: $color-success; | ||||||
|  |  | ||||||
|  |       .pinstripe { | ||||||
|  |         background-color: $color-success-highlight; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &.error { | ||||||
|  |       background-color: $color-error; | ||||||
|  |  | ||||||
|  |       .pinstripe { | ||||||
|  |         background-color: $color-error-highlight; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     &.warning { | ||||||
|  |       background-color: $color-warning; | ||||||
|  |  | ||||||
|  |       .pinstripe { | ||||||
|  |         background-color: $color-warning-highlight; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -4,83 +4,73 @@ | |||||||
|       v-for="option in options" |       v-for="option in options" | ||||||
|       :key="option" |       :key="option" | ||||||
|       class="toggle-button" |       class="toggle-button" | ||||||
|       @click="toggle(option)" |       :class="selected === option ? 'selected' : null" | ||||||
|       :class="toggleValue === option ? 'selected' : null" |       @click="() => toggleTo(option)" | ||||||
|     > |     > | ||||||
|       {{ option }} |       {{ option }} | ||||||
|     </button> |     </button> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script setup lang="ts"> | ||||||
| export default { |   import { defineProps, defineEmits } from "vue"; | ||||||
|   props: { |  | ||||||
|     options: { |   interface Props { | ||||||
|       Array, |     options: string[]; | ||||||
|       required: true |     selected?: string; | ||||||
|     }, |   } | ||||||
|     selected: { |  | ||||||
|       type: String, |   interface Emit { | ||||||
|       required: false, |     (e: "update:selected", selected: string); | ||||||
|       default: undefined |     (e: "change"); | ||||||
|     } |   } | ||||||
|   }, |  | ||||||
|   data() { |   defineProps<Props>(); | ||||||
|     return { |   const emit = defineEmits<Emit>(); | ||||||
|       toggleValue: this.selected || this.options[0] |  | ||||||
|     }; |   function toggleTo(option: string) { | ||||||
|   }, |     emit("update:selected", option); | ||||||
|   methods: { |     emit("change"); | ||||||
|     toggle(toggleValue) { |  | ||||||
|       this.toggleValue = toggleValue; |  | ||||||
|       if (this.selected !== undefined) { |  | ||||||
|         this.$emit("update:selected", toggleValue); |  | ||||||
|         this.$emit("change", toggleValue); |  | ||||||
|       } else { |  | ||||||
|         this.$emit("change", toggleValue); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| }; |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| @import "src/scss/variables"; |   @import "src/scss/variables"; | ||||||
|  |  | ||||||
| $background: $background-ui; |   $background: $background-ui; | ||||||
| $background-selected: $background-color-secondary; |   $background-selected: $background-color-secondary; | ||||||
|  |  | ||||||
| .toggle-container { |   .toggle-container { | ||||||
|   width: 100%; |     width: 100%; | ||||||
|   display: flex; |     display: flex; | ||||||
|   overflow-x: scroll; |     overflow-x: scroll; | ||||||
|   flex-direction: row; |     flex-direction: row; | ||||||
|   justify-content: center; |     justify-content: center; | ||||||
|   align-items: center; |     align-items: center; | ||||||
|   background-color: $background; |  | ||||||
|   border: 2px solid $background; |  | ||||||
|   border-radius: 8px; |  | ||||||
|   border-left: 4px solid $background; |  | ||||||
|   border-right: 4px solid $background; |  | ||||||
|  |  | ||||||
|   .toggle-button { |  | ||||||
|     font-size: 1rem; |  | ||||||
|     line-height: 1rem; |  | ||||||
|     font-weight: normal; |  | ||||||
|     padding: 0.5rem; |  | ||||||
|     border: 0; |  | ||||||
|     color: $text-color; |  | ||||||
|     background-color: $background; |     background-color: $background; | ||||||
|     text-transform: capitalize; |     border: 2px solid $background; | ||||||
|     cursor: pointer; |     border-radius: 8px; | ||||||
|     display: block; |     border-left: 4px solid $background; | ||||||
|     flex: 1 0 auto; |     border-right: 4px solid $background; | ||||||
|  |  | ||||||
|     &.selected { |     .toggle-button { | ||||||
|  |       font-size: 1rem; | ||||||
|  |       line-height: 1rem; | ||||||
|  |       font-weight: normal; | ||||||
|  |       padding: 0.5rem; | ||||||
|  |       border: 0; | ||||||
|       color: $text-color; |       color: $text-color; | ||||||
|       background-color: $background-selected; |       background-color: $background; | ||||||
|       border-radius: 8px; |       text-transform: capitalize; | ||||||
|  |       cursor: pointer; | ||||||
|  |       display: block; | ||||||
|  |       flex: 1 0 auto; | ||||||
|  |  | ||||||
|  |       &.selected { | ||||||
|  |         color: $text-color; | ||||||
|  |         background-color: $background-selected; | ||||||
|  |         border-radius: 8px; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,9 +0,0 @@ | |||||||
| import type IConfig from "./interfaces/IConfig"; |  | ||||||
|  |  | ||||||
| const config: IConfig = { |  | ||||||
|   SEASONED_URL: "", |  | ||||||
|   ELASTIC_URL: "https://elastic.kevinmidboe.com/", |  | ||||||
|   ELASTIC_INDEX: "shows,movies" |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default config; |  | ||||||
| @@ -1,5 +0,0 @@ | |||||||
| { |  | ||||||
|     "SEASONED_URL": "http://localhost:31459/api/", |  | ||||||
|     "ELASTIC_URL": "http://localhost:9200", |  | ||||||
|     "ELASTIC_INDEX": "shows,movies" |  | ||||||
| } |  | ||||||
							
								
								
									
										19
									
								
								src/icons/BubbleArrowLeft.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,19 @@ | |||||||
|  | <template> | ||||||
|  |   <svg | ||||||
|  |     version="1.1" | ||||||
|  |     xmlns="http://www.w3.org/2000/svg" | ||||||
|  |     width="100%" | ||||||
|  |     height="100%" | ||||||
|  |     viewBox="0 0 24 24" | ||||||
|  |     fill="none" | ||||||
|  |     stroke="currentColor" | ||||||
|  |     stroke-width="2" | ||||||
|  |     stroke-linecap="round" | ||||||
|  |     stroke-linejoin="round" | ||||||
|  |     class="feather feather-arrow-left-circle" | ||||||
|  |   > | ||||||
|  |     <circle cx="12" cy="12" r="10"></circle> | ||||||
|  |     <polyline points="12 8 8 12 12 16"></polyline> | ||||||
|  |     <line x1="16" y1="12" x2="8" y2="12"></line> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										19
									
								
								src/icons/BubbleArrowRight.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,19 @@ | |||||||
|  | <template> | ||||||
|  |   <svg | ||||||
|  |     version="1.1" | ||||||
|  |     xmlns="http://www.w3.org/2000/svg" | ||||||
|  |     width="100%" | ||||||
|  |     height="100%" | ||||||
|  |     viewBox="0 0 24 24" | ||||||
|  |     fill="none" | ||||||
|  |     stroke="currentColor" | ||||||
|  |     stroke-width="2" | ||||||
|  |     stroke-linecap="round" | ||||||
|  |     stroke-linejoin="round" | ||||||
|  |     class="feather feather-arrow-right-circle" | ||||||
|  |   > | ||||||
|  |     <circle cx="12" cy="12" r="10"></circle> | ||||||
|  |     <polyline points="12 16 16 12 12 8"></polyline> | ||||||
|  |     <line x1="8" y1="12" x2="16" y2="12"></line> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
| @@ -1,15 +1,14 @@ | |||||||
| <template> | <template> | ||||||
|   <svg |   <svg | ||||||
|  |     version="1.1" | ||||||
|     xmlns="http://www.w3.org/2000/svg" |     xmlns="http://www.w3.org/2000/svg" | ||||||
|     width="24" |     width="24" | ||||||
|     height="24" |     height="24" | ||||||
|     viewBox="0 0 24 24" |     viewBox="0 0 24 24" | ||||||
|     fill="none" |  | ||||||
|     stroke="currentColor" |     stroke="currentColor" | ||||||
|     stroke-width="2" |     stroke-width="2" | ||||||
|     stroke-linecap="round" |     stroke-linecap="round" | ||||||
|     stroke-linejoin="round" |     stroke-linejoin="round" | ||||||
|     style="transition: stroke-width 0.5s ease" |  | ||||||
|   > |   > | ||||||
|     <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline> |     <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline> | ||||||
|   </svg> |   </svg> | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| <template> | <template> | ||||||
|   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> |   <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       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" | ||||||
|     /> |     /> | ||||||
|   </svg> |   </svg> | ||||||
|   | |||||||
| @@ -1,15 +1,12 @@ | |||||||
| <template> | <template> | ||||||
|   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> |   <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       d="M31.313 18.896v0l-5.071-10.846c-0.004-0.008-0.008-0.017-0.012-0.029-0.542-1.154-1.529-2.021-2.696-2.429l-1.129-1.921c-0.004-0.004-0.008-0.013-0.012-0.017-0.358-0.608-1.021-0.987-1.725-0.987-1.104 0-2 0.896-2 2v2.071c-0.825 0.842-1.333 1.996-1.333 3.263v1.025c-0.392-0.229-0.85-0.358-1.333-0.358s-0.942 0.129-1.333 0.358v-1.025c0-1.267-0.508-2.421-1.333-3.263v-2.071c0-1.104-0.896-2-2-2-0.704 0-1.367 0.379-1.725 0.987-0.004 0.004-0.008 0.013-0.012 0.017l-1.129 1.921c-1.171 0.408-2.158 1.275-2.696 2.429-0.004 0.008-0.008 0.017-0.013 0.025l-5.071 10.85c-0.442 0.946-0.688 1.996-0.688 3.104 0 4.042 3.292 7.333 7.333 7.333 3.942 0 7.167-3.125 7.325-7.029 0.396 0.229 0.85 0.363 1.342 0.363 0.488 0 0.946-0.133 1.342-0.363 0.158 3.904 3.383 7.029 7.325 7.029 4.042 0 7.333-3.292 7.333-7.333 0-1.108-0.246-2.158-0.688-3.104zM26.5 14.9c-0.587-0.15-1.2-0.233-1.833-0.233-1.771 0-3.396 0.629-4.667 1.679v-6.346c0-1.104 0.896-2 2-2 0.767 0 1.471 0.446 1.804 1.133 0.004 0.008 0.008 0.012 0.008 0.021l2.688 5.746zM20.667 4c0.233 0 0.446 0.117 0.567 0.317 0.004 0.004 0.004 0.008 0.008 0.013l0.592 1.008c-0.654 0.025-1.275 0.183-1.833 0.446v-1.117c0-0.367 0.3-0.667 0.667-0.667zM16 12c0.733 0 1.333 0.6 1.333 1.333v4.358c-0.392-0.229-0.85-0.358-1.333-0.358s-0.942 0.129-1.333 0.358v-4.358c0-0.733 0.6-1.333 1.333-1.333zM10.767 4.317c0.121-0.2 0.333-0.317 0.567-0.317 0.367 0 0.667 0.3 0.667 0.667v1.117c-0.558-0.267-1.179-0.425-1.833-0.446l0.592-1.008c0.004-0.004 0.004-0.008 0.008-0.013zM8.188 9.154c0.004-0.008 0.004-0.012 0.008-0.021 0.333-0.688 1.037-1.133 1.804-1.133 1.104 0 2 0.896 2 2v6.346c-1.271-1.050-2.896-1.679-4.667-1.679-0.633 0-1.246 0.079-1.833 0.233l2.688-5.746zM7.333 26.667c-2.575 0-4.667-2.092-4.667-4.667s2.092-4.667 4.667-4.667 4.667 2.092 4.667 4.667-2.092 4.667-4.667 4.667zM16 21.333c-0.733 0-1.333-0.6-1.333-1.333s0.6-1.333 1.333-1.333c0.733 0 1.333 0.6 1.333 1.333s-0.6 1.333-1.333 1.333zM24.667 26.667c-2.575 0-4.667-2.092-4.667-4.667s2.092-4.667 4.667-4.667 4.667 2.092 4.667 4.667-2.092 4.667-4.667 4.667z" |       d="M31.313 18.896v0l-5.071-10.846c-0.004-0.008-0.008-0.017-0.012-0.029-0.542-1.154-1.529-2.021-2.696-2.429l-1.129-1.921c-0.004-0.004-0.008-0.013-0.012-0.017-0.358-0.608-1.021-0.987-1.725-0.987-1.104 0-2 0.896-2 2v2.071c-0.825 0.842-1.333 1.996-1.333 3.263v1.025c-0.392-0.229-0.85-0.358-1.333-0.358s-0.942 0.129-1.333 0.358v-1.025c0-1.267-0.508-2.421-1.333-3.263v-2.071c0-1.104-0.896-2-2-2-0.704 0-1.367 0.379-1.725 0.987-0.004 0.004-0.008 0.013-0.012 0.017l-1.129 1.921c-1.171 0.408-2.158 1.275-2.696 2.429-0.004 0.008-0.008 0.017-0.013 0.025l-5.071 10.85c-0.442 0.946-0.688 1.996-0.688 3.104 0 4.042 3.292 7.333 7.333 7.333 3.942 0 7.167-3.125 7.325-7.029 0.396 0.229 0.85 0.363 1.342 0.363 0.488 0 0.946-0.133 1.342-0.363 0.158 3.904 3.383 7.029 7.325 7.029 4.042 0 7.333-3.292 7.333-7.333 0-1.108-0.246-2.158-0.688-3.104zM26.5 14.9c-0.587-0.15-1.2-0.233-1.833-0.233-1.771 0-3.396 0.629-4.667 1.679v-6.346c0-1.104 0.896-2 2-2 0.767 0 1.471 0.446 1.804 1.133 0.004 0.008 0.008 0.012 0.008 0.021l2.688 5.746zM20.667 4c0.233 0 0.446 0.117 0.567 0.317 0.004 0.004 0.004 0.008 0.008 0.013l0.592 1.008c-0.654 0.025-1.275 0.183-1.833 0.446v-1.117c0-0.367 0.3-0.667 0.667-0.667zM16 12c0.733 0 1.333 0.6 1.333 1.333v4.358c-0.392-0.229-0.85-0.358-1.333-0.358s-0.942 0.129-1.333 0.358v-4.358c0-0.733 0.6-1.333 1.333-1.333zM10.767 4.317c0.121-0.2 0.333-0.317 0.567-0.317 0.367 0 0.667 0.3 0.667 0.667v1.117c-0.558-0.267-1.179-0.425-1.833-0.446l0.592-1.008c0.004-0.004 0.004-0.008 0.008-0.013zM8.188 9.154c0.004-0.008 0.004-0.012 0.008-0.021 0.333-0.688 1.037-1.133 1.804-1.133 1.104 0 2 0.896 2 2v6.346c-1.271-1.050-2.896-1.679-4.667-1.679-0.633 0-1.246 0.079-1.833 0.233l2.688-5.746zM7.333 26.667c-2.575 0-4.667-2.092-4.667-4.667s2.092-4.667 4.667-4.667 4.667 2.092 4.667 4.667-2.092 4.667-4.667 4.667zM16 21.333c-0.733 0-1.333-0.6-1.333-1.333s0.6-1.333 1.333-1.333c0.733 0 1.333 0.6 1.333 1.333s-0.6 1.333-1.333 1.333zM24.667 26.667c-2.575 0-4.667-2.092-4.667-4.667s2.092-4.667 4.667-4.667 4.667 2.092 4.667 4.667-2.092 4.667-4.667 4.667z" | ||||||
|     /> |     /> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       d="M5.333 22v-0.667h-1.333v0.667c0 1.837 1.496 3.333 3.333 3.333h0.667v-1.333h-0.667c-1.104 0-2-0.896-2-2z" |       d="M5.333 22v-0.667h-1.333v0.667c0 1.837 1.496 3.333 3.333 3.333h0.667v-1.333h-0.667c-1.104 0-2-0.896-2-2z" | ||||||
|     /> |     /> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       d="M22.667 22v-0.667h-1.333v0.667c0 1.837 1.496 3.333 3.333 3.333h0.667v-1.333h-0.667c-1.104 0-2-0.896-2-2z" |       d="M22.667 22v-0.667h-1.333v0.667c0 1.837 1.496 3.333 3.333 3.333h0.667v-1.333h-0.667c-1.104 0-2-0.896-2-2z" | ||||||
|     /> |     /> | ||||||
|   </svg> |   </svg> | ||||||
|   | |||||||
| @@ -2,10 +2,11 @@ | |||||||
|   <svg |   <svg | ||||||
|     id="icon-cross" |     id="icon-cross" | ||||||
|     viewBox="0 0 32 32" |     viewBox="0 0 32 32" | ||||||
|  |     version="1.1" | ||||||
|     xmlns="http://www.w3.org/2000/svg" |     xmlns="http://www.w3.org/2000/svg" | ||||||
|  |     style="transition-duration: 0s" | ||||||
|     @click="$emit('click')" |     @click="$emit('click')" | ||||||
|     @keydown="event => $emit('keydown', event)" |     @keydown="event => $emit('keydown', event)" | ||||||
|     style="transition-duration: 0s;" |  | ||||||
|   > |   > | ||||||
|     <path |     <path | ||||||
|       fill="inherit" |       fill="inherit" | ||||||
|   | |||||||
| @@ -1,11 +1,12 @@ | |||||||
| <template> | <template> | ||||||
|   <svg |   <svg | ||||||
|     @click="$emit('click')" |     version="1.1" | ||||||
|     xmlns="http://www.w3.org/2000/svg" |     xmlns="http://www.w3.org/2000/svg" | ||||||
|     viewBox="0 0 32 32" |     viewBox="0 0 32 32" | ||||||
|  |     @click="$emit('click')" | ||||||
|  |     @keydown.enter="$emit('click')" | ||||||
|   > |   > | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       d="M30.229 1.771c-1.142-1.142-2.658-1.771-4.275-1.771s-3.133 0.629-4.275 1.771l-18.621 18.621c-0.158 0.158-0.275 0.358-0.337 0.575l-2.667 9.333c-0.133 0.467-0.004 0.967 0.338 1.308 0.254 0.254 0.596 0.392 0.942 0.392 0.121 0 0.246-0.017 0.367-0.050l9.333-2.667c0.217-0.063 0.417-0.179 0.575-0.337l18.621-18.621c2.358-2.362 2.358-6.196 0-8.554zM6.079 21.137l14.392-14.392 4.779 4.779-14.387 14.396-4.783-4.783zM21.413 5.804l1.058-1.058 4.779 4.779-1.058 1.058-4.779-4.779zM5.167 22.108l4.725 4.725-6.617 1.892 1.892-6.617zM28.346 8.438l-0.15 0.15-4.783-4.783 0.15-0.15c0.642-0.637 1.488-0.988 2.392-0.988s1.75 0.35 2.392 0.992c1.317 1.317 1.317 3.458 0 4.779z" |       d="M30.229 1.771c-1.142-1.142-2.658-1.771-4.275-1.771s-3.133 0.629-4.275 1.771l-18.621 18.621c-0.158 0.158-0.275 0.358-0.337 0.575l-2.667 9.333c-0.133 0.467-0.004 0.967 0.338 1.308 0.254 0.254 0.596 0.392 0.942 0.392 0.121 0 0.246-0.017 0.367-0.050l9.333-2.667c0.217-0.063 0.417-0.179 0.575-0.337l18.621-18.621c2.358-2.362 2.358-6.196 0-8.554zM6.079 21.137l14.392-14.392 4.779 4.779-14.387 14.396-4.783-4.783zM21.413 5.804l1.058-1.058 4.779 4.779-1.058 1.058-4.779-4.779zM5.167 22.108l4.725 4.725-6.617 1.892 1.892-6.617zM28.346 8.438l-0.15 0.15-4.783-4.783 0.15-0.15c0.642-0.637 1.488-0.988 2.392-0.988s1.75 0.35 2.392 0.992c1.317 1.317 1.317 3.458 0 4.779z" | ||||||
|     /> |     /> | ||||||
|   </svg> |   </svg> | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| <template> | <template> | ||||||
|   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> |   <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       d="M30.742 9.771c-0.804-1.904-1.958-3.617-3.429-5.083-1.471-1.471-3.179-2.621-5.083-3.429-1.975-0.833-4.071-1.258-6.229-1.258s-4.254 0.425-6.229 1.258c-1.904 0.804-3.617 1.958-5.083 3.429-1.471 1.471-2.621 3.179-3.429 5.083-0.833 1.975-1.258 4.071-1.258 6.229s0.425 4.254 1.258 6.229c0.804 1.904 1.958 3.617 3.429 5.083 1.471 1.471 3.179 2.621 5.083 3.429 1.975 0.833 4.071 1.258 6.229 1.258h6.667v-2.667h-6.667c-7.35 0-13.333-5.983-13.333-13.333s5.983-13.333 13.333-13.333c7.35 0 13.333 5.983 13.333 13.333v0.667c0 1.837-1.496 3.333-3.333 3.333s-3.333-1.496-3.333-3.333v-7.333h-2.667v1.338c-1.117-0.838-2.5-1.338-4-1.338-3.675 0-6.667 2.992-6.667 6.667s2.992 6.667 6.667 6.667c2.079 0 3.938-0.958 5.162-2.454 1.092 1.488 2.854 2.454 4.837 2.454 3.308 0 6-2.692 6-6v-0.667c0-2.158-0.425-4.254-1.258-6.229zM16 20c-2.204 0-4-1.796-4-4s1.796-4 4-4 4 1.796 4 4-1.796 4-4 4z" |       d="M30.742 9.771c-0.804-1.904-1.958-3.617-3.429-5.083-1.471-1.471-3.179-2.621-5.083-3.429-1.975-0.833-4.071-1.258-6.229-1.258s-4.254 0.425-6.229 1.258c-1.904 0.804-3.617 1.958-5.083 3.429-1.471 1.471-2.621 3.179-3.429 5.083-0.833 1.975-1.258 4.071-1.258 6.229s0.425 4.254 1.258 6.229c0.804 1.904 1.958 3.617 3.429 5.083 1.471 1.471 3.179 2.621 5.083 3.429 1.975 0.833 4.071 1.258 6.229 1.258h6.667v-2.667h-6.667c-7.35 0-13.333-5.983-13.333-13.333s5.983-13.333 13.333-13.333c7.35 0 13.333 5.983 13.333 13.333v0.667c0 1.837-1.496 3.333-3.333 3.333s-3.333-1.496-3.333-3.333v-7.333h-2.667v1.338c-1.117-0.838-2.5-1.338-4-1.338-3.675 0-6.667 2.992-6.667 6.667s2.992 6.667 6.667 6.667c2.079 0 3.938-0.958 5.162-2.454 1.092 1.488 2.854 2.454 4.837 2.454 3.308 0 6-2.692 6-6v-0.667c0-2.158-0.425-4.254-1.258-6.229zM16 20c-2.204 0-4-1.796-4-4s1.796-4 4-4 4 1.796 4 4-1.796 4-4 4z" | ||||||
|     /> |     /> | ||||||
|   </svg> |   </svg> | ||||||
|   | |||||||
| @@ -4,18 +4,18 @@ | |||||||
|     viewBox="0 0 32 32" |     viewBox="0 0 32 32" | ||||||
|     width="100%" |     width="100%" | ||||||
|     height="100%" |     height="100%" | ||||||
|     style="transition-duration: 0s;" |     style="transition-duration: 0s" | ||||||
|   > |   > | ||||||
|     <path |     <path | ||||||
|       style="transition-duration: 0s;" |       style="transition-duration: 0s" | ||||||
|       d="M29.333 2.667h-26.667c-1.471 0-2.667 1.196-2.667 2.667v21.333c0 1.471 1.196 2.667 2.667 2.667h26.667c1.471 0 2.667-1.196 2.667-2.667v-21.333c0-1.471-1.196-2.667-2.667-2.667zM29.333 26.667h-26.667v-21.333h26.667v21.333c0.004 0 0 0 0 0z" |       d="M29.333 2.667h-26.667c-1.471 0-2.667 1.196-2.667 2.667v21.333c0 1.471 1.196 2.667 2.667 2.667h26.667c1.471 0 2.667-1.196 2.667-2.667v-21.333c0-1.471-1.196-2.667-2.667-2.667zM29.333 26.667h-26.667v-21.333h26.667v21.333c0.004 0 0 0 0 0z" | ||||||
|     ></path> |     ></path> | ||||||
|     <path |     <path | ||||||
|       style="transition-duration: 0s;" |       style="transition-duration: 0s" | ||||||
|       d="M11.333 17.058l-4.667 4.667v-1.725h-1.333v3.333c0 0.367 0.3 0.667 0.667 0.667h3.333v-1.333h-1.725l4.667-4.667-0.942-0.942z" |       d="M11.333 17.058l-4.667 4.667v-1.725h-1.333v3.333c0 0.367 0.3 0.667 0.667 0.667h3.333v-1.333h-1.725l4.667-4.667-0.942-0.942z" | ||||||
|     ></path> |     ></path> | ||||||
|     <path |     <path | ||||||
|       style="transition-duration: 0s;" |       style="transition-duration: 0s" | ||||||
|       d="M26 8h-3.333v1.333h1.725l-4.667 4.667 0.942 0.942 4.667-4.667v1.725h1.333v-3.333c0-0.367-0.3-0.667-0.667-0.667z" |       d="M26 8h-3.333v1.333h1.725l-4.667 4.667 0.942 0.942 4.667-4.667v1.725h1.333v-3.333c0-0.367-0.3-0.667-0.667-0.667z" | ||||||
|     ></path> |     ></path> | ||||||
|   </svg> |   </svg> | ||||||
|   | |||||||
| @@ -2,13 +2,12 @@ | |||||||
|   <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> |   <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|     <path |     <path | ||||||
|       fill="inherit" |       fill="inherit" | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       d="M31.671 16.054v0l-6.421-11.708c-0.117-0.213-0.342-0.346-0.583-0.346h-17.333c-0.242 0-0.467 0.133-0.583 0.346l-6.421 11.708c-0.208 0.379-0.329 0.817-0.329 1.279v8c0 1.471 1.196 2.667 2.667 2.667h26.667c1.471 0 2.667-1.196 2.667-2.667v-8c0-0.462-0.121-0.9-0.329-1.279zM29.333 25.333h-26.667v-8h8.167c0.592 2.296 2.683 4 5.167 4 2.479 0 4.571-1.704 5.167-4h8.167v8zM20.667 14.667c-0.367 0-0.667 0.3-0.667 0.667v0.667c0 2.204-1.796 4-4 4s-4-1.796-4-4v-0.667c0-0.367-0.3-0.667-0.667-0.667h-8.667c-0.021 0-0.038 0-0.058 0l5.121-9.333h16.546l5.121 9.333c-0.021 0-0.038 0-0.058 0h-8.671z" |       d="M31.671 16.054v0l-6.421-11.708c-0.117-0.213-0.342-0.346-0.583-0.346h-17.333c-0.242 0-0.467 0.133-0.583 0.346l-6.421 11.708c-0.208 0.379-0.329 0.817-0.329 1.279v8c0 1.471 1.196 2.667 2.667 2.667h26.667c1.471 0 2.667-1.196 2.667-2.667v-8c0-0.462-0.121-0.9-0.329-1.279zM29.333 25.333h-26.667v-8h8.167c0.592 2.296 2.683 4 5.167 4 2.479 0 4.571-1.704 5.167-4h8.167v8zM20.667 14.667c-0.367 0-0.667 0.3-0.667 0.667v0.667c0 2.204-1.796 4-4 4s-4-1.796-4-4v-0.667c0-0.367-0.3-0.667-0.667-0.667h-8.667c-0.021 0-0.038 0-0.058 0l5.121-9.333h16.546l5.121 9.333c-0.021 0-0.038 0-0.058 0h-8.671z" | ||||||
|     /> |     /> | ||||||
|   </svg> |   </svg> | ||||||
|  |  | ||||||
|   <!--   <svg |   <!--   <svg | ||||||
|     xmlns="http://www.w3.org/2000/svg" |     version="1.1" xmlns="http://www.w3.org/2000/svg" | ||||||
|     width="24" |     width="24" | ||||||
|     height="24" |     height="24" | ||||||
|     viewBox="0 0 24 24" |     viewBox="0 0 24 24" | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| <template> | <template> | ||||||
|   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> |   <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       d="M30.742 9.771c-0.804-1.904-1.958-3.617-3.429-5.083-1.471-1.471-3.179-2.621-5.083-3.429-1.975-0.833-4.071-1.258-6.229-1.258s-4.254 0.425-6.229 1.258c-1.904 0.804-3.617 1.958-5.083 3.429-1.471 1.471-2.621 3.179-3.429 5.083-0.833 1.975-1.258 4.071-1.258 6.229s0.425 4.254 1.258 6.229c0.804 1.904 1.958 3.617 3.429 5.083 1.471 1.471 3.179 2.621 5.083 3.429 1.975 0.833 4.071 1.258 6.229 1.258s4.254-0.425 6.229-1.258c1.904-0.804 3.617-1.958 5.083-3.429 1.471-1.471 2.621-3.179 3.429-5.083 0.833-1.975 1.258-4.071 1.258-6.229s-0.425-4.254-1.258-6.229zM27.438 9.15c-0.171 0.083-0.342 0.192-0.508 0.321l-0.087 0.067-0.063 0.092c-0.625 0.925-1.604 1.208-2.254 1.008-0.35-0.108-0.529-0.321-0.529-0.637 0-0.9-0.479-1.6-0.863-2.167-0.333-0.492-0.533-0.804-0.467-1.033 0.058-0.2 0.358-0.612 1.629-1.233 1.25 0.996 2.317 2.213 3.142 3.583zM16 2.667c0.271 0 0.538 0.008 0.8 0.025-0.133 0.087-0.292 0.158-0.471 0.242-0.521 0.237-1.238 0.567-1.613 1.483l-0.012 0.025-0.008 0.025c-0.858 2.717-2.008 3.783-2.771 4.487-0.546 0.504-1.017 0.942-1.025 1.667-0.008 0.629 0.329 1.296 1.242 2.458 0.729 0.929 1.429 1.4 2.142 1.45 0.9 0.058 1.533-0.558 2.046-1.054 0.258-0.25 0.525-0.512 0.725-0.579 0.054-0.017 0.175-0.058 0.479 0.242 1.108 1.108 2.012 1.4 2.675 1.617 0.613 0.2 0.892 0.292 1.204 0.887 0.521 0.987 1.308 1.371 1.883 1.65 0.629 0.304 0.708 0.383 0.708 0.708 0 0.212 0.008 0.442 0.012 0.679 0.025 0.85 0.058 2.137-0.325 2.533-0.054 0.058-0.146 0.121-0.354 0.121-1.329 0-1.863 1.183-2.217 1.967-0.1 0.225-0.267 0.587-0.375 0.704-0.871-0.121-1.938 0.875-3.592 2.492-0.367 0.358-0.85 0.833-1.233 1.167 0.042-0.442 0.142-1.104 0.358-2.046 0.292-1.279 0.708-2.658 1.008-3.354 0.196-0.454 0.25-1.163-0.608-1.942-0.462-0.417-1.1-0.792-1.721-1.154-0.446-0.258-0.863-0.504-1.183-0.746-0.358-0.271-0.425-0.408-0.433-0.433 0-0.246 0.046-0.525 0.088-0.821 0.171-1.113 0.425-2.796-1.821-3.779-0.233-0.104-0.479-0.2-0.713-0.292-1.879-0.742-3.817-1.508-4.162-6.667 2.392-2.325 5.662-3.763 9.267-3.763zM3.817 21.413c0.796 0.137 1.375-0.446 1.804-0.875 0.142-0.142 0.438-0.438 0.558-0.467 0.058 0.025 0.479 0.25 1.204 2.167 0.425 1.125 0.446 2.283 0.467 3.621 0.004 0.225 0.008 0.454 0.013 0.696-1.742-1.346-3.142-3.113-4.046-5.142zM9.229 27.483c-0.033-0.571-0.042-1.117-0.054-1.65-0.025-1.404-0.046-2.725-0.554-4.071-0.742-1.958-1.371-2.825-2.175-3-0.779-0.167-1.354 0.413-1.775 0.833-0.171 0.171-0.521 0.521-0.633 0.5-0.046-0.008-0.446-0.137-1.163-1.742-0.138-0.767-0.208-1.55-0.208-2.354 0-3.104 1.067-5.967 2.854-8.233 0.242 1.792 0.721 3.175 1.446 4.196 1.004 1.417 2.296 1.925 3.433 2.375 0.233 0.092 0.454 0.179 0.671 0.275 1.308 0.571 1.208 1.242 1.037 2.354-0.050 0.337-0.104 0.688-0.104 1.033 0 0.988 1.108 1.633 2.279 2.321 0.558 0.329 1.137 0.667 1.496 0.992 0.308 0.279 0.292 0.396 0.279 0.425-0.35 0.813-0.817 2.367-1.129 3.783-0.171 0.767-0.283 1.45-0.338 1.979-0.075 0.779-0.017 1.242 0.192 1.546 0.075 0.108 0.175 0.196 0.287 0.258-2.121-0.15-4.108-0.796-5.842-1.821zM16 29.333c-0.067 0-0.129 0-0.196 0 0.525-0.188 1.167-0.8 2.275-1.883 0.533-0.521 1.083-1.058 1.571-1.475 0.613-0.521 0.871-0.625 0.942-0.642 0.288 0.042 0.788 0.012 1.212-0.525 0.217-0.271 0.367-0.604 0.525-0.958 0.375-0.833 0.6-1.179 1.004-1.179 0.521 0 0.975-0.183 1.308-0.525 0.779-0.8 0.738-2.233 0.704-3.5-0.008-0.229-0.012-0.446-0.012-0.642 0-1.204-0.846-1.613-1.462-1.908-0.496-0.242-0.967-0.467-1.283-1.067-0.567-1.079-1.283-1.313-1.975-1.537-0.592-0.192-1.262-0.412-2.146-1.292-0.587-0.588-1.208-0.779-1.846-0.563-0.488 0.162-0.863 0.529-1.229 0.887-0.371 0.358-0.717 0.7-1.025 0.679-0.175-0.012-0.558-0.15-1.183-0.942-0.637-0.813-0.963-1.354-0.958-1.613 0.004-0.15 0.229-0.367 0.6-0.708 0.808-0.75 2.162-2.004 3.129-5.033 0.167-0.392 0.438-0.529 0.925-0.754 0.5-0.229 1.125-0.517 1.483-1.267 1.704 0.308 3.296 0.938 4.708 1.825-0.988 0.563-1.508 1.108-1.688 1.729-0.246 0.846 0.229 1.542 0.646 2.154 0.325 0.475 0.633 0.925 0.633 1.412 0 0.9 0.563 1.633 1.471 1.912 0.246 0.075 0.521 0.117 0.808 0.117 0.958 0 2.079-0.446 2.875-1.554 0.087-0.063 0.171-0.108 0.246-0.146 0.817 1.713 1.271 3.633 1.271 5.662 0 7.35-5.983 13.333-13.333 13.333z" |       d="M30.742 9.771c-0.804-1.904-1.958-3.617-3.429-5.083-1.471-1.471-3.179-2.621-5.083-3.429-1.975-0.833-4.071-1.258-6.229-1.258s-4.254 0.425-6.229 1.258c-1.904 0.804-3.617 1.958-5.083 3.429-1.471 1.471-2.621 3.179-3.429 5.083-0.833 1.975-1.258 4.071-1.258 6.229s0.425 4.254 1.258 6.229c0.804 1.904 1.958 3.617 3.429 5.083 1.471 1.471 3.179 2.621 5.083 3.429 1.975 0.833 4.071 1.258 6.229 1.258s4.254-0.425 6.229-1.258c1.904-0.804 3.617-1.958 5.083-3.429 1.471-1.471 2.621-3.179 3.429-5.083 0.833-1.975 1.258-4.071 1.258-6.229s-0.425-4.254-1.258-6.229zM27.438 9.15c-0.171 0.083-0.342 0.192-0.508 0.321l-0.087 0.067-0.063 0.092c-0.625 0.925-1.604 1.208-2.254 1.008-0.35-0.108-0.529-0.321-0.529-0.637 0-0.9-0.479-1.6-0.863-2.167-0.333-0.492-0.533-0.804-0.467-1.033 0.058-0.2 0.358-0.612 1.629-1.233 1.25 0.996 2.317 2.213 3.142 3.583zM16 2.667c0.271 0 0.538 0.008 0.8 0.025-0.133 0.087-0.292 0.158-0.471 0.242-0.521 0.237-1.238 0.567-1.613 1.483l-0.012 0.025-0.008 0.025c-0.858 2.717-2.008 3.783-2.771 4.487-0.546 0.504-1.017 0.942-1.025 1.667-0.008 0.629 0.329 1.296 1.242 2.458 0.729 0.929 1.429 1.4 2.142 1.45 0.9 0.058 1.533-0.558 2.046-1.054 0.258-0.25 0.525-0.512 0.725-0.579 0.054-0.017 0.175-0.058 0.479 0.242 1.108 1.108 2.012 1.4 2.675 1.617 0.613 0.2 0.892 0.292 1.204 0.887 0.521 0.987 1.308 1.371 1.883 1.65 0.629 0.304 0.708 0.383 0.708 0.708 0 0.212 0.008 0.442 0.012 0.679 0.025 0.85 0.058 2.137-0.325 2.533-0.054 0.058-0.146 0.121-0.354 0.121-1.329 0-1.863 1.183-2.217 1.967-0.1 0.225-0.267 0.587-0.375 0.704-0.871-0.121-1.938 0.875-3.592 2.492-0.367 0.358-0.85 0.833-1.233 1.167 0.042-0.442 0.142-1.104 0.358-2.046 0.292-1.279 0.708-2.658 1.008-3.354 0.196-0.454 0.25-1.163-0.608-1.942-0.462-0.417-1.1-0.792-1.721-1.154-0.446-0.258-0.863-0.504-1.183-0.746-0.358-0.271-0.425-0.408-0.433-0.433 0-0.246 0.046-0.525 0.088-0.821 0.171-1.113 0.425-2.796-1.821-3.779-0.233-0.104-0.479-0.2-0.713-0.292-1.879-0.742-3.817-1.508-4.162-6.667 2.392-2.325 5.662-3.763 9.267-3.763zM3.817 21.413c0.796 0.137 1.375-0.446 1.804-0.875 0.142-0.142 0.438-0.438 0.558-0.467 0.058 0.025 0.479 0.25 1.204 2.167 0.425 1.125 0.446 2.283 0.467 3.621 0.004 0.225 0.008 0.454 0.013 0.696-1.742-1.346-3.142-3.113-4.046-5.142zM9.229 27.483c-0.033-0.571-0.042-1.117-0.054-1.65-0.025-1.404-0.046-2.725-0.554-4.071-0.742-1.958-1.371-2.825-2.175-3-0.779-0.167-1.354 0.413-1.775 0.833-0.171 0.171-0.521 0.521-0.633 0.5-0.046-0.008-0.446-0.137-1.163-1.742-0.138-0.767-0.208-1.55-0.208-2.354 0-3.104 1.067-5.967 2.854-8.233 0.242 1.792 0.721 3.175 1.446 4.196 1.004 1.417 2.296 1.925 3.433 2.375 0.233 0.092 0.454 0.179 0.671 0.275 1.308 0.571 1.208 1.242 1.037 2.354-0.050 0.337-0.104 0.688-0.104 1.033 0 0.988 1.108 1.633 2.279 2.321 0.558 0.329 1.137 0.667 1.496 0.992 0.308 0.279 0.292 0.396 0.279 0.425-0.35 0.813-0.817 2.367-1.129 3.783-0.171 0.767-0.283 1.45-0.338 1.979-0.075 0.779-0.017 1.242 0.192 1.546 0.075 0.108 0.175 0.196 0.287 0.258-2.121-0.15-4.108-0.796-5.842-1.821zM16 29.333c-0.067 0-0.129 0-0.196 0 0.525-0.188 1.167-0.8 2.275-1.883 0.533-0.521 1.083-1.058 1.571-1.475 0.613-0.521 0.871-0.625 0.942-0.642 0.288 0.042 0.788 0.012 1.212-0.525 0.217-0.271 0.367-0.604 0.525-0.958 0.375-0.833 0.6-1.179 1.004-1.179 0.521 0 0.975-0.183 1.308-0.525 0.779-0.8 0.738-2.233 0.704-3.5-0.008-0.229-0.012-0.446-0.012-0.642 0-1.204-0.846-1.613-1.462-1.908-0.496-0.242-0.967-0.467-1.283-1.067-0.567-1.079-1.283-1.313-1.975-1.537-0.592-0.192-1.262-0.412-2.146-1.292-0.587-0.588-1.208-0.779-1.846-0.563-0.488 0.162-0.863 0.529-1.229 0.887-0.371 0.358-0.717 0.7-1.025 0.679-0.175-0.012-0.558-0.15-1.183-0.942-0.637-0.813-0.963-1.354-0.958-1.613 0.004-0.15 0.229-0.367 0.6-0.708 0.808-0.75 2.162-2.004 3.129-5.033 0.167-0.392 0.438-0.529 0.925-0.754 0.5-0.229 1.125-0.517 1.483-1.267 1.704 0.308 3.296 0.938 4.708 1.825-0.988 0.563-1.508 1.108-1.688 1.729-0.246 0.846 0.229 1.542 0.646 2.154 0.325 0.475 0.633 0.925 0.633 1.412 0 0.9 0.563 1.633 1.471 1.912 0.246 0.075 0.521 0.117 0.808 0.117 0.958 0 2.079-0.446 2.875-1.554 0.087-0.063 0.171-0.108 0.246-0.146 0.817 1.713 1.271 3.633 1.271 5.662 0 7.35-5.983 13.333-13.333 13.333z" | ||||||
|     /> |     /> | ||||||
|   </svg> |   </svg> | ||||||
|   | |||||||
| @@ -1,19 +1,15 @@ | |||||||
| <template> | <template> | ||||||
|   <svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"> |   <svg viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       d="M24 13.333v-2.667c0-2.137-0.833-4.146-2.342-5.658s-3.521-2.342-5.658-2.342-4.146 0.833-5.658 2.342-2.342 3.521-2.342 5.658v2.667c-1.471 0-2.667 1.196-2.667 2.667v10.667c0 1.471 1.196 2.667 2.667 2.667h16c1.471 0 2.667-1.196 2.667-2.667v-10.667c0-1.471-1.196-2.667-2.667-2.667zM10.667 10.667c0-2.942 2.392-5.333 5.333-5.333s5.333 2.392 5.333 5.333v2.667h-10.667v-2.667zM24 26.667h-16v-10.667h16v10.667c0.004 0 0 0 0 0z" |       d="M24 13.333v-2.667c0-2.137-0.833-4.146-2.342-5.658s-3.521-2.342-5.658-2.342-4.146 0.833-5.658 2.342-2.342 3.521-2.342 5.658v2.667c-1.471 0-2.667 1.196-2.667 2.667v10.667c0 1.471 1.196 2.667 2.667 2.667h16c1.471 0 2.667-1.196 2.667-2.667v-10.667c0-1.471-1.196-2.667-2.667-2.667zM10.667 10.667c0-2.942 2.392-5.333 5.333-5.333s5.333 2.392 5.333 5.333v2.667h-10.667v-2.667zM24 26.667h-16v-10.667h16v10.667c0.004 0 0 0 0 0z" | ||||||
|     /> |     /> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       d="M12 20c-0.733 0-1.333 0.6-1.333 1.333s0.6 1.333 1.333 1.333 1.333-0.6 1.333-1.333-0.6-1.333-1.333-1.333zM12 21.333c0 0 0 0 0 0v0z" |       d="M12 20c-0.733 0-1.333 0.6-1.333 1.333s0.6 1.333 1.333 1.333 1.333-0.6 1.333-1.333-0.6-1.333-1.333-1.333zM12 21.333c0 0 0 0 0 0v0z" | ||||||
|     /> |     /> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       d="M16 20c-0.733 0-1.333 0.6-1.333 1.333s0.6 1.333 1.333 1.333c0.733 0 1.333-0.6 1.333-1.333s-0.6-1.333-1.333-1.333zM16 21.333c0 0 0 0 0 0v0z" |       d="M16 20c-0.733 0-1.333 0.6-1.333 1.333s0.6 1.333 1.333 1.333c0.733 0 1.333-0.6 1.333-1.333s-0.6-1.333-1.333-1.333zM16 21.333c0 0 0 0 0 0v0z" | ||||||
|     /> |     /> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       d="M20 20c-0.733 0-1.333 0.6-1.333 1.333s0.6 1.333 1.333 1.333 1.333-0.6 1.333-1.333-0.6-1.333-1.333-1.333zM20 21.333c0 0 0 0 0 0v0z" |       d="M20 20c-0.733 0-1.333 0.6-1.333 1.333s0.6 1.333 1.333 1.333 1.333-0.6 1.333-1.333-0.6-1.333-1.333-1.333zM20 21.333c0 0 0 0 0 0v0z" | ||||||
|     /> |     /> | ||||||
|   </svg> |   </svg> | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| <template> | <template> | ||||||
|   <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> |   <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       d="M31.608 13.725l-4-4c-0.304-0.304-0.729-0.442-1.154-0.375-0.421 0.067-0.788 0.333-0.979 0.713-2.938 5.804-5.517 9.617-8.354 12.358-0.004 0.004-0.012 0.012-0.017 0.017-1.008 1.008-2.346 1.563-3.771 1.563s-2.762-0.554-3.771-1.563c-1.008-1.008-1.563-2.346-1.563-3.771s0.554-2.762 1.563-3.771c0.004-0.004 0.012-0.012 0.017-0.017 2.742-2.842 6.55-5.417 12.358-8.354 0.383-0.192 0.646-0.558 0.712-0.979s-0.071-0.85-0.375-1.154l-4-4c-0.404-0.404-1.021-0.504-1.538-0.25-2.271 1.125-4.475 2.438-6.554 3.9-2.175 1.529-4.279 3.271-6.258 5.179-0.004 0.004-0.013 0.012-0.017 0.017-2.521 2.521-3.908 5.867-3.908 9.429s1.387 6.908 3.904 9.429c2.521 2.517 5.867 3.904 9.429 3.904s6.908-1.387 9.429-3.904c0.004-0.004 0.012-0.012 0.017-0.017 1.908-1.979 3.65-4.088 5.179-6.258 1.462-2.079 2.775-4.283 3.904-6.558 0.254-0.517 0.15-1.133-0.254-1.537zM17.075 2.962l2.025 2.025c-1.188 0.629-2.292 1.246-3.317 1.854l-2-2c1.071-0.667 2.167-1.296 3.292-1.879zM20.867 26.217c-2.012 2.008-4.688 3.117-7.533 3.117-2.85 0-5.529-1.108-7.542-3.125-4.154-4.154-4.158-10.917-0.008-15.075 2.183-2.104 4.454-3.942 6.858-5.55l1.971 1.971c-2.879 1.804-5.117 3.571-6.946 5.463-1.504 1.508-2.333 3.517-2.333 5.65 0 2.137 0.833 4.146 2.342 5.658 1.512 1.508 3.521 2.342 5.658 2.342 2.133 0 4.138-0.829 5.65-2.333 1.892-1.829 3.663-4.067 5.463-6.946l1.971 1.971c-1.608 2.404-3.446 4.675-5.55 6.858zM27.158 18.212l-1.996-1.996c0.608-1.025 1.225-2.129 1.854-3.317l2.025 2.025c-0.587 1.125-1.217 2.221-1.883 3.288z" |       d="M31.608 13.725l-4-4c-0.304-0.304-0.729-0.442-1.154-0.375-0.421 0.067-0.788 0.333-0.979 0.713-2.938 5.804-5.517 9.617-8.354 12.358-0.004 0.004-0.012 0.012-0.017 0.017-1.008 1.008-2.346 1.563-3.771 1.563s-2.762-0.554-3.771-1.563c-1.008-1.008-1.563-2.346-1.563-3.771s0.554-2.762 1.563-3.771c0.004-0.004 0.012-0.012 0.017-0.017 2.742-2.842 6.55-5.417 12.358-8.354 0.383-0.192 0.646-0.558 0.712-0.979s-0.071-0.85-0.375-1.154l-4-4c-0.404-0.404-1.021-0.504-1.538-0.25-2.271 1.125-4.475 2.438-6.554 3.9-2.175 1.529-4.279 3.271-6.258 5.179-0.004 0.004-0.013 0.012-0.017 0.017-2.521 2.521-3.908 5.867-3.908 9.429s1.387 6.908 3.904 9.429c2.521 2.517 5.867 3.904 9.429 3.904s6.908-1.387 9.429-3.904c0.004-0.004 0.012-0.012 0.017-0.017 1.908-1.979 3.65-4.088 5.179-6.258 1.462-2.079 2.775-4.283 3.904-6.558 0.254-0.517 0.15-1.133-0.254-1.537zM17.075 2.962l2.025 2.025c-1.188 0.629-2.292 1.246-3.317 1.854l-2-2c1.071-0.667 2.167-1.296 3.292-1.879zM20.867 26.217c-2.012 2.008-4.688 3.117-7.533 3.117-2.85 0-5.529-1.108-7.542-3.125-4.154-4.154-4.158-10.917-0.008-15.075 2.183-2.104 4.454-3.942 6.858-5.55l1.971 1.971c-2.879 1.804-5.117 3.571-6.946 5.463-1.504 1.508-2.333 3.517-2.333 5.65 0 2.137 0.833 4.146 2.342 5.658 1.512 1.508 3.521 2.342 5.658 2.342 2.133 0 4.138-0.829 5.65-2.333 1.892-1.829 3.663-4.067 5.463-6.946l1.971 1.971c-1.608 2.404-3.446 4.675-5.55 6.858zM27.158 18.212l-1.996-1.996c0.608-1.025 1.225-2.129 1.854-3.317l2.025 2.025c-0.587 1.125-1.217 2.221-1.883 3.288z" | ||||||
|     /> |     /> | ||||||
|   </svg> |   </svg> | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| <template> | <template> | ||||||
|   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> |   <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       d="M32 10.667c0-3.675-2.992-6.667-6.667-6.667-1.050 0-2.1 0.25-3.029 0.725-0.271 0.138-0.533 0.296-0.783 0.471-0.333-0.546-0.729-1.058-1.196-1.521-1.512-1.508-3.521-2.342-5.658-2.342s-4.146 0.833-5.658 2.342-2.342 3.521-2.342 5.658c0 1.262 0.3 2.517 0.871 3.633 0.446 0.875 1.062 1.671 1.796 2.329v1.35l-7.475-3.204c-0.413-0.175-0.883-0.133-1.258 0.113s-0.6 0.662-0.6 1.113v14.667c0 0.475 0.254 0.917 0.662 1.154 0.208 0.121 0.438 0.179 0.671 0.179 0.229 0 0.458-0.058 0.662-0.175l7.338-4.196v1.704c0 1.471 1.196 2.667 2.667 2.667h17.333c1.471 0 2.667-1.196 2.667-2.667v-13.333c0-0.567-0.175-1.088-0.479-1.521 0.317-0.783 0.479-1.625 0.479-2.479zM29.333 25.333h-17.333v-10.667h17.333v10.667zM25.333 6.667c2.204 0 4 1.796 4 4 0 0.458-0.079 0.908-0.229 1.333h-6.892c0.3-0.846 0.454-1.746 0.454-2.667 0-0.517-0.050-1.021-0.142-1.517 0.746-0.737 1.742-1.15 2.808-1.15zM14.667 4c2.942 0 5.333 2.392 5.333 5.333 0 0.95-0.246 1.863-0.712 2.667h-7.287c-0.6 0-1.154 0.2-1.6 0.537-0.688-0.912-1.067-2.025-1.067-3.204 0-2.942 2.392-5.333 5.333-5.333zM2.667 27.038v-10.35l6.667 2.858v3.679l-6.667 3.813zM12 28.004c0 0 0-0.004 0 0v-1.337h17.333v1.333l-17.333 0.004z" |       d="M32 10.667c0-3.675-2.992-6.667-6.667-6.667-1.050 0-2.1 0.25-3.029 0.725-0.271 0.138-0.533 0.296-0.783 0.471-0.333-0.546-0.729-1.058-1.196-1.521-1.512-1.508-3.521-2.342-5.658-2.342s-4.146 0.833-5.658 2.342-2.342 3.521-2.342 5.658c0 1.262 0.3 2.517 0.871 3.633 0.446 0.875 1.062 1.671 1.796 2.329v1.35l-7.475-3.204c-0.413-0.175-0.883-0.133-1.258 0.113s-0.6 0.662-0.6 1.113v14.667c0 0.475 0.254 0.917 0.662 1.154 0.208 0.121 0.438 0.179 0.671 0.179 0.229 0 0.458-0.058 0.662-0.175l7.338-4.196v1.704c0 1.471 1.196 2.667 2.667 2.667h17.333c1.471 0 2.667-1.196 2.667-2.667v-13.333c0-0.567-0.175-1.088-0.479-1.521 0.317-0.783 0.479-1.625 0.479-2.479zM29.333 25.333h-17.333v-10.667h17.333v10.667zM25.333 6.667c2.204 0 4 1.796 4 4 0 0.458-0.079 0.908-0.229 1.333h-6.892c0.3-0.846 0.454-1.746 0.454-2.667 0-0.517-0.050-1.021-0.142-1.517 0.746-0.737 1.742-1.15 2.808-1.15zM14.667 4c2.942 0 5.333 2.392 5.333 5.333 0 0.95-0.246 1.863-0.712 2.667h-7.287c-0.6 0-1.154 0.2-1.6 0.537-0.688-0.912-1.067-2.025-1.067-3.204 0-2.942 2.392-5.333 5.333-5.333zM2.667 27.038v-10.35l6.667 2.858v3.679l-6.667 3.813zM12 28.004c0 0 0-0.004 0 0v-1.337h17.333v1.333l-17.333 0.004z" | ||||||
|     /> |     /> | ||||||
|   </svg> |   </svg> | ||||||
|   | |||||||
| @@ -1,19 +1,17 @@ | |||||||
| <template> | <template> | ||||||
|   <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> |   <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       fill="inherit" |       fill="inherit" | ||||||
|       d="M31.608 12.392l-3.642-3.642c-0.521-0.521-1.367-0.521-1.887 0-0.375 0.375-0.879 0.583-1.413 0.583s-1.038-0.208-1.413-0.588c-0.779-0.779-0.779-2.050 0-2.829 0.521-0.521 0.521-1.367 0-1.888l-3.646-3.638c-0.25-0.25-0.587-0.392-0.942-0.392s-0.692 0.142-0.942 0.392l-17.333 17.333c-0.521 0.521-0.521 1.367 0 1.887l3.642 3.642c0.25 0.25 0.588 0.392 0.942 0.392s0.692-0.142 0.942-0.392c0.379-0.379 0.879-0.587 1.412-0.587s1.037 0.208 1.412 0.587 0.592 0.879 0.592 1.413c0 0.533-0.208 1.038-0.588 1.413-0.25 0.25-0.392 0.587-0.392 0.942s0.142 0.692 0.392 0.942l3.642 3.642c0.258 0.258 0.6 0.392 0.942 0.392s0.683-0.129 0.942-0.392l17.333-17.333c0.525-0.517 0.525-1.358 0.004-1.879zM13.333 28.779l-1.896-1.896c0.954-1.767 0.688-4.029-0.804-5.521-0.883-0.875-2.054-1.363-3.3-1.363 0 0 0 0 0 0-0.787 0-1.546 0.196-2.221 0.558l-1.892-1.892 15.446-15.446 1.896 1.896c-0.954 1.767-0.688 4.029 0.804 5.521 0.908 0.908 2.104 1.367 3.3 1.367 0.767 0 1.529-0.188 2.221-0.558l1.896 1.896-15.45 15.438z" |       d="M31.608 12.392l-3.642-3.642c-0.521-0.521-1.367-0.521-1.887 0-0.375 0.375-0.879 0.583-1.413 0.583s-1.038-0.208-1.413-0.588c-0.779-0.779-0.779-2.050 0-2.829 0.521-0.521 0.521-1.367 0-1.888l-3.646-3.638c-0.25-0.25-0.587-0.392-0.942-0.392s-0.692 0.142-0.942 0.392l-17.333 17.333c-0.521 0.521-0.521 1.367 0 1.887l3.642 3.642c0.25 0.25 0.588 0.392 0.942 0.392s0.692-0.142 0.942-0.392c0.379-0.379 0.879-0.587 1.412-0.587s1.037 0.208 1.412 0.587 0.592 0.879 0.592 1.413c0 0.533-0.208 1.038-0.588 1.413-0.25 0.25-0.392 0.587-0.392 0.942s0.142 0.692 0.392 0.942l3.642 3.642c0.258 0.258 0.6 0.392 0.942 0.392s0.683-0.129 0.942-0.392l17.333-17.333c0.525-0.517 0.525-1.358 0.004-1.879zM13.333 28.779l-1.896-1.896c0.954-1.767 0.688-4.029-0.804-5.521-0.883-0.875-2.054-1.363-3.3-1.363 0 0 0 0 0 0-0.787 0-1.546 0.196-2.221 0.558l-1.892-1.892 15.446-15.446 1.896 1.896c-0.954 1.767-0.688 4.029 0.804 5.521 0.908 0.908 2.104 1.367 3.3 1.367 0.767 0 1.529-0.188 2.221-0.558l1.896 1.896-15.45 15.438z" | ||||||
|     /> |     /> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       fill="inherit" |       fill="inherit" | ||||||
|       d="M17.137 8.196c-0.258-0.258-0.683-0.258-0.942 0l-8 8c-0.258 0.258-0.258 0.683 0 0.942l6.667 6.667c0.129 0.129 0.3 0.196 0.471 0.196s0.342-0.067 0.471-0.196l8-8c0.258-0.258 0.258-0.683 0-0.942l-6.667-6.667zM15.333 22.392l-5.725-5.725 7.058-7.058 5.725 5.725-7.058 7.058z" |       d="M17.137 8.196c-0.258-0.258-0.683-0.258-0.942 0l-8 8c-0.258 0.258-0.258 0.683 0 0.942l6.667 6.667c0.129 0.129 0.3 0.196 0.471 0.196s0.342-0.067 0.471-0.196l8-8c0.258-0.258 0.258-0.683 0-0.942l-6.667-6.667zM15.333 22.392l-5.725-5.725 7.058-7.058 5.725 5.725-7.058 7.058z" | ||||||
|     /> |     /> | ||||||
|   </svg> |   </svg> | ||||||
|  |  | ||||||
|   <!--   <svg |   <!--   <svg | ||||||
|     xmlns="http://www.w3.org/2000/svg" |     version="1.1" xmlns="http://www.w3.org/2000/svg" | ||||||
|     width="24" |     width="24" | ||||||
|     height="24" |     height="24" | ||||||
|     viewBox="0 0 24 24" |     viewBox="0 0 24 24" | ||||||
|   | |||||||
| @@ -1,11 +1,9 @@ | |||||||
| <template> | <template> | ||||||
|   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> |   <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       d="M16 18.667c4.413 0 8-3.588 8-8s-3.587-8-8-8-8 3.588-8 8 3.588 8 8 8zM16 5.333c2.942 0 5.333 2.392 5.333 5.333s-2.392 5.333-5.333 5.333c-2.942 0-5.333-2.392-5.333-5.333s2.392-5.333 5.333-5.333z" |       d="M16 18.667c4.413 0 8-3.588 8-8s-3.587-8-8-8-8 3.588-8 8 3.588 8 8 8zM16 5.333c2.942 0 5.333 2.392 5.333 5.333s-2.392 5.333-5.333 5.333c-2.942 0-5.333-2.392-5.333-5.333s2.392-5.333 5.333-5.333z" | ||||||
|     /> |     /> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       d="M26.279 22.758c-1.842-1.858-5.204-2.758-10.279-2.758s-8.438 0.9-10.279 2.758c-1.721 1.733-1.721 3.846-1.721 5.242v0.667c0 0.367 0.3 0.667 0.667 0.667h22.667c0.367 0 0.667-0.3 0.667-0.667v-0.667c0-1.396 0-3.508-1.721-5.242zM6.667 28c0-1.183 0-2.413 0.946-3.363 0.563-0.567 1.429-1.017 2.583-1.342 1.475-0.417 3.429-0.629 5.804-0.629s4.329 0.212 5.804 0.629c1.154 0.325 2.021 0.775 2.583 1.342 0.946 0.95 0.946 2.179 0.946 3.363h-18.667z" |       d="M26.279 22.758c-1.842-1.858-5.204-2.758-10.279-2.758s-8.438 0.9-10.279 2.758c-1.721 1.733-1.721 3.846-1.721 5.242v0.667c0 0.367 0.3 0.667 0.667 0.667h22.667c0.367 0 0.667-0.3 0.667-0.667v-0.667c0-1.396 0-3.508-1.721-5.242zM6.667 28c0-1.183 0-2.413 0.946-3.363 0.563-0.567 1.429-1.017 2.583-1.342 1.475-0.417 3.429-0.629 5.804-0.629s4.329 0.212 5.804 0.629c1.154 0.325 2.021 0.775 2.583 1.342 0.946 0.95 0.946 2.179 0.946 3.363h-18.667z" | ||||||
|     /> |     /> | ||||||
|   </svg> |   </svg> | ||||||
|   | |||||||
| @@ -1,11 +1,9 @@ | |||||||
| <template> | <template> | ||||||
|   <svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"> |   <svg viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg"> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       d="M31.333 4h-30.667c-0.367 0-0.667 0.3-0.667 0.667v20c0 0.367 0.3 0.667 0.667 0.667h16.417c1.579 1.642 3.796 2.667 6.25 2.667 4.779 0 8.667-3.887 8.667-8.667v-14.667c0-0.367-0.3-0.667-0.667-0.667zM5.333 5.333v2.667h-2.667v-2.667h2.667zM2.667 17.333h2.667v2.667h-2.667v-2.667zM2.667 16v-2.667h2.667v2.667h-2.667zM2.667 12v-2.667h2.667v2.667h-2.667zM2.667 24v-2.667h2.667v2.667h-2.667zM29.333 12h-1.387c-0.404-0.254-0.833-0.479-1.279-0.667v-2h2.667v2.667zM8 6.667h16v4.025c-0.221-0.017-0.442-0.025-0.667-0.025-4.779 0-8.667 3.888-8.667 8.667 0 1.179 0.238 2.308 0.667 3.333h-7.333v-16zM23.333 26.667c-4.042 0-7.333-3.292-7.333-7.333s3.292-7.333 7.333-7.333 7.333 3.292 7.333 7.333-3.292 7.333-7.333 7.333zM29.333 8h-2.667v-2.667h2.667v2.667z" |       d="M31.333 4h-30.667c-0.367 0-0.667 0.3-0.667 0.667v20c0 0.367 0.3 0.667 0.667 0.667h16.417c1.579 1.642 3.796 2.667 6.25 2.667 4.779 0 8.667-3.887 8.667-8.667v-14.667c0-0.367-0.3-0.667-0.667-0.667zM5.333 5.333v2.667h-2.667v-2.667h2.667zM2.667 17.333h2.667v2.667h-2.667v-2.667zM2.667 16v-2.667h2.667v2.667h-2.667zM2.667 12v-2.667h2.667v2.667h-2.667zM2.667 24v-2.667h2.667v2.667h-2.667zM29.333 12h-1.387c-0.404-0.254-0.833-0.479-1.279-0.667v-2h2.667v2.667zM8 6.667h16v4.025c-0.221-0.017-0.442-0.025-0.667-0.025-4.779 0-8.667 3.888-8.667 8.667 0 1.179 0.238 2.308 0.667 3.333h-7.333v-16zM23.333 26.667c-4.042 0-7.333-3.292-7.333-7.333s3.292-7.333 7.333-7.333 7.333 3.292 7.333 7.333-3.292 7.333-7.333 7.333zM29.333 8h-2.667v-2.667h2.667v2.667z" | ||||||
|     /> |     /> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       d="M27.675 18.762l-6.667-4c-0.204-0.125-0.462-0.125-0.671-0.008s-0.337 0.342-0.337 0.579v8c0 0.242 0.129 0.462 0.337 0.579 0.1 0.058 0.217 0.087 0.329 0.087 0.121 0 0.238-0.033 0.342-0.096l6.667-4c0.2-0.121 0.325-0.337 0.325-0.571s-0.121-0.45-0.325-0.571zM21.333 22.154v-5.642l4.704 2.821-4.704 2.821z" |       d="M27.675 18.762l-6.667-4c-0.204-0.125-0.462-0.125-0.671-0.008s-0.337 0.342-0.337 0.579v8c0 0.242 0.129 0.462 0.337 0.579 0.1 0.058 0.217 0.087 0.329 0.087 0.121 0 0.238-0.033 0.342-0.096l6.667-4c0.2-0.121 0.325-0.337 0.325-0.571s-0.121-0.45-0.325-0.571zM21.333 22.154v-5.642l4.704 2.821-4.704 2.821z" | ||||||
|     /> |     /> | ||||||
|   </svg> |   </svg> | ||||||
|   | |||||||
| @@ -1,27 +1,22 @@ | |||||||
| <template> | <template> | ||||||
|   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> |   <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       fill="inherit" |       fill="inherit" | ||||||
|       d="M21.333 20h-2.667c-0.738 0-1.333 0.596-1.333 1.333v10c0 0.367 0.3 0.667 0.667 0.667h4c0.367 0 0.667-0.3 0.667-0.667v-10c0-0.738-0.596-1.333-1.333-1.333zM18.667 30.667v-8h1.333v8h-1.333z" |       d="M21.333 20h-2.667c-0.738 0-1.333 0.596-1.333 1.333v10c0 0.367 0.3 0.667 0.667 0.667h4c0.367 0 0.667-0.3 0.667-0.667v-10c0-0.738-0.596-1.333-1.333-1.333zM18.667 30.667v-8h1.333v8h-1.333z" | ||||||
|     /> |     /> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       fill="inherit" |       fill="inherit" | ||||||
|       d="M13.333 14.667h-2.667c-0.738 0-1.333 0.596-1.333 1.333v15.333c0 0.367 0.3 0.667 0.667 0.667h4c0.367 0 0.667-0.3 0.667-0.667v-15.333c0-0.738-0.596-1.333-1.333-1.333zM10.667 30.667v-13.333h1.333v13.333h-1.333z" |       d="M13.333 14.667h-2.667c-0.738 0-1.333 0.596-1.333 1.333v15.333c0 0.367 0.3 0.667 0.667 0.667h4c0.367 0 0.667-0.3 0.667-0.667v-15.333c0-0.738-0.596-1.333-1.333-1.333zM10.667 30.667v-13.333h1.333v13.333h-1.333z" | ||||||
|     /> |     /> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       fill="inherit" |       fill="inherit" | ||||||
|       d="M5.333 20h-2.667c-0.738 0-1.333 0.596-1.333 1.333v10c0 0.367 0.3 0.667 0.667 0.667h4c0.367 0 0.667-0.3 0.667-0.667v-10c0-0.738-0.596-1.333-1.333-1.333zM2.667 30.667v-8h1.333v8h-1.333z" |       d="M5.333 20h-2.667c-0.738 0-1.333 0.596-1.333 1.333v10c0 0.367 0.3 0.667 0.667 0.667h4c0.367 0 0.667-0.3 0.667-0.667v-10c0-0.738-0.596-1.333-1.333-1.333zM2.667 30.667v-8h1.333v8h-1.333z" | ||||||
|     /> |     /> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       fill="inherit" |       fill="inherit" | ||||||
|       d="M29.333 9.333h-2.667c-0.738 0-1.333 0.596-1.333 1.333v20.667c0 0.367 0.3 0.667 0.667 0.667h4c0.367 0 0.667-0.3 0.667-0.667v-20.667c0-0.738-0.596-1.333-1.333-1.333zM26.667 30.667v-18.667h1.333v18.667h-1.333z" |       d="M29.333 9.333h-2.667c-0.738 0-1.333 0.596-1.333 1.333v20.667c0 0.367 0.3 0.667 0.667 0.667h4c0.367 0 0.667-0.3 0.667-0.667v-20.667c0-0.738-0.596-1.333-1.333-1.333zM26.667 30.667v-18.667h1.333v18.667h-1.333z" | ||||||
|     /> |     /> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       fill="inherit" |       fill="inherit" | ||||||
|       d="M31.333 0h-3.333v1.333h1.592l-11.738 9.267-9.004-2.571c-0.208-0.058-0.429-0.012-0.6 0.121l-7.188 5.75 0.833 1.042 6.917-5.533 9.004 2.571c0.204 0.058 0.429 0.017 0.596-0.117l12.254-9.679v1.817h1.333v-3.333c0-0.367-0.3-0.667-0.667-0.667z" |       d="M31.333 0h-3.333v1.333h1.592l-11.738 9.267-9.004-2.571c-0.208-0.058-0.429-0.012-0.6 0.121l-7.188 5.75 0.833 1.042 6.917-5.533 9.004 2.571c0.204 0.058 0.429 0.017 0.596-0.117l12.254-9.679v1.817h1.333v-3.333c0-0.367-0.3-0.667-0.667-0.667z" | ||||||
|     /> |     /> | ||||||
|   | |||||||
| @@ -1,12 +1,10 @@ | |||||||
| <template> | <template> | ||||||
|   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> |   <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       fill="inherit" |       fill="inherit" | ||||||
|       d="M16 18.667c4.413 0 8-3.588 8-8s-3.587-8-8-8-8 3.588-8 8 3.588 8 8 8zM16 5.333c2.942 0 5.333 2.392 5.333 5.333s-2.392 5.333-5.333 5.333c-2.942 0-5.333-2.392-5.333-5.333s2.392-5.333 5.333-5.333z" |       d="M16 18.667c4.413 0 8-3.588 8-8s-3.587-8-8-8-8 3.588-8 8 3.588 8 8 8zM16 5.333c2.942 0 5.333 2.392 5.333 5.333s-2.392 5.333-5.333 5.333c-2.942 0-5.333-2.392-5.333-5.333s2.392-5.333 5.333-5.333z" | ||||||
|     /> |     /> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       fill="inherit" |       fill="inherit" | ||||||
|       d="M26.279 22.758c-1.842-1.858-5.204-2.758-10.279-2.758s-8.438 0.9-10.279 2.758c-1.721 1.733-1.721 3.846-1.721 5.242v0.667c0 0.367 0.3 0.667 0.667 0.667h22.667c0.367 0 0.667-0.3 0.667-0.667v-0.667c0-1.396 0-3.508-1.721-5.242zM6.667 28c0-1.183 0-2.413 0.946-3.363 0.563-0.567 1.429-1.017 2.583-1.342 1.475-0.417 3.429-0.629 5.804-0.629s4.329 0.212 5.804 0.629c1.154 0.325 2.021 0.775 2.583 1.342 0.946 0.95 0.946 2.179 0.946 3.363h-18.667z" |       d="M26.279 22.758c-1.842-1.858-5.204-2.758-10.279-2.758s-8.438 0.9-10.279 2.758c-1.721 1.733-1.721 3.846-1.721 5.242v0.667c0 0.367 0.3 0.667 0.667 0.667h22.667c0.367 0 0.667-0.3 0.667-0.667v-0.667c0-1.396 0-3.508-1.721-5.242zM6.667 28c0-1.183 0-2.413 0.946-3.363 0.563-0.567 1.429-1.017 2.583-1.342 1.475-0.417 3.429-0.629 5.804-0.629s4.329 0.212 5.804 0.629c1.154 0.325 2.021 0.775 2.583 1.342 0.946 0.95 0.946 2.179 0.946 3.363h-18.667z" | ||||||
|     /> |     /> | ||||||
|   | |||||||
| @@ -1,15 +1,12 @@ | |||||||
| <template> | <template> | ||||||
|   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> |   <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       d="M12 16c4.413 0 8-3.588 8-8s-3.587-8-8-8-8 3.587-8 8 3.588 8 8 8zM12 2.667c2.942 0 5.333 2.392 5.333 5.333s-2.392 5.333-5.333 5.333-5.333-2.392-5.333-5.333 2.392-5.333 5.333-5.333z" |       d="M12 16c4.413 0 8-3.588 8-8s-3.587-8-8-8-8 3.587-8 8 3.588 8 8 8zM12 2.667c2.942 0 5.333 2.392 5.333 5.333s-2.392 5.333-5.333 5.333-5.333-2.392-5.333-5.333 2.392-5.333 5.333-5.333z" | ||||||
|     /> |     /> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       d="M23.333 14.667c-2.617 0-4.971 1.167-6.558 3.008-1.367-0.225-2.975-0.342-4.775-0.342-5.075 0-8.438 0.9-10.279 2.758-1.721 1.733-1.721 3.846-1.721 5.242v0.667c0 0.367 0.3 0.667 0.667 0.667h14.667c1.308 3.129 4.4 5.333 8 5.333 4.779 0 8.667-3.887 8.667-8.667s-3.887-8.667-8.667-8.667zM2.667 25.333c0-1.183 0-2.413 0.946-3.363 0.563-0.567 1.429-1.017 2.583-1.342 1.475-0.417 3.429-0.629 5.804-0.629 1.196 0 2.292 0.054 3.267 0.163-0.387 0.983-0.6 2.054-0.6 3.171 0 0.688 0.079 1.358 0.233 2h-12.233zM23.333 30.667c-4.042 0-7.333-3.292-7.333-7.333s3.292-7.333 7.333-7.333 7.333 3.292 7.333 7.333-3.292 7.333-7.333 7.333z" |       d="M23.333 14.667c-2.617 0-4.971 1.167-6.558 3.008-1.367-0.225-2.975-0.342-4.775-0.342-5.075 0-8.438 0.9-10.279 2.758-1.721 1.733-1.721 3.846-1.721 5.242v0.667c0 0.367 0.3 0.667 0.667 0.667h14.667c1.308 3.129 4.4 5.333 8 5.333 4.779 0 8.667-3.887 8.667-8.667s-3.887-8.667-8.667-8.667zM2.667 25.333c0-1.183 0-2.413 0.946-3.363 0.563-0.567 1.429-1.017 2.583-1.342 1.475-0.417 3.429-0.629 5.804-0.629 1.196 0 2.292 0.054 3.267 0.163-0.387 0.983-0.6 2.054-0.6 3.171 0 0.688 0.079 1.358 0.233 2h-12.233zM23.333 30.667c-4.042 0-7.333-3.292-7.333-7.333s3.292-7.333 7.333-7.333 7.333 3.292 7.333 7.333-3.292 7.333-7.333 7.333z" | ||||||
|     /> |     /> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       d="M27.333 22.667h-0.667v-0.667c0-1.837-1.496-3.333-3.333-3.333s-3.333 1.496-3.333 3.333v0.667h-0.667c-0.367 0-0.667 0.3-0.667 0.667v4c0 0.367 0.3 0.667 0.667 0.667h8c0.367 0 0.667-0.3 0.667-0.667v-4c0-0.367-0.3-0.667-0.667-0.667zM21.333 22c0-1.104 0.896-2 2-2s2 0.896 2 2v0.667h-4v-0.667zM26.667 26.667h-6.667v-2.667h6.667v2.667z" |       d="M27.333 22.667h-0.667v-0.667c0-1.837-1.496-3.333-3.333-3.333s-3.333 1.496-3.333 3.333v0.667h-0.667c-0.367 0-0.667 0.3-0.667 0.667v4c0 0.367 0.3 0.667 0.667 0.667h8c0.367 0 0.667-0.3 0.667-0.667v-4c0-0.367-0.3-0.667-0.667-0.667zM21.333 22c0-1.104 0.896-2 2-2s2 0.896 2 2v0.667h-4v-0.667zM26.667 26.667h-6.667v-2.667h6.667v2.667z" | ||||||
|     /> |     /> | ||||||
|   </svg> |   </svg> | ||||||
|   | |||||||
| @@ -1,16 +1,14 @@ | |||||||
| <template> | <template> | ||||||
|   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> |   <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       d="M23.988 13.863c0 0 0 0 0 0 0-0.004 0-0.008-0.004-0.012 0 0 0 0 0-0.004s0-0.004 0-0.008c0 0 0 0 0 0 0-0.004 0-0.004-0.004-0.008 0 0 0-0.004 0-0.004 0-0.004 0-0.004 0-0.008 0 0 0-0.004 0-0.004s0-0.004 0-0.004c0 0 0-0.004 0-0.004 0-0.004 0-0.004-0.004-0.008 0 0 0-0.004 0-0.004s0-0.004 0-0.004c0 0 0-0.004 0-0.004 0-0.004 0-0.004-0.004-0.008 0 0 0 0 0-0.004s0-0.004-0.004-0.008c0 0 0 0 0-0.004s-0.004-0.004-0.004-0.008c0 0 0 0 0 0-0.050-0.121-0.133-0.229-0.246-0.304 0 0 0 0 0 0-0.004-0.004-0.012-0.008-0.017-0.012 0 0 0 0 0 0-0.004 0-0.004-0.004-0.008-0.004 0 0 0 0 0 0-0.004 0-0.004-0.004-0.008-0.004 0 0 0 0-0.004 0 0 0 0 0-0.004 0-0.004-0.004-0.012-0.008-0.017-0.008 0 0 0 0 0 0-0.133-0.071-0.279-0.092-0.421-0.071 0 0 0 0 0 0-0.004 0-0.008 0-0.008 0s0 0 0 0c-0.004 0-0.004 0-0.008 0 0 0 0 0-0.004 0s-0.008 0-0.008 0c0 0 0 0 0 0-0.004 0-0.004 0-0.008 0 0 0 0 0-0.004 0s-0.008 0-0.008 0.004c0 0 0 0-0.004 0s-0.004 0-0.008 0.004c0 0 0 0 0 0-0.004 0-0.008 0-0.008 0.004 0 0 0 0 0 0-0.008 0-0.012 0.004-0.021 0.008 0 0 0 0 0 0-0.067 0.021-0.133 0.058-0.192 0.1l-14.679 10.662c-0.208 0.154-0.313 0.413-0.262 0.667s0.242 0.458 0.492 0.521l4.946 1.238 1.238 4.946c0.067 0.262 0.287 0.462 0.554 0.5 0.029 0.004 0.063 0.008 0.092 0.008 0.238 0 0.458-0.125 0.579-0.337l2.383-4.175 4.804 1.796c0.204 0.075 0.433 0.050 0.613-0.075s0.288-0.329 0.288-0.55v-14.654c0-0.050-0.004-0.1-0.012-0.15zM10.213 24.367l9.7-7.054-6.171 7.933-3.529-0.879zM15.579 29.563l-0.854-3.408 6.033-7.758-3.358 7.975-1.821 3.192zM18.883 26.288l3.783-8.988v10.404l-3.783-1.417z" |       d="M23.988 13.863c0 0 0 0 0 0 0-0.004 0-0.008-0.004-0.012 0 0 0 0 0-0.004s0-0.004 0-0.008c0 0 0 0 0 0 0-0.004 0-0.004-0.004-0.008 0 0 0-0.004 0-0.004 0-0.004 0-0.004 0-0.008 0 0 0-0.004 0-0.004s0-0.004 0-0.004c0 0 0-0.004 0-0.004 0-0.004 0-0.004-0.004-0.008 0 0 0-0.004 0-0.004s0-0.004 0-0.004c0 0 0-0.004 0-0.004 0-0.004 0-0.004-0.004-0.008 0 0 0 0 0-0.004s0-0.004-0.004-0.008c0 0 0 0 0-0.004s-0.004-0.004-0.004-0.008c0 0 0 0 0 0-0.050-0.121-0.133-0.229-0.246-0.304 0 0 0 0 0 0-0.004-0.004-0.012-0.008-0.017-0.012 0 0 0 0 0 0-0.004 0-0.004-0.004-0.008-0.004 0 0 0 0 0 0-0.004 0-0.004-0.004-0.008-0.004 0 0 0 0-0.004 0 0 0 0 0-0.004 0-0.004-0.004-0.012-0.008-0.017-0.008 0 0 0 0 0 0-0.133-0.071-0.279-0.092-0.421-0.071 0 0 0 0 0 0-0.004 0-0.008 0-0.008 0s0 0 0 0c-0.004 0-0.004 0-0.008 0 0 0 0 0-0.004 0s-0.008 0-0.008 0c0 0 0 0 0 0-0.004 0-0.004 0-0.008 0 0 0 0 0-0.004 0s-0.008 0-0.008 0.004c0 0 0 0-0.004 0s-0.004 0-0.008 0.004c0 0 0 0 0 0-0.004 0-0.008 0-0.008 0.004 0 0 0 0 0 0-0.008 0-0.012 0.004-0.021 0.008 0 0 0 0 0 0-0.067 0.021-0.133 0.058-0.192 0.1l-14.679 10.662c-0.208 0.154-0.313 0.413-0.262 0.667s0.242 0.458 0.492 0.521l4.946 1.238 1.238 4.946c0.067 0.262 0.287 0.462 0.554 0.5 0.029 0.004 0.063 0.008 0.092 0.008 0.238 0 0.458-0.125 0.579-0.337l2.383-4.175 4.804 1.796c0.204 0.075 0.433 0.050 0.613-0.075s0.288-0.329 0.288-0.55v-14.654c0-0.050-0.004-0.1-0.012-0.15zM10.213 24.367l9.7-7.054-6.171 7.933-3.529-0.879zM15.579 29.563l-0.854-3.408 6.033-7.758-3.358 7.975-1.821 3.192zM18.883 26.288l3.783-8.988v10.404l-3.783-1.417z" | ||||||
|     /> |     /> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       d="M28.488 3.513c-2.267-2.263-5.283-3.513-8.488-3.513-2.5 0-4.9 0.762-6.933 2.204-1.667 1.179-2.983 2.737-3.863 4.554-0.396-0.058-0.796-0.092-1.204-0.092-2.138 0-4.146 0.833-5.658 2.342s-2.342 3.521-2.342 5.658c0 1.858 0.65 3.667 1.829 5.096 1.163 1.408 2.788 2.383 4.571 2.746l0.529-2.613c-2.471-0.504-4.263-2.7-4.263-5.229 0-2.942 2.392-5.333 5.333-5.333 1.8 0 3.462 0.896 4.454 2.4l2.225-1.467c-0.75-1.137-1.754-2.042-2.917-2.662 1.608-2.996 4.775-4.938 8.238-4.938 5.146 0 9.333 4.188 9.333 9.333 0 2.225-0.938 4.367-2.571 5.875l1.808 1.958c1.071-0.988 1.913-2.163 2.504-3.488 0.613-1.371 0.925-2.833 0.925-4.35 0-3.2-1.25-6.217-3.512-8.483z" |       d="M28.488 3.513c-2.267-2.263-5.283-3.513-8.488-3.513-2.5 0-4.9 0.762-6.933 2.204-1.667 1.179-2.983 2.737-3.863 4.554-0.396-0.058-0.796-0.092-1.204-0.092-2.138 0-4.146 0.833-5.658 2.342s-2.342 3.521-2.342 5.658c0 1.858 0.65 3.667 1.829 5.096 1.163 1.408 2.788 2.383 4.571 2.746l0.529-2.613c-2.471-0.504-4.263-2.7-4.263-5.229 0-2.942 2.392-5.333 5.333-5.333 1.8 0 3.462 0.896 4.454 2.4l2.225-1.467c-0.75-1.137-1.754-2.042-2.917-2.662 1.608-2.996 4.775-4.938 8.238-4.938 5.146 0 9.333 4.188 9.333 9.333 0 2.225-0.938 4.367-2.571 5.875l1.808 1.958c1.071-0.988 1.913-2.163 2.504-3.488 0.613-1.371 0.925-2.833 0.925-4.35 0-3.2-1.25-6.217-3.512-8.483z" | ||||||
|     /> |     /> | ||||||
|  |  | ||||||
|     <!--     <path |     <!--     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |       version="1.1" xmlns="http://www.w3.org/2000/svg" | ||||||
|       d="M31.542 1.658c-0.396-0.342-0.95-0.425-1.425-0.208l-29.333 13.333c-0.512 0.233-0.821 0.758-0.779 1.317s0.425 1.029 0.963 1.183l8.367 2.387v9.663c0 0.587 0.383 1.104 0.946 1.275 0.129 0.038 0.258 0.058 0.387 0.058 0.438 0 0.858-0.217 1.108-0.596l4.683-7.017 6.946 3.475c0.354 0.175 0.767 0.188 1.129 0.029s0.637-0.467 0.746-0.846l6.667-22.667c0.146-0.504-0.012-1.046-0.404-1.387zM5.183 15.713l18.771-8.533-12.804 10.246c-0.037-0.017-0.079-0.029-0.117-0.042l-5.85-1.671zM12 24.929v-6.262c0-0.067-0.004-0.133-0.017-0.2l13.963-11.171-10.971 13.183c-0.029 0.038-0.058 0.075-0.083 0.113l-2.892 4.338zM23.171 23.429l-6.296-3.15 11.167-13.421-4.871 16.571z" |       d="M31.542 1.658c-0.396-0.342-0.95-0.425-1.425-0.208l-29.333 13.333c-0.512 0.233-0.821 0.758-0.779 1.317s0.425 1.029 0.963 1.183l8.367 2.387v9.663c0 0.587 0.383 1.104 0.946 1.275 0.129 0.038 0.258 0.058 0.387 0.058 0.438 0 0.858-0.217 1.108-0.596l4.683-7.017 6.946 3.475c0.354 0.175 0.767 0.188 1.129 0.029s0.637-0.467 0.746-0.846l6.667-22.667c0.146-0.504-0.012-1.046-0.404-1.387zM5.183 15.713l18.771-8.533-12.804 10.246c-0.037-0.017-0.079-0.029-0.117-0.042l-5.85-1.671zM12 24.929v-6.262c0-0.067-0.004-0.133-0.017-0.2l13.963-11.171-10.971 13.183c-0.029 0.038-0.058 0.075-0.083 0.113l-2.892 4.338zM23.171 23.429l-6.296-3.15 11.167-13.421-4.871 16.571z" | ||||||
|     /> --> |     /> --> | ||||||
|   </svg> |   </svg> | ||||||
|   | |||||||
| @@ -1,16 +1,14 @@ | |||||||
| <template> | <template> | ||||||
|   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> |   <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       d="M31.904 16.087c0.063-0.471 0.096-0.946 0.096-1.421 0-3.204-1.25-6.221-3.512-8.488-2.267-2.263-5.283-3.513-8.488-3.513-2.5 0-4.9 0.763-6.933 2.204-1.667 1.179-2.983 2.737-3.863 4.554-0.396-0.058-0.796-0.092-1.204-0.092-2.138 0-4.146 0.833-5.658 2.342s-2.342 3.521-2.342 5.658 0.833 4.146 2.342 5.658c1.513 1.508 3.521 2.342 5.658 2.342h8.033c1.542 2.404 4.238 4 7.3 4 4.779 0 8.667-3.887 8.667-8.667 0-0.992-0.167-1.946-0.475-2.833 0.175-0.567 0.304-1.154 0.379-1.746zM8 22.667c-2.942 0-5.333-2.392-5.333-5.333s2.392-5.333 5.333-5.333c1.8 0 3.463 0.896 4.454 2.4l2.225-1.467c-0.75-1.137-1.754-2.042-2.917-2.662 1.608-2.996 4.775-4.938 8.238-4.938 5.063 0 9.196 4.050 9.329 9.083-1.558-1.496-3.671-2.417-5.996-2.417-4.779 0-8.667 3.887-8.667 8.667 0 0.688 0.079 1.358 0.233 2h-6.9zM23.333 28c-4.042 0-7.333-3.292-7.333-7.333s3.292-7.333 7.333-7.333 7.333 3.292 7.333 7.333-3.292 7.333-7.333 7.333z" |       d="M31.904 16.087c0.063-0.471 0.096-0.946 0.096-1.421 0-3.204-1.25-6.221-3.512-8.488-2.267-2.263-5.283-3.513-8.488-3.513-2.5 0-4.9 0.763-6.933 2.204-1.667 1.179-2.983 2.737-3.863 4.554-0.396-0.058-0.796-0.092-1.204-0.092-2.138 0-4.146 0.833-5.658 2.342s-2.342 3.521-2.342 5.658 0.833 4.146 2.342 5.658c1.513 1.508 3.521 2.342 5.658 2.342h8.033c1.542 2.404 4.238 4 7.3 4 4.779 0 8.667-3.887 8.667-8.667 0-0.992-0.167-1.946-0.475-2.833 0.175-0.567 0.304-1.154 0.379-1.746zM8 22.667c-2.942 0-5.333-2.392-5.333-5.333s2.392-5.333 5.333-5.333c1.8 0 3.463 0.896 4.454 2.4l2.225-1.467c-0.75-1.137-1.754-2.042-2.917-2.662 1.608-2.996 4.775-4.938 8.238-4.938 5.063 0 9.196 4.050 9.329 9.083-1.558-1.496-3.671-2.417-5.996-2.417-4.779 0-8.667 3.887-8.667 8.667 0 0.688 0.079 1.358 0.233 2h-6.9zM23.333 28c-4.042 0-7.333-3.292-7.333-7.333s3.292-7.333 7.333-7.333 7.333 3.292 7.333 7.333-3.292 7.333-7.333 7.333z" | ||||||
|     /> |     /> | ||||||
|     <path |     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |  | ||||||
|       d="M22 22.392l-2.667-2.667-0.942 0.942 3.137 3.137c0.129 0.129 0.3 0.196 0.471 0.196s0.342-0.067 0.471-0.196l5.804-5.804-0.942-0.942-5.333 5.333z" |       d="M22 22.392l-2.667-2.667-0.942 0.942 3.137 3.137c0.129 0.129 0.3 0.196 0.471 0.196s0.342-0.067 0.471-0.196l5.804-5.804-0.942-0.942-5.333 5.333z" | ||||||
|     /> |     /> | ||||||
|  |  | ||||||
|     <!--     <path |     <!--     <path | ||||||
|       xmlns="http://www.w3.org/2000/svg" |       version="1.1" xmlns="http://www.w3.org/2000/svg" | ||||||
|       d="M31.542 1.658c-0.396-0.342-0.95-0.425-1.425-0.208l-29.333 13.333c-0.512 0.233-0.821 0.758-0.779 1.317s0.425 1.029 0.963 1.183l8.367 2.387v9.663c0 0.587 0.383 1.104 0.946 1.275 0.129 0.038 0.258 0.058 0.387 0.058 0.438 0 0.858-0.217 1.108-0.596l4.683-7.017 6.946 3.475c0.354 0.175 0.767 0.188 1.129 0.029s0.637-0.467 0.746-0.846l6.667-22.667c0.146-0.504-0.012-1.046-0.404-1.387zM5.183 15.713l18.771-8.533-12.804 10.246c-0.037-0.017-0.079-0.029-0.117-0.042l-5.85-1.671zM12 24.929v-6.262c0-0.067-0.004-0.133-0.017-0.2l13.963-11.171-10.971 13.183c-0.029 0.038-0.058 0.075-0.083 0.113l-2.892 4.338zM23.171 23.429l-6.296-3.15 11.167-13.421-4.871 16.571z" |       d="M31.542 1.658c-0.396-0.342-0.95-0.425-1.425-0.208l-29.333 13.333c-0.512 0.233-0.821 0.758-0.779 1.317s0.425 1.029 0.963 1.183l8.367 2.387v9.663c0 0.587 0.383 1.104 0.946 1.275 0.129 0.038 0.258 0.058 0.387 0.058 0.438 0 0.858-0.217 1.108-0.596l4.683-7.017 6.946 3.475c0.354 0.175 0.767 0.188 1.129 0.029s0.637-0.467 0.746-0.846l6.667-22.667c0.146-0.504-0.012-1.046-0.404-1.387zM5.183 15.713l18.771-8.533-12.804 10.246c-0.037-0.017-0.079-0.029-0.117-0.042l-5.85-1.671zM12 24.929v-6.262c0-0.067-0.004-0.133-0.017-0.2l13.963-11.171-10.971 13.183c-0.029 0.038-0.058 0.075-0.083 0.113l-2.892 4.338zM23.171 23.429l-6.296-3.15 11.167-13.421-4.871 16.571z" | ||||||
|     /> --> |     /> --> | ||||||
|   </svg> |   </svg> | ||||||
|   | |||||||
| @@ -1,10 +1,12 @@ | |||||||
| <template> | <template> | ||||||
|   <svg |   <svg | ||||||
|  |     version="1.1" | ||||||
|     xmlns="http://www.w3.org/2000/svg" |     xmlns="http://www.w3.org/2000/svg" | ||||||
|     viewBox="0 0 32 32" |     viewBox="0 0 32 32" | ||||||
|     style="transition-duration: 0s;" |     style="transition-duration: 0s" | ||||||
|   > |   > | ||||||
|     <path |     <path | ||||||
|  |       version="1.1" | ||||||
|       xmlns="http://www.w3.org/2000/svg" |       xmlns="http://www.w3.org/2000/svg" | ||||||
|       fill="inherit" |       fill="inherit" | ||||||
|       d="M21.388 21.141c-0.045 0.035-0.089 0.073-0.132 0.116s-0.080 0.085-0.116 0.132c-1.677 1.617-3.959 2.611-6.473 2.611-2.577 0-4.909-1.043-6.6-2.733s-2.733-4.023-2.733-6.6 1.043-4.909 2.733-6.6 4.023-2.733 6.6-2.733 4.909 1.043 6.6 2.733 2.733 4.023 2.733 6.6c0 2.515-0.993 4.796-2.612 6.475zM28.943 27.057l-4.9-4.9c1.641-2.053 2.624-4.657 2.624-7.491 0-3.313-1.344-6.315-3.515-8.485s-5.172-3.515-8.485-3.515-6.315 1.344-8.485 3.515-3.515 5.172-3.515 8.485 1.344 6.315 3.515 8.485 5.172 3.515 8.485 3.515c2.833 0 5.437-0.983 7.491-2.624l4.9 4.9c0.521 0.521 1.365 0.521 1.885 0s0.521-1.365 0-1.885z" |       d="M21.388 21.141c-0.045 0.035-0.089 0.073-0.132 0.116s-0.080 0.085-0.116 0.132c-1.677 1.617-3.959 2.611-6.473 2.611-2.577 0-4.909-1.043-6.6-2.733s-2.733-4.023-2.733-6.6 1.043-4.909 2.733-6.6 4.023-2.733 6.6-2.733 4.909 1.043 6.6 2.733 2.733 4.023 2.733 6.6c0 2.515-0.993 4.796-2.612 6.475zM28.943 27.057l-4.9-4.9c1.641-2.053 2.624-4.657 2.624-7.491 0-3.313-1.344-6.315-3.515-8.485s-5.172-3.515-8.485-3.515-6.315 1.344-8.485 3.515-3.515 5.172-3.515 8.485 1.344 6.315 3.515 8.485 5.172 3.515 8.485 3.515c2.833 0 5.437-0.983 7.491-2.624l4.9 4.9c0.521 0.521 1.365 0.521 1.885 0s0.521-1.365 0-1.885z" | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| <template> | <template> | ||||||
|   <svg |   <svg | ||||||
|  |     version="1.1" | ||||||
|     xmlns="http://www.w3.org/2000/svg" |     xmlns="http://www.w3.org/2000/svg" | ||||||
|     width="24" |     width="24" | ||||||
|     height="24" |     height="24" | ||||||
| @@ -9,7 +10,6 @@ | |||||||
|     stroke-width="2" |     stroke-width="2" | ||||||
|     stroke-linecap="round" |     stroke-linecap="round" | ||||||
|     stroke-linejoin="round" |     stroke-linejoin="round" | ||||||
|     style="transition: stroke-width 0.5s ease" |  | ||||||
|   > |   > | ||||||
|     <line x1="4" y1="21" x2="4" y2="14"></line> |     <line x1="4" y1="21" x2="4" y2="14"></line> | ||||||
|     <line x1="4" y1="10" x2="4" y2="3"></line> |     <line x1="4" y1="10" x2="4" y2="3"></line> | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| <template> | <template> | ||||||
|   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> |   <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|     <path |     <path | ||||||
|       d="M29.333 6.667h-26.667c-1.471 0-2.667 1.196-2.667 2.667v18.667c0 1.471 1.196 2.667 2.667 2.667h26.667c1.471 0 2.667-1.196 2.667-2.667v-18.667c0-1.471-1.196-2.667-2.667-2.667zM29.333 28h-26.667v-18.667h26.667v18.667z" |       d="M29.333 6.667h-26.667c-1.471 0-2.667 1.196-2.667 2.667v18.667c0 1.471 1.196 2.667 2.667 2.667h26.667c1.471 0 2.667-1.196 2.667-2.667v-18.667c0-1.471-1.196-2.667-2.667-2.667zM29.333 28h-26.667v-18.667h26.667v18.667z" | ||||||
|     ></path> |     ></path> | ||||||
|   | |||||||
							
								
								
									
										17
									
								
								src/icons/IconStop.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | |||||||
|  | <template> | ||||||
|  |   <svg | ||||||
|  |     version="1.1" | ||||||
|  |     xmlns="http://www.w3.org/2000/svg" | ||||||
|  |     width="768" | ||||||
|  |     height="768" | ||||||
|  |     viewBox="0 0 768 768" | ||||||
|  |     fill="currentColor" | ||||||
|  |   > | ||||||
|  |     <path | ||||||
|  |       d="M758.6 215.6l-206.2-206.2c-6-6-14.1-9.4-22.6-9.4h-291.6c-8.5 0-16.6 3.4-22.6 9.4l-206.2 206.2c-6 6-9.4 14.1-9.4 22.6v291.6c0 8.5 3.4 16.6 9.4 22.6l206.2 206.2c6 6 14.1 9.4 22.6 9.4h291.6c8.5 0 16.6-3.4 22.6-9.4l206.2-206.2c6-6 9.4-14.1 9.4-22.6v-291.6c0-8.5-3.4-16.6-9.4-22.6zM704 516.5l-187.5 187.5h-265l-187.5-187.5v-265l187.5-187.5h265.1l187.4 187.5v265z" | ||||||
|  |     ></path> | ||||||
|  |     <path | ||||||
|  |       d="M512 240.7c0-13.1-5.4-25.8-14.8-35-9.3-9.1-21.5-14-34.3-13.7-5.8 0.1-11.4 1.3-16.7 3.3-5.5-20.3-24.1-35.3-46.2-35.3s-40.8 15-46.3 35.4c-5.5-2.2-11.4-3.4-17.7-3.4-26.5 0-48 21.5-48 48v18.7c-4.8-1.7-9.8-2.6-15-2.7-12.8-0.3-25 4.6-34.3 13.7-9.4 9.2-14.8 21.9-14.8 35v111.3c0 56.6 10.1 104.3 29.1 137.9 19.8 34.9 49.2 54.1 82.9 54.1h106.6c38.9 0 74.2-21.4 92.2-55.9l67.5-129.4c0.1-0.2 0.2-0.3 0.3-0.5 4-7.7 5.8-16.3 5.3-25-1.4-24-20.5-43.4-44.5-45-16.3-1.1-32 6-41.8 19.1l-9.5 12.7v-143.3zM547.2 390.4c3.3-4.4 8.5-6.7 14-6.4 7.8 0.5 14.3 7.1 14.8 15 0.2 2.8-0.4 5.5-1.6 8-0.1 0.1-0.1 0.2-0.2 0.3l-67.7 129.9c-12.5 24-36.9 38.8-63.9 38.8h-106.6c-27.8 0-45.3-20.6-55.1-37.9-16.3-28.8-24.9-71-24.9-122.1v-111.3c0-4.5 1.9-8.9 5.1-12.2 3.1-3 7.1-4.7 11.2-4.6 8.8 0.2 15.6 7.2 15.6 16v112h32v-175.9c0-8.8 7.2-16 16-16s16 7.2 16 16v144h32v-176c0-8.8 7.2-16 16-16s16 7.2 16 16v176h32v-144c0-8.8 6.9-15.8 15.6-16 4.2-0.1 8.1 1.5 11.2 4.6 3.3 3.2 5.1 7.6 5.1 12.2v191.2c0 6.9 4.4 13 10.9 15.2s13.7-0.1 17.9-5.6l38.6-51.2z" | ||||||
|  |     ></path> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
| @@ -1,6 +1,7 @@ | |||||||
| <template> | <template> | ||||||
|   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> |   <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|     <path |     <path | ||||||
|  |       version="1.1" | ||||||
|       xmlns="http://www.w3.org/2000/svg" |       xmlns="http://www.w3.org/2000/svg" | ||||||
|       d="M28 13.333c0-0.708-0.188-1.4-0.538-2 0.346-0.6 0.538-1.292 0.538-2 0-1.208-0.546-2.329-1.442-3.075 0.071-0.3 0.108-0.612 0.108-0.925 0-2.204-1.796-4-4-4h-3.621c-3.842 0-7.683 0.946-11.104 2.729 0 0-0.004 0-0.004 0.004-2.429 1.279-3.938 3.792-3.938 6.563v4.671c0 1.137 0.263 2.275 0.763 3.292 0.504 1.025 1.246 1.925 2.146 2.613 1.237 0.942 2.417 1.542 3.554 2.117 2.175 1.104 3.896 1.979 5.412 4.962 0.313 0.783 0.817 1.417 1.475 1.846 0.708 0.462 1.529 0.633 2.321 0.483 0.921-0.175 1.717-0.767 2.246-1.671 0.508-0.871 0.758-2.004 0.742-3.358 0-1.283-0.296-2.863-1.029-4.25h2.375c2.204 0 4-1.796 4-4 0-0.708-0.188-1.4-0.538-2 0.346-0.6 0.533-1.292 0.533-2zM22.667 6.667h-2.667v1.333h4c0.208 0 0.404 0.046 0.587 0.137 0.454 0.221 0.746 0.683 0.746 1.196 0 0.329-0.121 0.646-0.337 0.887-0.254 0.283-0.613 0.442-0.992 0.446 0 0-0.004 0-0.004 0h-2.667v1.333h2.667c0 0 0.004 0 0.004 0 0.325 0 0.637 0.121 0.879 0.333 0.288 0.254 0.45 0.617 0.45 1s-0.163 0.746-0.45 1c-0.242 0.213-0.554 0.333-0.883 0.333h-2.667v1.333h2.667c0.379 0 0.742 0.163 0.996 0.446 0.217 0.242 0.337 0.558 0.337 0.887 0 0.733-0.6 1.333-1.333 1.333h-5.333c-0.6 0-1.125 0.4-1.283 0.979s0.083 1.192 0.6 1.5c1.379 0.829 2.008 2.887 2.008 4.45 0 0.004 0 0.012 0 0.017 0.021 1.633-0.475 2.321-0.813 2.387-0.254 0.046-0.629-0.188-0.833-0.725-0.017-0.046-0.033-0.087-0.058-0.129-1.917-3.813-4.304-5.025-6.617-6.2-1.033-0.525-2.1-1.067-3.146-1.863-1.162-0.883-1.858-2.296-1.858-3.779v-4.675c0-1.775 0.963-3.388 2.508-4.2 3.042-1.587 6.454-2.429 9.871-2.429h3.621c0.733 0 1.333 0.6 1.333 1.333 0 0.229-0.058 0.45-0.163 0.646-0.238 0.425-0.688 0.688-1.171 0.688z" |       d="M28 13.333c0-0.708-0.188-1.4-0.538-2 0.346-0.6 0.538-1.292 0.538-2 0-1.208-0.546-2.329-1.442-3.075 0.071-0.3 0.108-0.612 0.108-0.925 0-2.204-1.796-4-4-4h-3.621c-3.842 0-7.683 0.946-11.104 2.729 0 0-0.004 0-0.004 0.004-2.429 1.279-3.938 3.792-3.938 6.563v4.671c0 1.137 0.263 2.275 0.763 3.292 0.504 1.025 1.246 1.925 2.146 2.613 1.237 0.942 2.417 1.542 3.554 2.117 2.175 1.104 3.896 1.979 5.412 4.962 0.313 0.783 0.817 1.417 1.475 1.846 0.708 0.462 1.529 0.633 2.321 0.483 0.921-0.175 1.717-0.767 2.246-1.671 0.508-0.871 0.758-2.004 0.742-3.358 0-1.283-0.296-2.863-1.029-4.25h2.375c2.204 0 4-1.796 4-4 0-0.708-0.188-1.4-0.538-2 0.346-0.6 0.533-1.292 0.533-2zM22.667 6.667h-2.667v1.333h4c0.208 0 0.404 0.046 0.587 0.137 0.454 0.221 0.746 0.683 0.746 1.196 0 0.329-0.121 0.646-0.337 0.887-0.254 0.283-0.613 0.442-0.992 0.446 0 0-0.004 0-0.004 0h-2.667v1.333h2.667c0 0 0.004 0 0.004 0 0.325 0 0.637 0.121 0.879 0.333 0.288 0.254 0.45 0.617 0.45 1s-0.163 0.746-0.45 1c-0.242 0.213-0.554 0.333-0.883 0.333h-2.667v1.333h2.667c0.379 0 0.742 0.163 0.996 0.446 0.217 0.242 0.337 0.558 0.337 0.887 0 0.733-0.6 1.333-1.333 1.333h-5.333c-0.6 0-1.125 0.4-1.283 0.979s0.083 1.192 0.6 1.5c1.379 0.829 2.008 2.887 2.008 4.45 0 0.004 0 0.012 0 0.017 0.021 1.633-0.475 2.321-0.813 2.387-0.254 0.046-0.629-0.188-0.833-0.725-0.017-0.046-0.033-0.087-0.058-0.129-1.917-3.813-4.304-5.025-6.617-6.2-1.033-0.525-2.1-1.067-3.146-1.863-1.162-0.883-1.858-2.296-1.858-3.779v-4.675c0-1.775 0.963-3.388 2.508-4.2 3.042-1.587 6.454-2.429 9.871-2.429h3.621c0.733 0 1.333 0.6 1.333 1.333 0 0.229-0.058 0.45-0.163 0.646-0.238 0.425-0.688 0.688-1.171 0.688z" | ||||||
|     /> |     /> | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| <template> | <template> | ||||||
|   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> |   <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | ||||||
|     <path |     <path | ||||||
|  |       version="1.1" | ||||||
|       xmlns="http://www.w3.org/2000/svg" |       xmlns="http://www.w3.org/2000/svg" | ||||||
|       d="M27.462 16.667c0.346-0.6 0.538-1.292 0.538-2 0-2.204-1.796-4-4-4h-2.375c0.733-1.387 1.029-2.967 1.029-4.25 0.017-1.358-0.233-2.487-0.742-3.358-0.525-0.904-1.321-1.5-2.246-1.671-0.788-0.15-1.613 0.025-2.321 0.483-0.654 0.425-1.163 1.063-1.475 1.846-1.517 2.983-3.237 3.858-5.412 4.962-1.137 0.579-2.313 1.175-3.554 2.117-0.904 0.688-1.646 1.588-2.146 2.612-0.5 1.017-0.763 2.154-0.763 3.292v4.671c0 2.771 1.508 5.283 3.938 6.563 0 0 0.004 0 0.004 0.004 3.421 1.788 7.263 2.729 11.104 2.729h3.625c2.204 0 4-1.796 4-4 0-0.317-0.038-0.625-0.108-0.925 0.896-0.746 1.442-1.867 1.442-3.075 0-0.708-0.188-1.4-0.538-2 0.346-0.6 0.538-1.292 0.538-2s-0.188-1.4-0.538-2zM23.837 26.021c0.108 0.196 0.163 0.417 0.163 0.646 0 0.733-0.6 1.333-1.333 1.333h-3.621c-3.412 0-6.825-0.837-9.871-2.429-1.55-0.817-2.508-2.425-2.508-4.2v-4.671c0-1.483 0.696-2.896 1.858-3.779 1.050-0.796 2.117-1.338 3.146-1.863 2.308-1.175 4.696-2.387 6.617-6.2 0.021-0.042 0.042-0.083 0.058-0.129 0.2-0.533 0.579-0.771 0.833-0.725 0.337 0.063 0.833 0.75 0.813 2.388 0 0.004 0 0.013 0 0.017 0 1.563-0.629 3.621-2.008 4.45-0.512 0.308-0.758 0.921-0.6 1.5 0.158 0.575 0.683 0.975 1.283 0.975h5.333c0.733 0 1.333 0.6 1.333 1.333 0 0.329-0.121 0.646-0.337 0.887-0.254 0.283-0.617 0.446-0.996 0.446h-2.667v1.333h2.667c0.325 0 0.642 0.121 0.883 0.333 0.288 0.254 0.45 0.617 0.45 1s-0.163 0.746-0.45 1c-0.242 0.212-0.554 0.333-0.879 0.333 0 0-0.004 0-0.004 0v0h-2.667v1.333h2.667c0 0 0.004 0 0.004 0 0.375 0 0.738 0.163 0.992 0.446 0.217 0.242 0.337 0.558 0.337 0.887 0 0.512-0.292 0.975-0.746 1.196-0.183 0.092-0.383 0.137-0.587 0.137h-4v1.333h2.667c0.483 0 0.933 0.262 1.171 0.688z" |       d="M27.462 16.667c0.346-0.6 0.538-1.292 0.538-2 0-2.204-1.796-4-4-4h-2.375c0.733-1.387 1.029-2.967 1.029-4.25 0.017-1.358-0.233-2.487-0.742-3.358-0.525-0.904-1.321-1.5-2.246-1.671-0.788-0.15-1.613 0.025-2.321 0.483-0.654 0.425-1.163 1.063-1.475 1.846-1.517 2.983-3.237 3.858-5.412 4.962-1.137 0.579-2.313 1.175-3.554 2.117-0.904 0.688-1.646 1.588-2.146 2.612-0.5 1.017-0.763 2.154-0.763 3.292v4.671c0 2.771 1.508 5.283 3.938 6.563 0 0 0.004 0 0.004 0.004 3.421 1.788 7.263 2.729 11.104 2.729h3.625c2.204 0 4-1.796 4-4 0-0.317-0.038-0.625-0.108-0.925 0.896-0.746 1.442-1.867 1.442-3.075 0-0.708-0.188-1.4-0.538-2 0.346-0.6 0.538-1.292 0.538-2s-0.188-1.4-0.538-2zM23.837 26.021c0.108 0.196 0.163 0.417 0.163 0.646 0 0.733-0.6 1.333-1.333 1.333h-3.621c-3.412 0-6.825-0.837-9.871-2.429-1.55-0.817-2.508-2.425-2.508-4.2v-4.671c0-1.483 0.696-2.896 1.858-3.779 1.050-0.796 2.117-1.338 3.146-1.863 2.308-1.175 4.696-2.387 6.617-6.2 0.021-0.042 0.042-0.083 0.058-0.129 0.2-0.533 0.579-0.771 0.833-0.725 0.337 0.063 0.833 0.75 0.813 2.388 0 0.004 0 0.013 0 0.017 0 1.563-0.629 3.621-2.008 4.45-0.512 0.308-0.758 0.921-0.6 1.5 0.158 0.575 0.683 0.975 1.283 0.975h5.333c0.733 0 1.333 0.6 1.333 1.333 0 0.329-0.121 0.646-0.337 0.887-0.254 0.283-0.617 0.446-0.996 0.446h-2.667v1.333h2.667c0.325 0 0.642 0.121 0.883 0.333 0.288 0.254 0.45 0.617 0.45 1s-0.163 0.746-0.45 1c-0.242 0.212-0.554 0.333-0.879 0.333 0 0-0.004 0-0.004 0v0h-2.667v1.333h2.667c0 0 0.004 0 0.004 0 0.375 0 0.738 0.163 0.992 0.446 0.217 0.242 0.337 0.558 0.337 0.887 0 0.512-0.292 0.975-0.746 1.196-0.183 0.092-0.383 0.137-0.587 0.137h-4v1.333h2.667c0.483 0 0.933 0.262 1.171 0.688z" | ||||||
|     /> |     /> | ||||||
|   | |||||||
							
								
								
									
										21
									
								
								src/icons/IconTombstone.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | |||||||
|  | <template> | ||||||
|  |   <svg | ||||||
|  |     version="1.1" | ||||||
|  |     xmlns="http://www.w3.org/2000/svg" | ||||||
|  |     width="768" | ||||||
|  |     height="768" | ||||||
|  |     viewBox="0 0 768 768" | ||||||
|  |     fill="currentColor" | ||||||
|  |   > | ||||||
|  |     <path | ||||||
|  |       d="M672 608h-56.1l87.8-396.5c0.8-3.7 0.3-7.6-1.5-11l-102.2-192c-2.8-5.2-8.2-8.5-14.1-8.5h-393.9c-11.7 0-22.5 6.4-28.1 16.7l-96 176c-3.7 6.8-4.8 14.7-3.1 22.3l87.3 393h-56.1c-35.3 0-64 28.7-64 64v80c0 8.8 7.2 16 16 16h672c8.8 0 16-7.2 16-16v-80c0-35.3-28.7-64-64-64zM519.9 608l85.3-384h62.8l-85 384h-63.1zM661.4 192h-57.6l-87.3-160h59.8l85.1 160zM129.8 212.8l81.2-148.8h250l81.2 148.8-87.9 395.2h-236.6l-87.9-395.2zM96 736v-64h576v64h-576z" | ||||||
|  |     ></path> | ||||||
|  |     <path d="M320 224h32v160h-32v-160z"></path> | ||||||
|  |     <path | ||||||
|  |       d="M288 272c0-26.5-21.5-48-48-48h-32c-8.8 0-16 7.2-16 16v144h32v-64h6.1l34.8 69.5 28.6-14.3-30.5-61c14.9-8.2 25-24 25-42.2zM224 256h16c8.8 0 16 7.2 16 16s-7.2 16-16 16h-16v-32z" | ||||||
|  |     ></path> | ||||||
|  |     <path | ||||||
|  |       d="M480 272c0-26.5-21.5-48-48-48h-32c-8.8 0-16 7.2-16 16v144h32v-64h16c26.5 0 48-21.5 48-48zM416 256h16c8.8 0 16 7.2 16 16s-7.2 16-16 16h-16v-32z" | ||||||
|  |     ></path> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
							
								
								
									
										1
									
								
								src/icons/request-svg-icons/IconBinoculars.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | IconBinoculars.vue | ||||||
							
								
								
									
										7
									
								
								src/icons/request-svg-icons/accessibility.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,7 @@ | |||||||
|  | <!-- Generated by IcoMoon.io --> | ||||||
|  | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="768" height="768" viewBox="0 0 768 768"> | ||||||
|  | <title>accessibility</title> | ||||||
|  | <path d="M384 224c35.3 0 64-28.7 64-64s-28.7-64-64-64c-35.3 0-64 28.7-64 64s28.7 64 64 64zM384 128c17.6 0 32 14.4 32 32s-14.4 32-32 32c-17.6 0-32-14.4-32-32s14.4-32 32-32z"></path> | ||||||
|  | <path d="M586.1 254.4l-20.2-60.8-181.9 60.7-181.9-60.7-20.2 60.8 170.1 56.7v127.1l-130.5 191.8 53 36 109.5-161.1 109.5 161.1 53-36-130.5-191.8v-127.1z"></path> | ||||||
|  | <path d="M737.8 234.5c-19.3-45.7-47-86.8-82.3-122.1s-76.3-62.9-122.1-82.3c-47.3-19.9-97.6-30.1-149.4-30.1s-102.1 10.2-149.5 30.2c-45.7 19.3-86.8 47-122.1 82.3s-62.9 76.3-82.3 122.1c-19.9 47.3-30.1 97.6-30.1 149.4s10.2 102.1 30.2 149.5c19.3 45.7 47 86.8 82.3 122.1s76.3 62.9 122.1 82.3c47.4 20 97.6 30.2 149.5 30.2 51.8 0 102.1-10.2 149.5-30.2 45.7-19.3 86.8-47 122.1-82.3s62.9-76.3 82.3-122.1c20-47.4 30.2-97.6 30.2-149.5-0.2-51.8-10.4-102.1-30.4-149.5zM384 736c-194.1 0-352-157.9-352-352s157.9-352 352-352c194.1 0 352 157.9 352 352s-157.9 352-352 352z"></path> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 1.0 KiB | 
							
								
								
									
										7
									
								
								src/icons/request-svg-icons/at-sign.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,7 @@ | |||||||
|  | <!-- Generated by IcoMoon.io --> | ||||||
|  | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="768" height="768" viewBox="0 0 768 768"> | ||||||
|  | <title></title> | ||||||
|  | <g id="icomoon-ignore"> | ||||||
|  | </g> | ||||||
|  | <path d="M737.8 234.5c-19.3-45.7-47-86.8-82.3-122-35.3-35.3-76.3-62.9-122-82.3-47.4-20-97.7-30.2-149.5-30.2s-102.1 10.2-149.5 30.2c-45.7 19.3-86.8 47-122 82.3-35.3 35.3-62.9 76.3-82.3 122-20 47.4-30.2 97.7-30.2 149.5s10.2 102.1 30.2 149.5c19.3 45.7 47 86.8 82.3 122 35.3 35.3 76.3 62.9 122 82.3 47.4 20 97.7 30.2 149.5 30.2h160v-64h-160c-176.4 0-320-143.6-320-320s143.6-320 320-320c176.4 0 320 143.6 320 320v16c0 44.1-35.9 80-80 80s-80-35.9-80-80v-176h-64v32.1c-26.8-20.1-60-32.1-96-32.1-88.2 0-160 71.8-160 160s71.8 160 160 160c49.9 0 94.5-23 123.9-58.9 26.2 35.7 68.5 58.9 116.1 58.9 79.4 0 144-64.6 144-144v-16c0-51.8-10.2-102.1-30.2-149.5zM384 480c-52.9 0-96-43.1-96-96s43.1-96 96-96 96 43.1 96 96-43.1 96-96 96z"></path> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 913 B | 
							
								
								
									
										9
									
								
								src/icons/request-svg-icons/binoculars.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | |||||||
|  | <!-- Generated by IcoMoon.io --> | ||||||
|  | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="768" height="768" viewBox="0 0 768 768"> | ||||||
|  | <title></title> | ||||||
|  | <g id="icomoon-ignore"> | ||||||
|  | </g> | ||||||
|  | <path d="M751.5 453.5v0l-121.7-260.3c-0.1-0.2-0.2-0.4-0.3-0.7-13-27.7-36.7-48.5-64.7-58.3l-27.1-46.1c-0.1-0.1-0.2-0.3-0.3-0.4-8.6-14.6-24.5-23.7-41.4-23.7-26.5 0-48 21.5-48 48v49.7c-19.8 20.2-32 47.9-32 78.3v24.6c-9.4-5.5-20.4-8.6-32-8.6s-22.6 3.1-32 8.6v-24.6c0-30.4-12.2-58.1-32-78.3v-49.7c0-26.5-21.5-48-48-48-16.9 0-32.8 9.1-41.4 23.7-0.1 0.1-0.2 0.3-0.3 0.4l-27.1 46.1c-28.1 9.8-51.8 30.6-64.7 58.3-0.1 0.2-0.2 0.4-0.3 0.6l-121.7 260.4c-10.6 22.7-16.5 47.9-16.5 74.5 0 97 79 176 176 176 94.6 0 172-75 175.8-168.7 9.5 5.5 20.4 8.7 32.2 8.7 11.7 0 22.7-3.2 32.2-8.7 3.8 93.7 81.2 168.7 175.8 168.7 97 0 176-79 176-176 0-26.6-5.9-51.8-16.5-74.5zM636 357.6c-14.1-3.6-28.8-5.6-44-5.6-42.5 0-81.5 15.1-112 40.3v-152.3c0-26.5 21.5-48 48-48 18.4 0 35.3 10.7 43.3 27.2 0.1 0.2 0.2 0.3 0.2 0.5l64.5 137.9zM496 96c5.6 0 10.7 2.8 13.6 7.6 0.1 0.1 0.1 0.2 0.2 0.3l14.2 24.2c-15.7 0.6-30.6 4.4-44 10.7v-26.8c0-8.8 7.2-16 16-16zM384 288c17.6 0 32 14.4 32 32v104.6c-9.4-5.5-20.4-8.6-32-8.6s-22.6 3.1-32 8.6v-104.6c0-17.6 14.4-32 32-32zM258.4 103.6c2.9-4.8 8-7.6 13.6-7.6 8.8 0 16 7.2 16 16v26.8c-13.4-6.4-28.3-10.2-44-10.7l14.2-24.2c0.1-0.1 0.1-0.2 0.2-0.3zM196.5 219.7c0.1-0.2 0.1-0.3 0.2-0.5 8-16.5 24.9-27.2 43.3-27.2 26.5 0 48 21.5 48 48v152.3c-30.5-25.2-69.5-40.3-112-40.3-15.2 0-29.9 1.9-44 5.6l64.5-137.9zM176 640c-61.8 0-112-50.2-112-112s50.2-112 112-112 112 50.2 112 112-50.2 112-112 112zM384 512c-17.6 0-32-14.4-32-32s14.4-32 32-32c17.6 0 32 14.4 32 32s-14.4 32-32 32zM592 640c-61.8 0-112-50.2-112-112s50.2-112 112-112 112 50.2 112 112-50.2 112-112 112z"></path> | ||||||
|  | <path d="M128 528v-16h-32v16c0 44.1 35.9 80 80 80h16v-32h-16c-26.5 0-48-21.5-48-48z"></path> | ||||||
|  | <path d="M544 528v-16h-32v16c0 44.1 35.9 80 80 80h16v-32h-16c-26.5 0-48-21.5-48-48z"></path> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 1.9 KiB | 
							
								
								
									
										5
									
								
								src/icons/request-svg-icons/bookmark.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,5 @@ | |||||||
|  | <!-- Generated by IcoMoon.io --> | ||||||
|  | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="768" height="768" viewBox="0 0 768 768"> | ||||||
|  | <title>bookmark</title> | ||||||
|  | <path d="M576 32h-384c-35.3 0-64 28.7-64 64v608c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l201.3-201.4 201.4 201.4c6.1 6.1 14.3 9.4 22.6 9.4 4.1 0 8.3-0.8 12.2-2.4 12-5 19.8-16.6 19.8-29.6v-608.1c0-35.3-28.7-64-64-64zM576 626.7l-169.4-169.3c-6.2-6.2-14.4-9.4-22.6-9.4s-16.4 3.1-22.6 9.4l-169.4 169.3v-530.7h384v530.7z"></path> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 494 B | 
							
								
								
									
										5
									
								
								src/icons/request-svg-icons/branches2.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,5 @@ | |||||||
|  | <!-- Generated by IcoMoon.io --> | ||||||
|  | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="768" height="768" viewBox="0 0 768 768"> | ||||||
|  | <title>branches2</title> | ||||||
|  | <path d="M704 517.5v-101.5c0-35.3-28.7-64-64-64h-224v-101.5c37.2-13.2 64-48.8 64-90.5 0-52.9-43.1-96-96-96s-96 43.1-96 96c0 41.7 26.8 77.3 64 90.5v101.5h-224c-35.3 0-64 28.7-64 64v101.5c-37.2 13.2-64 48.8-64 90.5 0 52.9 43.1 96 96 96s96-43.1 96-96c0-41.7-26.8-77.3-64-90.5v-101.5h224v101.5c-37.2 13.2-64 48.8-64 90.5 0 52.9 43.1 96 96 96s96-43.1 96-96c0-41.7-26.8-77.3-64-90.5v-101.5h224v101.5c-37.2 13.2-64 48.8-64 90.5 0 52.9 43.1 96 96 96s96-43.1 96-96c0-41.7-26.8-77.3-64-90.5zM320 160c0-35.3 28.7-64 64-64s64 28.7 64 64-28.7 64-64 64c-35.3 0-64-28.7-64-64zM160 608c0 35.3-28.7 64-64 64s-64-28.7-64-64 28.7-64 64-64 64 28.7 64 64zM448 608c0 35.3-28.7 64-64 64s-64-28.7-64-64 28.7-64 64-64c35.3 0 64 28.7 64 64zM672 672c-35.3 0-64-28.7-64-64s28.7-64 64-64 64 28.7 64 64-28.7 64-64 64z"></path> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 964 B | 
							
								
								
									
										5
									
								
								src/icons/request-svg-icons/bricks.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,5 @@ | |||||||
|  | <!-- Generated by IcoMoon.io --> | ||||||
|  | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="768" height="768" viewBox="0 0 768 768"> | ||||||
|  | <title>bricks</title> | ||||||
|  | <path d="M704 0h-640c-35.3 0-64 28.7-64 64v640c0 35.3 28.7 64 64 64h640c35.3 0 64-28.7 64-64v-640c0-35.3-28.7-64-64-64zM704 128h-128v-64h128v64zM320 128v-64h224v64h-224zM448 160v128h-256v-128h256zM320 448v-128h224v128h-224zM160 288h-96v-128h96v128zM288 320v128h-224v-128h224zM160 480v128h-96v-128h96zM192 480h256v128h-256v-128zM544 640v64h-224v-64h224zM480 608v-128h224v128h-224zM576 448v-128h128v128h-128zM480 288v-128h224v128h-224zM288 64v64h-224v-64h224zM64 640h224v64h-224v-64zM704 704h-128v-64h128.1l-0.1 64c0.1 0 0 0 0 0z"></path> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 701 B | 
							
								
								
									
										14
									
								
								src/icons/request-svg-icons/calendar-2.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | |||||||
|  | <!-- Generated by IcoMoon.io --> | ||||||
|  | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="768" height="768" viewBox="0 0 768 768"> | ||||||
|  | <title></title> | ||||||
|  | <g id="icomoon-ignore"> | ||||||
|  | </g> | ||||||
|  | <path d="M0 32h160v64h-160v-64z"></path> | ||||||
|  | <path d="M288 32h192v64h-192v-64z"></path> | ||||||
|  | <path d="M608 32h160v64h-160v-64z"></path> | ||||||
|  | <path d="M192 0h64v224h-64v-224z"></path> | ||||||
|  | <path d="M512 0h64v224h-64v-224z"></path> | ||||||
|  | <path d="M288 128h192v64h-192v-64z"></path> | ||||||
|  | <path d="M704 128h-96v64h96v512h-640v-512h96v-64h-96c-35.3 0-64 28.7-64 64v512c0 35.3 28.7 64 64 64h640c35.3 0 64-28.7 64-64v-512c0-35.3-28.7-64-64-64z"></path> | ||||||
|  | <path d="M128 304v320c0 8.8 7.2 16 16 16h480c8.8 0 16-7.2 16-16v-320c0-8.8-7.2-16-16-16h-480c-8.8 0-16 7.2-16 16zM160 480h128v128h-128v-128zM448 480v128h-128v-128h128zM320 448v-128h128v128h-128zM480 608v-128h128v128h-128zM608 448h-128v-128h128v128zM288 320v128h-128v-128h128z"></path> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 888 B | 
							
								
								
									
										39
									
								
								src/icons/request-svg-icons/calendar-3.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,39 @@ | |||||||
|  | <!-- Generated by IcoMoon.io --> | ||||||
|  | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="768" height="768" viewBox="0 0 768 768"> | ||||||
|  | <title></title> | ||||||
|  | <g id="icomoon-ignore"> | ||||||
|  | </g> | ||||||
|  | <path d="M0 32h160v64h-160v-64z"></path> | ||||||
|  | <path d="M288 32h192v64h-192v-64z"></path> | ||||||
|  | <path d="M608 32h160v64h-160v-64z"></path> | ||||||
|  | <path d="M192 0h64v224h-64v-224z"></path> | ||||||
|  | <path d="M512 0h64v224h-64v-224z"></path> | ||||||
|  | <path d="M288 128h192v64h-192v-64z"></path> | ||||||
|  | <path d="M704 128h-96v64h96v512h-480v-144c0-8.8-7.2-16-16-16h-144v-352h96v-64h-96c-35.3 0-64 28.7-64 64v352c0 123.5 100.5 224 224 224h480c35.3 0 64-28.7 64-64v-512c0-35.3-28.7-64-64-64zM67.2 576h124.8v124.8c-62.6-12.8-112-62.2-124.8-124.8z"></path> | ||||||
|  | <path d="M352 288h64v32h-64v-32z"></path> | ||||||
|  | <path d="M448 288h64v32h-64v-32z"></path> | ||||||
|  | <path d="M544 288h64v32h-64v-32z"></path> | ||||||
|  | <path d="M256 352h64v32h-64v-32z"></path> | ||||||
|  | <path d="M352 352h64v32h-64v-32z"></path> | ||||||
|  | <path d="M448 352h64v32h-64v-32z"></path> | ||||||
|  | <path d="M544 352h64v32h-64v-32z"></path> | ||||||
|  | <path d="M160 352h64v32h-64v-32z"></path> | ||||||
|  | <path d="M256 416h64v32h-64v-32z"></path> | ||||||
|  | <path d="M352 416h64v32h-64v-32z"></path> | ||||||
|  | <path d="M448 416h64v32h-64v-32z"></path> | ||||||
|  | <path d="M544 416h64v32h-64v-32z"></path> | ||||||
|  | <path d="M160 416h64v32h-64v-32z"></path> | ||||||
|  | <path d="M256 480h64v32h-64v-32z"></path> | ||||||
|  | <path d="M352 480h64v32h-64v-32z"></path> | ||||||
|  | <path d="M448 480h64v32h-64v-32z"></path> | ||||||
|  | <path d="M544 480h64v32h-64v-32z"></path> | ||||||
|  | <path d="M160 480h64v32h-64v-32z"></path> | ||||||
|  | <path d="M256 544h64v32h-64v-32z"></path> | ||||||
|  | <path d="M352 544h64v32h-64v-32z"></path> | ||||||
|  | <path d="M448 544h64v32h-64v-32z"></path> | ||||||
|  | <path d="M544 544h64v32h-64v-32z"></path> | ||||||
|  | <path d="M256 608h64v32h-64v-32z"></path> | ||||||
|  | <path d="M352 608h64v32h-64v-32z"></path> | ||||||
|  | <path d="M448 608h64v32h-64v-32z"></path> | ||||||
|  | <path d="M544 608h64v32h-64v-32z"></path> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 1.7 KiB | 
							
								
								
									
										26
									
								
								src/icons/request-svg-icons/calendar-time.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,26 @@ | |||||||
|  | <!-- Generated by IcoMoon.io --> | ||||||
|  | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="768" height="768" viewBox="0 0 768 768"> | ||||||
|  | <title></title> | ||||||
|  | <g id="icomoon-ignore"> | ||||||
|  | </g> | ||||||
|  | <path d="M0 32h160v64h-160v-64z"></path> | ||||||
|  | <path d="M288 32h192v64h-192v-64z"></path> | ||||||
|  | <path d="M608 32h160v64h-160v-64z"></path> | ||||||
|  | <path d="M192 0h64v224h-64v-224z"></path> | ||||||
|  | <path d="M512 0h64v224h-64v-224z"></path> | ||||||
|  | <path d="M288 128h192v64h-192v-64z"></path> | ||||||
|  | <path d="M352 288h64v32h-64v-32z"></path> | ||||||
|  | <path d="M448 288h64v32h-64v-32z"></path> | ||||||
|  | <path d="M544 288h64v32h-64v-32z"></path> | ||||||
|  | <path d="M256 352h64v32h-64v-32z"></path> | ||||||
|  | <path d="M352 352h64v32h-64v-32z"></path> | ||||||
|  | <path d="M160 352h64v32h-64v-32z"></path> | ||||||
|  | <path d="M256 416h64v32h-64v-32z"></path> | ||||||
|  | <path d="M160 416h64v32h-64v-32z"></path> | ||||||
|  | <path d="M256 480h64v32h-64v-32z"></path> | ||||||
|  | <path d="M160 480h64v32h-64v-32z"></path> | ||||||
|  | <path d="M256 544h64v32h-64v-32z"></path> | ||||||
|  | <path d="M256 608h64v32h-64v-32z"></path> | ||||||
|  | <path d="M704 128h-96v64h96v218c-37.4-35.9-88.2-58-144-58-114.7 0-208 93.3-208 208 0 55.8 22.1 106.6 58 144h-186v-144c0-8.8-7.2-16-16-16h-144v-352h96v-64h-96c-35.3 0-64 28.7-64 64v352c0 123.5 100.5 224 224 224h336c114.7 0 208-93.3 208-208v-368c0-35.3-28.7-64-64-64zM67.2 576h124.8v124.8c-62.6-12.8-112-62.2-124.8-124.8zM576 735.3v-63.3h-32v63.3c-84.3-7.6-151.6-75-159.3-159.3h63.3v-32h-63.3c7.6-84.3 75-151.6 159.3-159.3v63.3h32v-63.3c84.3 7.6 151.6 75 159.3 159.3h-63.3v32h63.3c-7.7 84.3-75 151.6-159.3 159.3z"></path> | ||||||
|  | <path d="M557.5 566.9l-61.5-61.5-22.6 22.6 75.3 75.3c3 3 7.1 4.7 11.3 4.7 0.5 0 1.1 0 1.6-0.1 4.8-0.5 9.1-3.1 11.7-7l72.9-109.3-26.6-17.8-62.1 93.1z"></path> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 1.6 KiB | 
							
								
								
									
										7
									
								
								src/icons/request-svg-icons/camera.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,7 @@ | |||||||
|  | <!-- Generated by IcoMoon.io --> | ||||||
|  | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="768" height="768" viewBox="0 0 768 768"> | ||||||
|  | <title></title> | ||||||
|  | <g id="icomoon-ignore"> | ||||||
|  | </g> | ||||||
|  | <path d="M768 256c0-88.2-71.8-160-160-160-25.2 0-50.4 6-72.7 17.4-6.5 3.3-12.8 7.1-18.8 11.3-8-13.1-17.5-25.4-28.7-36.5-36.3-36.2-84.5-56.2-135.8-56.2s-99.5 20-135.8 56.2-56.2 84.5-56.2 135.8c0 30.3 7.2 60.4 20.9 87.2 10.7 21 25.5 40.1 43.1 55.9v32.4l-179.4-76.9c-9.9-4.2-21.2-3.2-30.2 2.7s-14.4 15.9-14.4 26.7v352c0 11.4 6.1 22 15.9 27.7 5 2.9 10.5 4.3 16.1 4.3 5.5 0 11-1.4 15.9-4.2l176.1-100.7v40.9c0 35.3 28.7 64 64 64h416c35.3 0 64-28.7 64-64v-320c0-13.6-4.2-26.1-11.5-36.5 7.6-18.8 11.5-39 11.5-59.5zM704 608h-416v-256h416v256zM608 160c52.9 0 96 43.1 96 96 0 11-1.9 21.8-5.5 32h-165.4c7.2-20.3 10.9-41.9 10.9-64 0-12.4-1.2-24.5-3.4-36.4 17.9-17.7 41.8-27.6 67.4-27.6zM352 96c70.6 0 128 57.4 128 128 0 22.8-5.9 44.7-17.1 64h-174.9c-14.4 0-27.7 4.8-38.4 12.9-16.5-21.9-25.6-48.6-25.6-76.9 0-70.6 57.4-128 128-128zM64 648.9v-248.4l160 68.6v88.3l-160 91.5zM288 672.1c0 0 0-0.1 0 0v-32.1h416v32l-416 0.1z"></path> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 1.1 KiB | 
							
								
								
									
										11
									
								
								src/icons/request-svg-icons/chart-upward.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | |||||||
|  | <!-- Generated by IcoMoon.io --> | ||||||
|  | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="768" height="768" viewBox="0 0 768 768"> | ||||||
|  | <title></title> | ||||||
|  | <g id="icomoon-ignore"> | ||||||
|  | </g> | ||||||
|  | <path d="M512 480h-64c-17.7 0-32 14.3-32 32v240c0 8.8 7.2 16 16 16h96c8.8 0 16-7.2 16-16v-240c0-17.7-14.3-32-32-32zM448 736v-192h32v192h-32z"></path> | ||||||
|  | <path d="M320 352h-64c-17.7 0-32 14.3-32 32v368c0 8.8 7.2 16 16 16h96c8.8 0 16-7.2 16-16v-368c0-17.7-14.3-32-32-32zM256 736v-320h32v320h-32z"></path> | ||||||
|  | <path d="M128 480h-64c-17.7 0-32 14.3-32 32v240c0 8.8 7.2 16 16 16h96c8.8 0 16-7.2 16-16v-240c0-17.7-14.3-32-32-32zM64 736v-192h32v192h-32z"></path> | ||||||
|  | <path d="M704 224h-64c-17.7 0-32 14.3-32 32v496c0 8.8 7.2 16 16 16h96c8.8 0 16-7.2 16-16v-496c0-17.7-14.3-32-32-32zM640 736v-448h32v448h-32z"></path> | ||||||
|  | <path d="M752 0h-80v32h38.2l-281.7 222.4-216.1-61.7c-5-1.4-10.3-0.3-14.4 2.9l-172.5 138 20 25 166-132.8 216.1 61.7c4.9 1.4 10.3 0.4 14.3-2.8l294.1-232.3v43.6h32v-80c0-8.8-7.2-16-16-16z"></path> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 980 B | 
							
								
								
									
										6
									
								
								src/icons/request-svg-icons/chart.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | |||||||
|  | <!-- Generated by IcoMoon.io --> | ||||||
|  | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="768" height="768" viewBox="0 0 768 768"> | ||||||
|  | <title>chart</title> | ||||||
|  | <path d="M288 544c35.3 0 64-28.7 64-64 0-11.7-3.2-22.8-8.7-32.2l73-127.8c3.2 0 6.3-0.3 9.4-0.7l58.7 73.4c-2.8 7.2-4.4 15.1-4.4 23.3 0 35.3 28.7 64 64 64s64-28.7 64-64c0-15.6-5.6-30-15-41.1l90.1-247.9c30-5.3 52.9-31.5 52.9-63 0-35.3-28.7-64-64-64s-64 28.7-64 64c0 15.6 5.6 30 15 41.1l-89.6 246.4-57.8-72.2c2.8-7.2 4.4-15.1 4.4-23.3 0-35.3-28.7-64-64-64s-64 28.7-64 64c0 11.7 3.2 22.8 8.7 32.2l-73 127.8c-35.2 0.2-63.7 28.8-63.7 64 0 35.3 28.7 64 64 64zM544 448c-17.6 0-32-14.4-32-32s14.4-32 32-32 32 14.4 32 32-14.4 32-32 32zM672 32c17.6 0 32 14.4 32 32s-14.4 32-32 32-32-14.4-32-32 14.4-32 32-32zM416 224c17.6 0 32 14.4 32 32s-14.4 32-32 32-32-14.4-32-32 14.4-32 32-32zM288 448c17.6 0 32 14.4 32 32s-14.4 32-32 32-32-14.4-32-32 14.4-32 32-32z"></path> | ||||||
|  | <path d="M758.6 649.4l-72-72-45.3 45.3 17.4 17.4h-530.7v-530.8l17.4 17.4 45.3-45.3-72-72c-12.5-12.5-32.8-12.5-45.3 0l-72 72 45.3 45.3 17.3-17.4v562.7c0 17.7 14.3 32 32 32h562.7l-17.4 17.4 45.3 45.3 72-72c12.5-12.6 12.5-32.8 0-45.3z"></path> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 1.1 KiB | 
							
								
								
									
										9
									
								
								src/icons/request-svg-icons/clapboard-play.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | |||||||
|  | <!-- Generated by IcoMoon.io --> | ||||||
|  | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="768" height="768" viewBox="0 0 768 768"> | ||||||
|  | <title></title> | ||||||
|  | <g id="icomoon-ignore"> | ||||||
|  | </g> | ||||||
|  | <path d="M752 256h-402l374-96.5c8.5-2.2 13.7-10.8 11.5-19.4l-32-128c-2.1-8.6-10.8-13.8-19.4-11.6l-560.9 140.2c-12.5-8-27.3-12.7-43.2-12.7-44.1 0-80 35.9-80 80v496c0 35.3 28.7 64 64 64h640c35.3 0 64-28.7 64-64v-432c0-8.8-7.2-16-16-16v0zM272 384c10.1-13.4 16-30 16-48s-6-34.6-16-48h121.4l-96 96h-25.4zM438.6 288h114.7l-96 96h-114.7l96-96zM598.6 288h114.7l-96 96h-114.7l96-96zM736 310.6v73.4h-73.4l73.4-73.4zM208 256c-26.5 0-48-21.5-48-48 0-14.9-4.1-28.8-11.2-40.7l88.6-22.2 124.4 74.8-139.8 36.1h-14zM488.7 187.2l-83.3 21.5-124-74.5 84.1-21 123.2 74zM409.3 102.2l84.1-21 122 73.3-83.3 21.5-122.8-73.8zM700.6 132.5l-41.7 10.8-121.6-73.1 139-34.8 24.3 97.1zM32 208c0-26.5 21.5-48 48-48s48 21.5 48 48c0 44.1 35.9 80 80 80 26.5 0 48 21.5 48 48s-21.5 48-48 48h-128c-26.5 0-48-21.5-48-48v-128zM64 704v-289.6c5.2 1.1 10.5 1.6 16 1.6h624v288h-640z"></path> | ||||||
|  | <path d="M96 352c17.6 0 32-14.4 32-32s-14.4-32-32-32-32 14.4-32 32 14.4 32 32 32z"></path> | ||||||
|  | <path d="M295.6 669.6c2.6 1.6 5.5 2.4 8.4 2.4 2.4 0 4.9-0.6 7.2-1.7l192-96c5.4-2.7 8.8-8.3 8.8-14.3s-3.4-11.6-8.8-14.3l-192-96c-5-2.5-10.8-2.2-15.6 0.7-4.7 2.9-7.6 8.1-7.6 13.6v192c0 5.5 2.9 10.7 7.6 13.6zM320 489.9l140.2 70.1-140.2 70.1v-140.2z"></path> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 1.3 KiB | 
							
								
								
									
										6
									
								
								src/icons/request-svg-icons/cloud-check.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | |||||||
|  | <!-- Generated by IcoMoon.io --> | ||||||
|  | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="768" height="768" viewBox="0 0 768 768"> | ||||||
|  | <title>cloud-check</title> | ||||||
|  | <path d="M765.7 386.1c1.5-11.3 2.3-22.7 2.3-34.1 0-76.9-30-149.3-84.3-203.7-54.4-54.3-126.8-84.3-203.7-84.3-60 0-117.6 18.3-166.4 52.9-40 28.3-71.6 65.7-92.7 109.3-9.5-1.4-19.1-2.2-28.9-2.2-51.3 0-99.5 20-135.8 56.2s-56.2 84.5-56.2 135.8 20 99.5 56.2 135.8c36.3 36.2 84.5 56.2 135.8 56.2h192.8c37 57.7 101.7 96 175.2 96 114.7 0 208-93.3 208-208 0-23.8-4-46.7-11.4-68 4.2-13.6 7.3-27.7 9.1-41.9zM192 544c-70.6 0-128-57.4-128-128s57.4-128 128-128c43.2 0 83.1 21.5 106.9 57.6l53.4-35.2c-18-27.3-42.1-49-70-63.9 38.6-71.9 114.6-118.5 197.7-118.5 121.5 0 220.7 97.2 223.9 218-37.4-35.9-88.1-58-143.9-58-114.7 0-208 93.3-208 208 0 16.5 1.9 32.6 5.6 48h-165.6zM560 672c-97 0-176-79-176-176s79-176 176-176 176 79 176 176-79 176-176 176z"></path> | ||||||
|  | <path d="M528 537.4l-64-64-22.6 22.6 75.3 75.3c3.1 3.1 7.2 4.7 11.3 4.7s8.2-1.6 11.3-4.7l139.3-139.3-22.6-22.6-128 128z"></path> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 1.0 KiB | 
							
								
								
									
										6
									
								
								src/icons/request-svg-icons/cloud-cross.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | |||||||
|  | <!-- Generated by IcoMoon.io --> | ||||||
|  | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="768" height="768" viewBox="0 0 768 768"> | ||||||
|  | <title>cloud-cross</title> | ||||||
|  | <path d="M765.7 386.1c1.5-11.3 2.3-22.7 2.3-34.1 0-76.9-30-149.3-84.3-203.7-54.4-54.3-126.8-84.3-203.7-84.3-60 0-117.6 18.3-166.4 52.9-40 28.3-71.6 65.7-92.7 109.3-9.5-1.4-19.1-2.2-28.9-2.2-51.3 0-99.5 20-135.8 56.2s-56.2 84.5-56.2 135.8 20 99.5 56.2 135.8c36.3 36.2 84.5 56.2 135.8 56.2h192.8c37 57.7 101.7 96 175.2 96 114.7 0 208-93.3 208-208 0-23.8-4-46.7-11.4-68 4.2-13.6 7.3-27.7 9.1-41.9zM192 544c-70.6 0-128-57.4-128-128s57.4-128 128-128c43.2 0 83.1 21.5 106.9 57.6l53.4-35.2c-18-27.3-42.1-49-70-63.9 38.6-71.9 114.6-118.5 197.7-118.5 121.5 0 220.7 97.2 223.9 218-37.4-35.9-88.1-58-143.9-58-114.7 0-208 93.3-208 208 0 16.5 1.9 32.6 5.6 48h-165.6zM560 672c-97 0-176-79-176-176s79-176 176-176 176 79 176 176-79 176-176 176z"></path> | ||||||
|  | <path d="M624 409.4l-64 64-64-64-22.6 22.6 64 64-64 64 22.6 22.6 64-64 64 64 22.6-22.6-64-64 64-64z"></path> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 1016 B | 
							
								
								
									
										6
									
								
								src/icons/request-svg-icons/cloud-lock.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | |||||||
|  | <!-- Generated by IcoMoon.io --> | ||||||
|  | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="768" height="768" viewBox="0 0 768 768"> | ||||||
|  | <title>cloud-lock</title> | ||||||
|  | <path d="M765.7 386.1c1.5-11.3 2.3-22.7 2.3-34.1 0-76.9-30-149.3-84.3-203.7-54.4-54.3-126.8-84.3-203.7-84.3-60 0-117.6 18.3-166.4 52.9-40 28.3-71.6 65.7-92.7 109.3-9.5-1.4-19.1-2.2-28.9-2.2-51.3 0-99.5 20-135.8 56.2s-56.2 84.5-56.2 135.8 20 99.5 56.2 135.8c36.3 36.2 84.5 56.2 135.8 56.2h192.8c37 57.7 101.7 96 175.2 96 114.7 0 208-93.3 208-208 0-23.8-4-46.7-11.4-68 4.2-13.6 7.3-27.7 9.1-41.9zM192 544c-70.6 0-128-57.4-128-128s57.4-128 128-128c43.2 0 83.1 21.5 106.9 57.6l53.4-35.2c-18-27.3-42.1-49-70-63.9 38.6-71.9 114.6-118.5 197.7-118.5 121.5 0 220.7 97.2 223.9 218-37.4-35.9-88.1-58-143.9-58-114.7 0-208 93.3-208 208 0 16.5 1.9 32.6 5.6 48h-165.6zM560 672c-97 0-176-79-176-176s79-176 176-176 176 79 176 176-79 176-176 176z"></path> | ||||||
|  | <path d="M656 480h-16v-16c0-44.1-35.9-80-80-80s-80 35.9-80 80v16h-16c-8.8 0-16 7.2-16 16v96c0 8.8 7.2 16 16 16h192c8.8 0 16-7.2 16-16v-96c0-8.8-7.2-16-16-16zM512 464c0-26.5 21.5-48 48-48s48 21.5 48 48v16h-96v-16zM640 576h-160v-64h160v64z"></path> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 1.1 KiB |