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>
<!-- 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 />
@@ -17,61 +20,54 @@
</div>
</template>
<script>
import NavigationHeader from "@/components/header/NavigationHeader";
import NavigationIcons from "@/components/header/NavigationIcons";
import Popup from "@/components/Popup";
import DarkmodeToggle from "@/components/ui/DarkmodeToggle";
<script setup lang="ts">
import { useRouter } from "vue-router";
import NavigationHeader from "@/components/header/NavigationHeader.vue";
import NavigationIcons from "@/components/header/NavigationIcons.vue";
import Popup from "@/components/Popup.vue";
import DarkmodeToggle from "@/components/ui/DarkmodeToggle.vue";
export default {
name: "app",
components: {
NavigationHeader,
NavigationIcons,
Popup,
DarkmodeToggle
}
};
const router = useRouter();
</script>
<style lang="scss">
@import "src/scss/main";
@import "src/scss/media-queries";
@import "src/scss/main";
@import "src/scss/media-queries";
#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 {
#app {
display: grid;
grid-column: 2 / 3;
grid-row: 2;
z-index: 5;
grid-template-rows: var(--header-size);
grid-template-columns: var(--header-size) 1fr;
@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>

View File

@@ -1,5 +1,5 @@
import config from "./config";
import { IList } from "./interfaces/IList";
import { IList, IMediaCredits, IPersonCredits } from "./interfaces/IList";
let { SEASONED_URL, ELASTIC_URL, ELASTIC_INDEX } = config;
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.
* @param {number} id
* @returns {object} Tmdb response
*/
const getMovieCredits = id => {
const getMovieCredits = (id: number): Promise<IMediaCredits> => {
const url = new URL("/api/v2/movie", SEASONED_URL);
url.pathname = `${url.pathname}/${id.toString()}/credits`;
@@ -133,7 +121,7 @@ const getMovieCredits = id => {
* @param {number} id
* @returns {object} Tmdb response
*/
const getShowCredits = id => {
const getShowCredits = (id: number): Promise<IMediaCredits> => {
const url = new URL("/api/v2/show", SEASONED_URL);
url.pathname = `${url.pathname}/${id.toString()}/credits`;
@@ -150,7 +138,7 @@ const getShowCredits = id => {
* @param {number} id
* @returns {object} Tmdb response
*/
const getPersonCredits = id => {
const getPersonCredits = (id: number): Promise<IPersonCredits> => {
const url = new URL("/api/v2/person", SEASONED_URL);
url.pathname = `${url.pathname}/${id.toString()}/credits`;
@@ -250,7 +238,7 @@ const searchTorrents = query => {
* @param {boolean} tmdb_id
* @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 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 options = {
@@ -449,7 +437,7 @@ const unlinkPlexAccount = (username, password) => {
return fetch(url.href, options)
.then(resp => resp.json())
.catch(error => {
console.error(`api error unlinking plex account: ${username}`);
console.error(`api error unlinking your plex account`);
throw error;
});
};
@@ -539,7 +527,6 @@ export {
getMovieCredits,
getShowCredits,
getPersonCredits,
getCredits,
getTmdbMovieListByName,
searchTmdb,
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,189 +1,172 @@
<template>
<transition name="slide">
<div
v-if="show"
@click="clicked"
class="toast"
:class="type">
<div class="toast--content">
<div class=" toast--icon">
<i v-if="image"><img class="toast--icon-image" :src="image" /></i>
</div>
<div class="toast--text" v-if="description">
<span class="toast--text__title">{{title}}</span>
<br /><span class="toast--text__description" v-html="description"></span>
</div>
<div class="toast--text" v-else>
<span class="toast--text__title-large">{{title}}</span>
</div>
<div class="toast--dismiss" @click="dismiss">
<i class="fas fa-times"></i>
</div>
</div>
<div v-if="show" @click="clicked" class="toast" :class="type">
<div class="toast--content">
<div class="toast--icon">
<i v-if="image"><img class="toast--icon-image" :src="image" /></i>
</div>
<div class="toast--text" v-if="description">
<span class="toast--text__title">{{ title }}</span>
<br /><span
class="toast--text__description"
v-html="description"
></span>
</div>
<div class="toast--text" v-else>
<span class="toast--text__title-large">{{ title }}</span>
</div>
<div class="toast--dismiss" @click="dismiss">
<i class="fas fa-times"></i>
</div>
</div>
</div>
</transition>
</template>
<script>
export default {
data() {
return {
type: this.$root.type || 'info',
title: this.$root.title || undefined,
description: this.$root.description || undefined,
image: this.$root.image || undefined,
link: this.$root.link || undefined,
timeout: this.$root.timeout || 2000,
show: false
}
},
mounted() {
// Here we set show when mounted in-order to get the transition animation to be displayed correctly
this.show = true;
<script setup lang="ts">
import { ref, defineProps, onMounted } from "vue";
import { useRouter } from "vue-router";
import type { Ref } from "vue";
interface Props {
type: string;
title: string;
description?: string;
image?: string;
link?: string;
timeout?: number;
}
const {
type = "info",
title,
description,
image,
link,
timeout = 2000
} = defineProps<Props>();
const router = useRouter();
const show: Ref<boolean> = ref(false);
onMounted(() => {
show.value = true;
setTimeout(() => {
console.log('Your time is up 👋')
this.show = false;
}, this.timeout);
},
methods: {
clicked() {
if (this.link) {
let resolved = this.$root.router.resolve(this.link);
console.log("Notification time is up 👋");
show.value = false;
}, timeout);
});
if (resolved && resolved.route.name !== '404') {
this.$root.router.push(this.link);
} 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;
function clicked() {
show.value = false;
console.log('target', target)
var div = document.getElementById('target');
div.style.position = 'absolute';
div.style.top = e.clientY + 'px';
div.style.left = e.clientX + 'px';
},
if (link) {
router.push(link);
}
}
};
</script>
<style lang="scss" scoped>
// @import '@/scss/variables.scss';
.slide-enter-active {
transition: all 0.3s ease;
}
.slide-enter, .slide-leave-to {
transform: translateY(100vh);
opacity: 0;
}
.slide-leave-active {
transition: all 2s ease;
}
.toast--icon-image {
height: 100%;
width: 100%;
max-height: 45px;
max-width: 45px;
}
.toast {
position: fixed;
bottom: 0.5rem;
cursor: pointer;
z-index: 100;
background-color: white;
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);
padding: 0.5rem;
margin: 1rem 2rem 1rem 0.71rem;
// max-width: calc(100% - 3rem);
min-width: 320px;
// 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
right: 0;
line-height: 22.5px;
&--content {
display: flex;
align-items: center;
// @import '@/scss/variables.scss';
.slide-enter-active {
transition: all 0.3s ease;
}
.slide-enter,
.slide-leave-to {
transform: translateY(100vh);
opacity: 0;
}
.slide-leave-active {
transition: all 2s ease;
}
&--icon {
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
.toast--icon-image {
height: 100%;
width: 100%;
max-height: 45px;
max-width: 45px;
}
&--text {
margin-left: 0.5rem;
// color: $bt-brown;
color: black;
word-wrap: break-word;
.toast {
position: fixed;
bottom: 0.5rem;
cursor: pointer;
z-index: 100;
&__title {
text-transform: capitalize;
font-weight: 400;
background-color: white;
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);
padding: 0.5rem;
margin: 1rem 2rem 1rem 0.71rem;
// max-width: calc(100% - 3rem);
min-width: 320px;
&-large {
font-size: 2rem;
// 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
right: 0;
line-height: 22.5px;
&--content {
display: flex;
align-items: center;
}
&--icon {
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
&--text {
margin-left: 0.5rem;
// color: $bt-brown;
color: black;
word-wrap: break-word;
&__title {
text-transform: capitalize;
font-weight: 400;
&-large {
font-size: 2rem;
}
}
&__description {
font-weight: 300;
}
}
&__description {
font-weight: 300;
&--dismiss {
align-self: flex-end;
img {
width: 2.5rem;
}
}
&.success {
border-left: 6px solid #38c172;
}
&.info {
border-left: 6px solid #ffd300;
}
&.warning {
border-left: 6px solid #f6993f;
}
&.error {
border-left: 6px solid #e3342f;
}
&.simple {
border-left: unset;
}
}
&--dismiss {
align-self: flex-end;
img {
width: 2.5rem;
}
}
&.success {
border-left: 6px solid #38c172;
}
&.info {
border-left: 6px solid #FFD300;
}
&.warning {
border-left: 6px solid #f6993f;
}
&.error {
border-left: 6px solid #e3342f;
}
&.simple {
border-left: unset;
}
}
</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 VueRouter, { NavigationGuardNext, Route, RouteConfig } from "vue-router";
import { defineAsyncComponent } from "vue";
import {
createRouter,
createWebHistory,
RouteRecordRaw,
NavigationGuardNext,
RouteLocationNormalized
} from "vue-router";
import store from "./store";
declare global {
@@ -8,57 +14,61 @@ declare global {
}
}
Vue.use(VueRouter);
let routes = [
let routes: Array<RouteRecordRaw> = [
{
name: "home",
path: "/",
component: resolve => resolve("./pages/Home.vue")
component: () => import("./pages/Home.vue")
},
{
name: "activity",
path: "/activity",
meta: { requiresAuth: true },
component: resolve => resolve("./pages/ActivityPage.vue")
component: () => import("./pages/ActivityPage.vue")
},
{
name: "profile",
path: "/profile",
meta: { requiresAuth: true },
component: resolve => resolve("./pages/ProfilePage.vue")
component: () => import("./pages/ProfilePage.vue")
},
{
name: "list",
name: "requests-list",
path: "/list/requests",
component: resolve => resolve("./pages/RequestPage.vue")
component: () => import("./pages/RequestPage.vue")
},
{
name: "list",
path: "/list/:name",
component: resolve => resolve("./pages/ListPage.vue")
component: () => import("./pages/ListPage.vue")
},
{
name: "search",
path: "/search",
component: resolve => resolve("./pages/SearchPage.vue")
component: () => import("./pages/SearchPage.vue")
},
{
name: "register",
path: "/register",
component: resolve => resolve("./pages/RegisterPage.vue")
component: () => import("./pages/RegisterPage.vue")
},
{
name: "settings",
path: "/settings",
meta: { requiresAuth: true },
component: resolve => resolve("./pages/SettingsPage.vue")
component: () => import("./pages/SettingsPage.vue")
},
{
name: "signin",
path: "/signin",
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',
@@ -70,21 +80,21 @@ let routes = [
{
name: "404",
path: "/404",
component: resolve => resolve("./pages/404.vue")
},
{
path: "*",
redirect: "/"
},
{
path: "/request",
redirect: "/"
component: () => import("./pages/404.vue")
}
// {
// path: "*",
// redirect: "/"
// },
// {
// path: "/request",
// redirect: "/"
// }
];
const router = new VueRouter({
mode: "history",
base: "/",
const router = createRouter({
history: createWebHistory("/"),
// base: "/",
routes,
linkActiveClass: "is-active"
});
@@ -93,35 +103,24 @@ const loggedIn = () => store.getters["user/loggedIn"];
const popupIsOpen = () => store.getters["popup/isOpen"];
const hamburgerIsOpen = () => store.getters["hamburger/isOpen"];
window.preventPushState = false;
window.onpopstate = () => (window.preventPushState = true);
router.beforeEach(
(to: RouteLocationNormalized, from: RouteLocationNormalized, next: any) => {
store.dispatch("documentTitle/updateTitle", to.name);
store.dispatch("popup/resetStateFromUrlQuery", to.query);
router.beforeEach((to: Route, from: Route, next: NavigationGuardNext<Vue>) => {
store.dispatch("documentTitle/updateTitle", to.name);
const { movie, show, person } = to.query;
// Every route change we close hamburger if open
if (hamburgerIsOpen()) store.dispatch("hamburger/close");
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");
if (hamburgerIsOpen()) store.dispatch("hamburger/close");
// Toggle mobile nav
if (document.querySelector(".nav__hamburger--active")) {
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 (!loggedIn) {
next({ path: "/signin" });
// If pages has meta 'requiresAuth' and user not logged in
// send user to signin page.
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!loggedIn) {
next({ path: "/signin" });
}
}
}
next();
});
next();
}
);
export default router;

View File

@@ -1,5 +1,4 @@
import Vue from "vue";
import Vuex from "vuex";
import { createStore } from "vuex";
import darkmodeModule from "./modules/darkmodeModule";
import documentTitle from "./modules/documentTitle";
@@ -8,9 +7,7 @@ import user from "./modules/user";
import popup from "./modules/popup";
import hamburger from "./modules/hamburger";
Vue.use(Vuex);
const store = new Vuex.Store({
const store = createStore({
modules: {
darkmodeModule,
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 [numStr, unit] = string.split(" ");
if (UNITS.indexOf(unit) === -1) return string;
if (UNITS.indexOf(unit) === -1) return null;
const exponent = UNITS.indexOf(unit) * 3;
return Number(numStr) * Math.pow(10, exponent);
@@ -36,3 +36,65 @@ export const buildImageProxyUrl = (
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 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 isProd = process.env.NODE_ENV === "production";
@@ -36,9 +36,12 @@ module.exports = {
use: ["vue-loader"]
},
{
test: /\.tsx?$/,
test: /\.ts$/,
loader: "ts-loader",
exclude: /node_modules/
exclude: /node_modules/,
options: {
appendTsSuffixTo: [/\.vue$/]
}
},
{
test: /\.scss$/,
@@ -69,7 +72,7 @@ module.exports = {
resolve: {
extensions: [".js", ".ts", ".vue", ".json", ".scss"],
alias: {
vue$: "vue/dist/vue.common.js",
vue: "@vue/runtime-dom",
"@": path.resolve(__dirname, "src"),
src: path.resolve(__dirname, "src"),
assets: `${publicPath}/assets`,