diff --git a/src/App.vue b/src/App.vue index 997b249..9630126 100644 --- a/src/App.vue +++ b/src/App.vue @@ -8,7 +8,10 @@ - + @@ -17,61 +20,54 @@ - diff --git a/src/api.ts b/src/api.ts index 131809d..91582da 100644 --- a/src/api.ts +++ b/src/api.ts @@ -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 => { 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 => { 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 => { 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, diff --git a/src/main.js b/src/main.js deleted file mode 100644 index 148cd6e..0000000 --- a/src/main.js +++ /dev/null @@ -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: "" -}); diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..97f9376 --- /dev/null +++ b/src/main.ts @@ -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"); diff --git a/src/plugins/Toast/ToastComponent.vue b/src/plugins/Toast/ToastComponent.vue index 252e069..f0fe8e2 100644 --- a/src/plugins/Toast/ToastComponent.vue +++ b/src/plugins/Toast/ToastComponent.vue @@ -1,189 +1,172 @@ - \ No newline at end of file + diff --git a/src/plugins/Toast/index.js b/src/plugins/Toast/index.js deleted file mode 100644 index 6bf8f19..0000000 --- a/src/plugins/Toast/index.js +++ /dev/null @@ -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}) - } - } - } -} \ No newline at end of file diff --git a/src/plugins/Toast/index.ts b/src/plugins/Toast/index.ts new file mode 100644 index 0000000..cf5775f --- /dev/null +++ b/src/plugins/Toast/index.ts @@ -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; + } +} diff --git a/src/routes.ts b/src/routes.ts index 4004eaf..45afce8 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -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 = [ { 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) => { - 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; diff --git a/src/store.ts b/src/store.ts index 426699d..b319ecf 100644 --- a/src/store.ts +++ b/src/store.ts @@ -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, diff --git a/src/utils.ts b/src/utils.ts index cbfb108..204987d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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); +} diff --git a/webpack.config.js b/webpack.config.js index dda7f31..8564ba7 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -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`,