Upgraded entries, plugins, router & webpack to vue 3 & typescript

This commit is contained in:
2022-08-06 16:14:44 +02:00
parent d279298dec
commit 96c412ca49
11 changed files with 406 additions and 373 deletions

View File

@@ -8,7 +8,10 @@
</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
class="content"
:key="router.currentRoute.value.path"
></router-view>
<!-- Popup that will show above existing rendered content --> <!-- Popup that will show above existing rendered content -->
<popup /> <popup />
@@ -17,28 +20,21 @@
</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; display: grid;
grid-template-rows: var(--header-size); grid-template-rows: var(--header-size);
grid-template-columns: var(--header-size) 1fr; grid-template-columns: var(--header-size) 1fr;
@@ -73,5 +69,5 @@ export default {
grid-column: 1 / 3; grid-column: 1 / 3;
} }
} }
} }
</style> </style>

View File

@@ -1,5 +1,5 @@
import config from "./config"; import config from "./config";
import { IList } from "./interfaces/IList"; import { IList, IMediaCredits, IPersonCredits } from "./interfaces/IList";
let { SEASONED_URL, ELASTIC_URL, ELASTIC_INDEX } = config; let { SEASONED_URL, ELASTIC_URL, ELASTIC_INDEX } = config;
if (!SEASONED_URL) { if (!SEASONED_URL) {
@@ -99,24 +99,12 @@ const getPerson = (id, credits = false) => {
}); });
}; };
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", SEASONED_URL);
url.pathname = `${url.pathname}/${id.toString()}/credits`; url.pathname = `${url.pathname}/${id.toString()}/credits`;
@@ -133,7 +121,7 @@ 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", SEASONED_URL);
url.pathname = `${url.pathname}/${id.toString()}/credits`; url.pathname = `${url.pathname}/${id.toString()}/credits`;
@@ -150,7 +138,7 @@ 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", SEASONED_URL);
url.pathname = `${url.pathname}/${id.toString()}/credits`; url.pathname = `${url.pathname}/${id.toString()}/credits`;
@@ -250,7 +238,7 @@ const searchTorrents = query => {
* @param {boolean} tmdb_id * @param {boolean} tmdb_id
* @returns {object} Success/Failure response * @returns {object} Success/Failure response
*/ */
const addMagnet = (magnet, name, tmdb_id) => { const addMagnet = (magnet: string, name: string, tmdb_id: number | null) => {
const url = new URL("/api/v1/pirate/add", SEASONED_URL); const url = new URL("/api/v1/pirate/add", SEASONED_URL);
const options = { const options = {
@@ -438,7 +426,7 @@ const linkPlexAccount = (username, password) => {
}); });
}; };
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", SEASONED_URL);
const options = { const options = {
@@ -449,7 +437,7 @@ 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`);
throw error; throw error;
}); });
}; };
@@ -539,7 +527,6 @@ export {
getMovieCredits, getMovieCredits,
getShowCredits, getShowCredits,
getPersonCredits, getPersonCredits,
getCredits,
getTmdbMovieListByName, getTmdbMovieListByName,
searchTmdb, searchTmdb,
getUserRequests, getUserRequests,

View File

@@ -1,21 +0,0 @@
import Vue from "vue";
import VueRouter from "vue-router";
import router from "./routes";
import store from "./store";
import Toast from "./plugins/Toast";
import App from "./App.vue";
Vue.use(VueRouter);
Vue.use(Toast);
store.dispatch("darkmodeModule/findAndSetDarkmodeSupported");
store.dispatch("user/initUserFromCookie");
new Vue({
el: "#app",
router,
store,
components: { App },
template: "<App />"
});

16
src/main.ts Normal file
View File

@@ -0,0 +1,16 @@
import { createApp } from "vue";
import router from "./routes";
import store from "./store";
import Toast from "./plugins/Toast";
const App = require("./App.vue").default;
store.dispatch("darkmodeModule/findAndSetDarkmodeSupported");
store.dispatch("user/initUserFromCookie");
const app = createApp(App);
app.use(router);
app.use(store);
app.use(Toast);
app.mount("#entry");

View File

@@ -1,21 +1,19 @@
<template> <template>
<transition name="slide"> <transition name="slide">
<div <div v-if="show" @click="clicked" class="toast" :class="type">
v-if="show"
@click="clicked"
class="toast"
:class="type">
<div class="toast--content"> <div class="toast--content">
<div class=" toast--icon"> <div class="toast--icon">
<i v-if="image"><img class="toast--icon-image" :src="image" /></i> <i v-if="image"><img class="toast--icon-image" :src="image" /></i>
</div> </div>
<div class="toast--text" v-if="description"> <div class="toast--text" v-if="description">
<span class="toast--text__title">{{title}}</span> <span class="toast--text__title">{{ title }}</span>
<br /><span class="toast--text__description" v-html="description"></span> <br /><span
class="toast--text__description"
v-html="description"
></span>
</div> </div>
<div class="toast--text" v-else> <div class="toast--text" v-else>
<span class="toast--text__title-large">{{title}}</span> <span class="toast--text__title-large">{{ title }}</span>
</div> </div>
<div class="toast--dismiss" @click="dismiss"> <div class="toast--dismiss" @click="dismiss">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
@@ -23,86 +21,75 @@
</div> </div>
</div> </div>
</transition> </transition>
</template> </template>
<script> <script setup lang="ts">
export default { import { ref, defineProps, onMounted } from "vue";
data() { import { useRouter } from "vue-router";
return { import type { Ref } from "vue";
type: this.$root.type || 'info',
title: this.$root.title || undefined, interface Props {
description: this.$root.description || undefined, type: string;
image: this.$root.image || undefined, title: string;
link: this.$root.link || undefined, description?: string;
timeout: this.$root.timeout || 2000, image?: string;
show: false link?: string;
timeout?: number;
} }
},
mounted() { const {
// Here we set show when mounted in-order to get the transition animation to be displayed correctly type = "info",
this.show = true; title,
description,
image,
link,
timeout = 2000
} = defineProps<Props>();
const router = useRouter();
const show: Ref<boolean> = ref(false);
onMounted(() => {
show.value = true;
setTimeout(() => { setTimeout(() => {
console.log('Your time is up 👋') console.log("Notification time is up 👋");
this.show = false; show.value = false;
}, this.timeout); }, timeout);
}, });
methods: {
clicked() {
if (this.link) {
let resolved = this.$root.router.resolve(this.link);
if (resolved && resolved.route.name !== '404') { function clicked() {
this.$root.router.push(this.link); show.value = false;
} else {
console.error('Found a link but it resolved to 404. Link:', this.link)
}
} else {
this.show = false;
}
},
dismiss() {
this.show = false;
},
move(e) {
console.log('moving', e)
let target = e.target;
console.log('target', target) if (link) {
router.push(link);
var div = document.getElementById('target'); }
div.style.position = 'absolute';
div.style.top = e.clientY + 'px';
div.style.left = e.clientX + 'px';
},
} }
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
// @import '@/scss/variables.scss'; // @import '@/scss/variables.scss';
.slide-enter-active {
.slide-enter-active {
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.slide-enter, .slide-leave-to { .slide-enter,
.slide-leave-to {
transform: translateY(100vh); transform: translateY(100vh);
opacity: 0; opacity: 0;
} }
.slide-leave-active { .slide-leave-active {
transition: all 2s ease; transition: all 2s ease;
} }
.toast--icon-image { .toast--icon-image {
height: 100%; height: 100%;
width: 100%; width: 100%;
max-height: 45px; max-height: 45px;
max-width: 45px; max-width: 45px;
} }
.toast { .toast {
position: fixed; position: fixed;
bottom: 0.5rem; bottom: 0.5rem;
cursor: pointer; cursor: pointer;
@@ -110,13 +97,12 @@ export default {
background-color: white; background-color: white;
border-radius: 3px; border-radius: 3px;
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.17), 0 2px 4px 0 rgba(0,0,0,0.08); box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.17), 0 2px 4px 0 rgba(0, 0, 0, 0.08);
padding: 0.5rem; padding: 0.5rem;
margin: 1rem 2rem 1rem 0.71rem; margin: 1rem 2rem 1rem 0.71rem;
// max-width: calc(100% - 3rem); // max-width: calc(100% - 3rem);
min-width: 320px; min-width: 320px;
// If small screen we have a min-width that is related to the screen size. // If small screen we have a min-width that is related to the screen size.
// else large screens we want a max-width that only uses the space in bottom right // else large screens we want a max-width that only uses the space in bottom right
@@ -126,7 +112,6 @@ export default {
&--content { &--content {
display: flex; display: flex;
align-items: center; align-items: center;
} }
&--icon { &--icon {
@@ -164,13 +149,12 @@ export default {
} }
} }
&.success { &.success {
border-left: 6px solid #38c172; border-left: 6px solid #38c172;
} }
&.info { &.info {
border-left: 6px solid #FFD300; border-left: 6px solid #ffd300;
} }
&.warning { &.warning {
@@ -184,6 +168,5 @@ export default {
&.simple { &.simple {
border-left: unset; border-left: unset;
} }
}
}
</style> </style>

View File

@@ -1,57 +0,0 @@
import Vue from 'vue'
import ToastComponent from './ToastComponent.vue'
const optionsDefaults = {
data: {
type: 'info',
show: true,
timeout: 3000,
onCreate(created = null) {
},
onEdit(editted = null) {
},
onRemove(removed = null) {
}
}
}
function toast(options, router) {
// merge the default options with the passed options.
const root = new Vue({
data: {
...optionsDefaults.data,
...options,
router
},
render: createElement => createElement(ToastComponent)
})
root.$mount(document.body.appendChild(document.createElement('div')))
}
export default {
install(vue, opts) {
console.log('installing toast plugin!')
console.log('plugin options', opts)
Vue.prototype.$notifications = {
info(options) {
toast({ type: 'info', ...options })
},
success(options) {
toast({ type: 'success', ...options })
},
warning(options) {
toast({ type: 'warning', ...options })
},
error(options) {
toast({ type: 'error', ...options })
},
simple(options) {
toast({ type: 'simple', ...options})
}
}
}
}

View File

@@ -0,0 +1,68 @@
import { createApp } from "vue";
import router from "../../routes";
import ToastComponent from "./ToastComponent.vue";
const optionsDefaults = {
data: {
type: "info",
show: true,
timeout: 3000,
onCreate(created = null) {},
onEdit(editted = null) {},
onRemove(removed = null) {}
}
};
function toast(options) {
// merge the default options with the passed options.
const toastComponent = createApp(ToastComponent, {
...optionsDefaults.data,
...options
});
toastComponent.use(router);
console.log("toastComponent:", toastComponent);
toastComponent.mount(
document.body.appendChild(document.createElement("div"))
);
}
export default {
install(app, options) {
console.log("installing toast plugin!");
console.log("plugin options", options);
function info(options) {
toast({ type: "info", ...options });
}
function success(options) {
toast({ type: "success", ...options });
}
function warning(options) {
toast({ type: "warning", ...options });
}
function error(options) {
toast({ type: "error", ...options });
}
function simple(options) {
toast({ type: "simple", ...options });
}
const notifications = { info, success, warning, error, simple };
app.config.globalProperties.$notifications = notifications;
app.provide("notifications", notifications);
}
};
declare module "@vue/runtime-core" {
interface ComponentCustomProperties {
$notifications: (info: object) => void;
}
}

View File

@@ -1,5 +1,11 @@
import Vue from "vue"; import { defineAsyncComponent } from "vue";
import VueRouter, { NavigationGuardNext, Route, RouteConfig } from "vue-router"; import {
createRouter,
createWebHistory,
RouteRecordRaw,
NavigationGuardNext,
RouteLocationNormalized
} from "vue-router";
import store from "./store"; import store from "./store";
declare global { declare global {
@@ -8,57 +14,61 @@ declare global {
} }
} }
Vue.use(VueRouter); let routes: Array<RouteRecordRaw> = [
let routes = [
{ {
name: "home", name: "home",
path: "/", path: "/",
component: resolve => resolve("./pages/Home.vue") component: () => import("./pages/Home.vue")
}, },
{ {
name: "activity", name: "activity",
path: "/activity", path: "/activity",
meta: { requiresAuth: true }, meta: { requiresAuth: true },
component: resolve => resolve("./pages/ActivityPage.vue") component: () => import("./pages/ActivityPage.vue")
}, },
{ {
name: "profile", name: "profile",
path: "/profile", path: "/profile",
meta: { requiresAuth: true }, meta: { requiresAuth: true },
component: resolve => resolve("./pages/ProfilePage.vue") component: () => import("./pages/ProfilePage.vue")
}, },
{ {
name: "list", name: "requests-list",
path: "/list/requests", path: "/list/requests",
component: resolve => resolve("./pages/RequestPage.vue") component: () => import("./pages/RequestPage.vue")
}, },
{ {
name: "list", name: "list",
path: "/list/:name", path: "/list/:name",
component: resolve => resolve("./pages/ListPage.vue") component: () => import("./pages/ListPage.vue")
}, },
{ {
name: "search", name: "search",
path: "/search", path: "/search",
component: resolve => resolve("./pages/SearchPage.vue") component: () => import("./pages/SearchPage.vue")
}, },
{ {
name: "register", name: "register",
path: "/register", path: "/register",
component: resolve => resolve("./pages/RegisterPage.vue") component: () => import("./pages/RegisterPage.vue")
}, },
{ {
name: "settings", name: "settings",
path: "/settings", path: "/settings",
meta: { requiresAuth: true }, meta: { requiresAuth: true },
component: resolve => resolve("./pages/SettingsPage.vue") component: () => import("./pages/SettingsPage.vue")
}, },
{ {
name: "signin", name: "signin",
path: "/signin", path: "/signin",
alias: "/login", alias: "/login",
component: resolve => resolve("./pages/SigninPage.vue") component: () => import("./pages/SigninPage.vue")
},
{
name: "torrents",
path: "/torrents",
meta: { requiresAuth: true },
component: () => import("./pages/TorrentsPage.vue")
}, },
// { // {
// name: 'user-requests', // name: 'user-requests',
@@ -70,21 +80,21 @@ let routes = [
{ {
name: "404", name: "404",
path: "/404", path: "/404",
component: resolve => resolve("./pages/404.vue") component: () => import("./pages/404.vue")
},
{
path: "*",
redirect: "/"
},
{
path: "/request",
redirect: "/"
} }
// {
// path: "*",
// redirect: "/"
// },
// {
// path: "/request",
// redirect: "/"
// }
]; ];
const router = new VueRouter({ const router = createRouter({
mode: "history", history: createWebHistory("/"),
base: "/", // base: "/",
routes, routes,
linkActiveClass: "is-active" linkActiveClass: "is-active"
}); });
@@ -93,28 +103,16 @@ const loggedIn = () => store.getters["user/loggedIn"];
const popupIsOpen = () => store.getters["popup/isOpen"]; const popupIsOpen = () => store.getters["popup/isOpen"];
const hamburgerIsOpen = () => store.getters["hamburger/isOpen"]; const hamburgerIsOpen = () => store.getters["hamburger/isOpen"];
window.preventPushState = false; router.beforeEach(
window.onpopstate = () => (window.preventPushState = true); (to: RouteLocationNormalized, from: RouteLocationNormalized, next: any) => {
router.beforeEach((to: Route, from: Route, next: NavigationGuardNext<Vue>) => {
store.dispatch("documentTitle/updateTitle", to.name); store.dispatch("documentTitle/updateTitle", to.name);
const { movie, show, person } = to.query; store.dispatch("popup/resetStateFromUrlQuery", to.query);
if (movie) store.dispatch("popup/open", { id: movie, type: "movie" });
else if (show) store.dispatch("popup/open", { id: show, type: "show" });
else if (person) store.dispatch("popup/open", { id: person, type: "person" });
else store.dispatch("popup/close");
// Every route change we close hamburger if open
if (hamburgerIsOpen()) store.dispatch("hamburger/close"); if (hamburgerIsOpen()) store.dispatch("hamburger/close");
// Toggle mobile nav // If pages has meta 'requiresAuth' and user not logged in
if (document.querySelector(".nav__hamburger--active")) { // send user to signin page.
document
.querySelector(".nav__hamburger")
.classList.remove("nav__hamburger--active");
document.querySelector(".nav__list").classList.remove("nav__list--active");
}
if (to.matched.some(record => record.meta.requiresAuth)) { if (to.matched.some(record => record.meta.requiresAuth)) {
if (!loggedIn) { if (!loggedIn) {
next({ path: "/signin" }); next({ path: "/signin" });
@@ -122,6 +120,7 @@ router.beforeEach((to: Route, from: Route, next: NavigationGuardNext<Vue>) => {
} }
next(); next();
}); }
);
export default router; export default router;

View File

@@ -1,5 +1,4 @@
import Vue from "vue"; import { createStore } from "vuex";
import Vuex from "vuex";
import darkmodeModule from "./modules/darkmodeModule"; import darkmodeModule from "./modules/darkmodeModule";
import documentTitle from "./modules/documentTitle"; import documentTitle from "./modules/documentTitle";
@@ -8,9 +7,7 @@ import user from "./modules/user";
import popup from "./modules/popup"; import popup from "./modules/popup";
import hamburger from "./modules/hamburger"; import hamburger from "./modules/hamburger";
Vue.use(Vuex); const store = createStore({
const store = new Vuex.Store({
modules: { modules: {
darkmodeModule, darkmodeModule,
documentTitle, documentTitle,

View File

@@ -1,8 +1,8 @@
export const sortableSize = (string: string) => { export const sortableSize = (string: string): number => {
const UNITS = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; const UNITS = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const [numStr, unit] = string.split(" "); const [numStr, unit] = string.split(" ");
if (UNITS.indexOf(unit) === -1) return string; if (UNITS.indexOf(unit) === -1) return null;
const exponent = UNITS.indexOf(unit) * 3; const exponent = UNITS.indexOf(unit) * 3;
return Number(numStr) * Math.pow(10, exponent); return Number(numStr) * Math.pow(10, exponent);
@@ -36,3 +36,65 @@ export const buildImageProxyUrl = (
return `${proxyHost}${proxySizeOptions}${assetUrl}`; return `${proxyHost}${proxySizeOptions}${assetUrl}`;
}; };
export function focusFirstFormInput(formElement: HTMLFormElement): void {
if (!formElement) return;
const firstInput = formElement?.getElementsByTagName("input")[0];
if (!firstInput) return;
firstInput.focus();
}
export function focusOnNextElement(elementEvent: KeyboardEvent): void {
const { target } = elementEvent;
console.log("target:", target);
if (!target) return;
const form = document.getElementsByTagName("form")[0];
console.log("form:", form);
if (!form) return;
const inputElements = form.getElementsByTagName("input");
console.log("inputElements:", inputElements);
const targetIndex = Array.from(inputElements).findIndex(
element => element === target
);
console.log("targetIndex:", targetIndex);
if (targetIndex < inputElements.length) {
inputElements[targetIndex + 1].focus();
}
}
export function 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`;
}
export function getValueFromUrlQuery(queryParameter: string): string | null {
const params = new URLSearchParams(window.location.search);
return params.get(queryParameter) || null;
}
export function setUrlQueryParameter(parameter: string, value: string): void {
const params = new URLSearchParams();
params.append(parameter, value);
const url = `${window.location.protocol}//${window.location.hostname}${
window.location.port ? `:${window.location.port}` : ""
}${window.location.pathname}${params.toString().length ? `?${params}` : ""}`;
window.history.pushState({}, "search", url);
}

View File

@@ -7,7 +7,7 @@ const TerserPlugin = require("terser-webpack-plugin");
const sourcePath = path.resolve(__dirname, "src"); const sourcePath = path.resolve(__dirname, "src");
const indexFile = path.join(sourcePath, "index.html"); const indexFile = path.join(sourcePath, "index.html");
const javascriptEntry = path.join(sourcePath, "main.js"); const javascriptEntry = path.join(sourcePath, "main.ts");
const publicPath = path.resolve(__dirname, "public"); const publicPath = path.resolve(__dirname, "public");
const isProd = process.env.NODE_ENV === "production"; const isProd = process.env.NODE_ENV === "production";
@@ -36,9 +36,12 @@ module.exports = {
use: ["vue-loader"] use: ["vue-loader"]
}, },
{ {
test: /\.tsx?$/, test: /\.ts$/,
loader: "ts-loader", loader: "ts-loader",
exclude: /node_modules/ exclude: /node_modules/,
options: {
appendTsSuffixTo: [/\.vue$/]
}
}, },
{ {
test: /\.scss$/, test: /\.scss$/,
@@ -69,7 +72,7 @@ module.exports = {
resolve: { resolve: {
extensions: [".js", ".ts", ".vue", ".json", ".scss"], extensions: [".js", ".ts", ".vue", ".json", ".scss"],
alias: { alias: {
vue$: "vue/dist/vue.common.js", vue: "@vue/runtime-dom",
"@": path.resolve(__dirname, "src"), "@": path.resolve(__dirname, "src"),
src: path.resolve(__dirname, "src"), src: path.resolve(__dirname, "src"),
assets: `${publicPath}/assets`, assets: `${publicPath}/assets`,