mirror of
https://github.com/KevinMidboe/seasoned.git
synced 2026-03-11 20:05:39 +00:00
Resolved ALL eslint issues for project
This commit is contained in:
@@ -3,8 +3,8 @@
|
||||
<ol class="persons">
|
||||
<CastListItem
|
||||
v-for="credit in cast"
|
||||
:creditItem="credit"
|
||||
:key="credit.id"
|
||||
:credit-item="credit"
|
||||
/>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<li class="card">
|
||||
<a @click="openCastItem">
|
||||
<img :src="pictureUrl" />
|
||||
<a @click="openCastItem" @keydown.enter="openCastItem">
|
||||
<img :src="pictureUrl" alt="Movie or person poster image" />
|
||||
<p class="name">{{ creditItem.name || creditItem.title }}</p>
|
||||
<p class="meta">{{ creditItem.character || creditItem.year }}</p>
|
||||
</a>
|
||||
@@ -25,7 +25,8 @@
|
||||
|
||||
if ("profile_path" in props.creditItem && props.creditItem.profile_path) {
|
||||
return baseUrl + props.creditItem.profile_path;
|
||||
} else if ("poster" in props.creditItem && props.creditItem.poster) {
|
||||
}
|
||||
if ("poster" in props.creditItem && props.creditItem.poster) {
|
||||
return baseUrl + props.creditItem.poster;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, defineProps, onMounted, watch } from "vue";
|
||||
import { ref, defineProps, onMounted, watch } from "vue";
|
||||
import {
|
||||
Chart,
|
||||
LineElement,
|
||||
@@ -19,6 +19,11 @@
|
||||
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,
|
||||
@@ -31,23 +36,6 @@
|
||||
Title,
|
||||
Tooltip
|
||||
);
|
||||
import {} from "chart.js";
|
||||
import type { Ref } from "vue";
|
||||
|
||||
enum GraphValueTypes {
|
||||
Number = "number",
|
||||
Time = "time"
|
||||
}
|
||||
|
||||
interface IGraphDataset {
|
||||
name: string;
|
||||
data: Array<number>;
|
||||
}
|
||||
|
||||
interface IGraphData {
|
||||
labels: Array<string>;
|
||||
series: Array<IGraphDataset>;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
name?: string;
|
||||
@@ -69,8 +57,10 @@
|
||||
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 = [
|
||||
{
|
||||
@@ -92,9 +82,9 @@
|
||||
tension: 0.4
|
||||
}
|
||||
];
|
||||
const gridColor = getComputedStyle(document.documentElement).getPropertyValue(
|
||||
"--text-color-5"
|
||||
);
|
||||
// const gridColor = getComputedStyle(document.documentElement).getPropertyValue(
|
||||
// "--text-color-5"
|
||||
// );
|
||||
|
||||
function hydrateGraphLineOptions(dataset: IGraphDataset, index: number) {
|
||||
return {
|
||||
@@ -105,6 +95,7 @@
|
||||
}
|
||||
|
||||
function removeEmptyDataset(dataset: IGraphDataset) {
|
||||
/* eslint-disable-next-line no-unneeded-ternary */
|
||||
return dataset.data.every(point => point === 0) ? false : true;
|
||||
}
|
||||
|
||||
@@ -143,7 +134,7 @@
|
||||
yAxes: {
|
||||
stacked: props.stacked,
|
||||
ticks: {
|
||||
callback: (value, index, values) => {
|
||||
callback: value => {
|
||||
if (props.graphValueType === GraphValueTypes.Time) {
|
||||
return convertSecondsToHumanReadable(value);
|
||||
}
|
||||
@@ -174,40 +165,6 @@
|
||||
options: graphOptions
|
||||
});
|
||||
}
|
||||
|
||||
function convertSecondsToHumanReadable(value, values = null) {
|
||||
const highestValue = values ? values[0] : value;
|
||||
|
||||
// minutes
|
||||
if (highestValue < 3600) {
|
||||
const minutes = Math.floor(value / 60);
|
||||
|
||||
value = `${minutes} m`;
|
||||
}
|
||||
// hours and minutes
|
||||
else if (highestValue > 3600 && highestValue < 86400) {
|
||||
const hours = Math.floor(value / 3600);
|
||||
const minutes = Math.floor((value % 3600) / 60);
|
||||
|
||||
value = hours != 0 ? `${hours} h ${minutes} m` : `${minutes} m`;
|
||||
}
|
||||
// days and hours
|
||||
else if (highestValue > 86400 && highestValue < 31557600) {
|
||||
const days = Math.floor(value / 86400);
|
||||
const hours = Math.floor((value % 86400) / 3600);
|
||||
|
||||
value = days != 0 ? `${days} d ${hours} h` : `${hours} h`;
|
||||
}
|
||||
// years and days
|
||||
else if (highestValue > 31557600) {
|
||||
const years = Math.floor(value / 31557600);
|
||||
const days = Math.floor((value % 31557600) / 86400);
|
||||
|
||||
value = years != 0 ? `${years} y ${days} d` : `${days} d`;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<header ref="headerElement" :class="{ expanded, noselect: true }">
|
||||
<img :src="bannerImage" ref="imageElement" />
|
||||
<img ref="imageElement" :src="bannerImage" alt="Page banner image" />
|
||||
<div class="container">
|
||||
<h1 class="title">Request movies or tv shows</h1>
|
||||
<strong class="subtitle"
|
||||
@@ -8,7 +8,13 @@
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="expand-icon" @click="expand" @mouseover="upgradeImage">
|
||||
<div
|
||||
class="expand-icon"
|
||||
@click="expand"
|
||||
@keydown.enter="expand"
|
||||
@mouseover="upgradeImage"
|
||||
@focus="focus"
|
||||
>
|
||||
<IconExpand v-if="!expanded" />
|
||||
<IconShrink v-else />
|
||||
</div>
|
||||
@@ -16,7 +22,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { ref } from "vue";
|
||||
import IconExpand from "@/icons/IconExpand.vue";
|
||||
import IconShrink from "@/icons/IconShrink.vue";
|
||||
import type { Ref } from "vue";
|
||||
@@ -35,9 +41,7 @@
|
||||
const headerElement: Ref<HTMLElement> = ref(null);
|
||||
const imageElement: Ref<HTMLImageElement> = ref(null);
|
||||
const defaultHeaderHeight: Ref<string> = ref();
|
||||
const disableProxy = true;
|
||||
|
||||
bannerImage.value = randomImage();
|
||||
// const disableProxy = true;
|
||||
|
||||
function expand() {
|
||||
expanded.value = !expanded.value;
|
||||
@@ -53,11 +57,17 @@
|
||||
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)];
|
||||
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;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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>
|
||||
<person v-if="type === 'person'" :id="id" type="person" />
|
||||
<movie v-else :id="id" :type="type"></movie>
|
||||
@@ -14,8 +14,8 @@
|
||||
import { useStore } from "vuex";
|
||||
import Movie from "@/components/popup/Movie.vue";
|
||||
import Person from "@/components/popup/Person.vue";
|
||||
import { MediaTypes } from "../interfaces/IList";
|
||||
import type { Ref } from "vue";
|
||||
import { MediaTypes } from "../interfaces/IList";
|
||||
|
||||
interface URLQueryParameters {
|
||||
id: number;
|
||||
@@ -34,14 +34,16 @@
|
||||
id.value = state.popup.id;
|
||||
type.value = state.popup.type;
|
||||
|
||||
isOpen.value
|
||||
? document.getElementsByTagName("body")[0].classList.add("no-scroll")
|
||||
: document.getElementsByTagName("body")[0].classList.remove("no-scroll");
|
||||
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;
|
||||
let _id: number;
|
||||
let _type: MediaTypes;
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.forEach((value, key) => {
|
||||
@@ -55,16 +57,16 @@
|
||||
return;
|
||||
}
|
||||
|
||||
id = Number(params.get(key));
|
||||
type = MediaTypes[key];
|
||||
_id = Number(params.get(key));
|
||||
_type = MediaTypes[key];
|
||||
});
|
||||
|
||||
return { id, type };
|
||||
return { id: _id, type: _type };
|
||||
}
|
||||
|
||||
function open(id: Number, type: string) {
|
||||
if (!id || !type) return;
|
||||
store.dispatch("popup/open", { id, type });
|
||||
function open(_id: number, _type: string) {
|
||||
if (!_id || !_type) return;
|
||||
store.dispatch("popup/open", { id: _id, type: _type });
|
||||
}
|
||||
|
||||
function close() {
|
||||
@@ -79,8 +81,8 @@
|
||||
window.addEventListener("keyup", checkEventForEscapeKey);
|
||||
|
||||
onMounted(() => {
|
||||
const { id, type } = getFromURLQuery();
|
||||
open(id, type);
|
||||
const query = getFromURLQuery();
|
||||
open(query?.id, query?.type);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<results-list-item
|
||||
v-for="(result, index) in results"
|
||||
:key="generateResultKey(index, `${result.type}-${result.id}`)"
|
||||
:listItem="result"
|
||||
:list-item="result"
|
||||
/>
|
||||
</ul>
|
||||
|
||||
@@ -23,11 +23,11 @@
|
||||
|
||||
interface Props {
|
||||
results: Array<ListResults>;
|
||||
shortList?: Boolean;
|
||||
loading?: Boolean;
|
||||
shortList?: boolean;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
defineProps<Props>();
|
||||
|
||||
function generateResultKey(index: string | number | symbol, value: string) {
|
||||
return `${String(index)}-${value}`;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<template>
|
||||
<li class="movie-item" ref="list-item">
|
||||
<li ref="list-item" class="movie-item">
|
||||
<figure
|
||||
ref="posterElement"
|
||||
class="movie-item__poster"
|
||||
@click="openMoviePopup"
|
||||
@keydown.enter="openMoviePopup"
|
||||
>
|
||||
<img
|
||||
class="movie-item__img"
|
||||
@@ -36,9 +37,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, defineProps, onMounted } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { buildImageProxyUrl } from "../utils";
|
||||
import type { Ref } from "vue";
|
||||
import type { IMovie, IShow, IPerson, IRequest } from "../interfaces/IList";
|
||||
import type { IMovie, IShow, IPerson } from "../interfaces/IList";
|
||||
|
||||
interface Props {
|
||||
listItem: IMovie | IShow | IPerson;
|
||||
@@ -53,19 +53,31 @@
|
||||
const posterElement: Ref<HTMLElement> = ref(null);
|
||||
const observed: Ref<boolean> = ref(false);
|
||||
|
||||
poster.value = props.listItem?.poster
|
||||
? IMAGE_BASE_URL + props.listItem?.poster
|
||||
: IMAGE_FALLBACK;
|
||||
if (props.listItem?.poster) {
|
||||
poster.value = IMAGE_BASE_URL + props.listItem.poster;
|
||||
} else {
|
||||
poster.value = IMAGE_FALLBACK;
|
||||
}
|
||||
|
||||
onMounted(observePosterAndSetImageSource);
|
||||
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, imgObserver) => {
|
||||
const imageObserver = new IntersectionObserver(entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting && observed.value == false) {
|
||||
if (entry.isIntersecting && observed.value === false) {
|
||||
const lazyImage = entry.target as HTMLImageElement;
|
||||
lazyImage.src = lazyImage.dataset.src;
|
||||
posterElement.value.classList.add("is-loaded");
|
||||
@@ -77,30 +89,20 @@
|
||||
imageObserver.observe(imageElement);
|
||||
}
|
||||
|
||||
onMounted(observePosterAndSetImageSource);
|
||||
|
||||
function openMoviePopup() {
|
||||
store.dispatch("popup/open", { ...props.listItem });
|
||||
}
|
||||
|
||||
const posterAltText = computed(() => {
|
||||
const type = props.listItem.type || "";
|
||||
let title: string = "";
|
||||
|
||||
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}`;
|
||||
});
|
||||
|
||||
const imageSize = computed(() => {
|
||||
if (!posterElement.value) return;
|
||||
const { height, width } = posterElement.value.getBoundingClientRect();
|
||||
return {
|
||||
height: Math.ceil(height),
|
||||
width: Math.ceil(width)
|
||||
};
|
||||
});
|
||||
// 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: {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
v-if="!loadedPages.includes(1) && loading == false"
|
||||
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
|
||||
>
|
||||
</div>
|
||||
@@ -16,10 +16,10 @@
|
||||
|
||||
<div ref="loadMoreButton" class="button-container">
|
||||
<seasoned-button
|
||||
class="load-button"
|
||||
v-if="!loading && !shortList && page != totalPages && results.length"
|
||||
class="load-button"
|
||||
:full-width="true"
|
||||
@click="loadMore"
|
||||
:fullWidth="true"
|
||||
>load more</seasoned-button
|
||||
>
|
||||
</div>
|
||||
@@ -28,12 +28,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, ref, computed, onMounted } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import ResultsList from "@/components/ResultsList.vue";
|
||||
import SeasonedButton from "@/components/ui/SeasonedButton.vue";
|
||||
import Loader from "@/components/ui/Loader.vue";
|
||||
import { getTmdbMovieListByName } from "../api";
|
||||
import type { Ref } from "vue";
|
||||
import type { IList, ListResults } from "../interfaces/IList";
|
||||
import type ISection from "../interfaces/ISection";
|
||||
@@ -44,7 +42,6 @@
|
||||
shortList?: boolean;
|
||||
}
|
||||
|
||||
const store = useStore();
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const results: Ref<ListResults> = ref([]);
|
||||
@@ -54,12 +51,23 @@
|
||||
const totalPages: Ref<number> = ref(0);
|
||||
const loading: Ref<boolean> = ref(true);
|
||||
const autoLoad: Ref<boolean> = ref(false);
|
||||
const observer: Ref<any> = ref(null);
|
||||
const observer: Ref<IntersectionObserver> = ref(null);
|
||||
const resultSection = ref(null);
|
||||
const loadMoreButton = ref(null);
|
||||
|
||||
page.value = getPageFromUrl() || page.value;
|
||||
if (results.value?.length === 0) getListResults();
|
||||
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];
|
||||
@@ -69,25 +77,30 @@
|
||||
return [pageCount, resultCount];
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (!props?.shortList) setupAutoloadObserver();
|
||||
});
|
||||
|
||||
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 getPageFromUrl() {
|
||||
const page = new URLSearchParams(window.location.search).get("page");
|
||||
if (!page) return null;
|
||||
const _page = new URLSearchParams(window.location.search).get("page");
|
||||
if (!_page) return null;
|
||||
|
||||
return Number(page);
|
||||
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) {
|
||||
@@ -105,7 +118,7 @@
|
||||
totalResults.value = listResponse.total_results;
|
||||
})
|
||||
.then(updateQueryParams)
|
||||
.finally(() => (loading.value = false));
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
|
||||
function loadMore() {
|
||||
@@ -114,9 +127,9 @@
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
let maxPage = [...loadedPages.value].slice(-1)[0];
|
||||
const maxPage = [...loadedPages.value].slice(-1)[0];
|
||||
|
||||
if (maxPage == NaN) return;
|
||||
if (Number.isNaN(maxPage)) return;
|
||||
page.value = maxPage + 1;
|
||||
getListResults();
|
||||
}
|
||||
@@ -130,25 +143,6 @@
|
||||
getListResults(true);
|
||||
}
|
||||
|
||||
function updateQueryParams() {
|
||||
let 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 handleButtonIntersection(entries) {
|
||||
entries.map(entry =>
|
||||
entry.isIntersecting && autoLoad.value ? loadMore() : null
|
||||
@@ -165,14 +159,12 @@
|
||||
observer.value.observe(loadMoreButton.value);
|
||||
}
|
||||
|
||||
// created() {
|
||||
// if (!this.shortList) {
|
||||
// store.dispatch(
|
||||
// "documentTitle/updateTitle",
|
||||
// `${this.$router.history.current.name} ${this.title}`
|
||||
// );
|
||||
// }
|
||||
// },
|
||||
page.value = getPageFromUrl() || page.value;
|
||||
if (results.value?.length === 0) getListResults();
|
||||
onMounted(() => {
|
||||
if (!props?.shortList) setupAutoloadObserver();
|
||||
});
|
||||
|
||||
// beforeDestroy() {
|
||||
// this.observer = undefined;
|
||||
// }
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<template>
|
||||
<transition name="shut">
|
||||
<ul class="dropdown">
|
||||
<!-- eslint-disable-next-line vuejs-accessibility/click-events-have-key-events -->
|
||||
<li
|
||||
v-for="result in searchResults"
|
||||
:key="`${result.index}-${result.title}-${result.type}`"
|
||||
v-for="(result, _index) in searchResults"
|
||||
:key="`${_index}-${result.title}-${result.type}`"
|
||||
:class="`result di-${_index} ${_index === index ? 'active' : ''}`"
|
||||
@click="openPopup(result)"
|
||||
:class="`result di-${result.index} ${
|
||||
result.index === index ? 'active' : ''
|
||||
}`"
|
||||
>
|
||||
<IconMovie v-if="result.type == 'movie'" class="type-icon" />
|
||||
<IconShow v-if="result.type == 'show'" class="type-icon" />
|
||||
@@ -29,36 +28,38 @@
|
||||
import { useStore } from "vuex";
|
||||
import IconMovie from "@/icons/IconMovie.vue";
|
||||
import IconShow from "@/icons/IconShow.vue";
|
||||
import IconPerson from "@/icons/IconPerson.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";
|
||||
|
||||
interface Props {
|
||||
query?: string;
|
||||
index?: Number;
|
||||
results?: Array<any>;
|
||||
index?: number;
|
||||
results?: Array<IAutocompleteResult>;
|
||||
}
|
||||
|
||||
interface Emit {
|
||||
(e: "update:results", value: Array<any>);
|
||||
(e: "update:results", value: Array<IAutocompleteResult>);
|
||||
}
|
||||
|
||||
const numberOfResults: number = 10;
|
||||
const numberOfResults = 10;
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emit>();
|
||||
const store = useStore();
|
||||
|
||||
const searchResults: Ref<Array<any>> = ref([]);
|
||||
const searchResults: Ref<Array<IAutocompleteResult>> = ref([]);
|
||||
const keyboardNavigationIndex: Ref<number> = ref(0);
|
||||
|
||||
// on load functions
|
||||
fetchAutocompleteResults();
|
||||
// end on load functions
|
||||
|
||||
watch(
|
||||
() => props.query,
|
||||
newQuery => {
|
||||
if (newQuery?.length > 0) fetchAutocompleteResults();
|
||||
if (newQuery?.length > 0)
|
||||
fetchAutocompleteResults(); /* eslint-disable-line no-use-before-define */
|
||||
}
|
||||
);
|
||||
|
||||
@@ -68,6 +69,53 @@
|
||||
store.dispatch("popup/open", { ...result });
|
||||
}
|
||||
|
||||
function removeDuplicates(_searchResults) {
|
||||
const filteredResults = [];
|
||||
_searchResults.forEach(result => {
|
||||
if (result === undefined) return;
|
||||
const numberOfDuplicates = filteredResults.filter(
|
||||
filterItem => filterItem.id === result.id
|
||||
);
|
||||
if (numberOfDuplicates.length >= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
filteredResults.push(result);
|
||||
});
|
||||
|
||||
return filteredResults;
|
||||
}
|
||||
|
||||
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 = [];
|
||||
@@ -80,44 +128,9 @@
|
||||
});
|
||||
}
|
||||
|
||||
function parseElasticResponse(elasticResponse: any) {
|
||||
const data = elasticResponse.hits.hits;
|
||||
|
||||
let results = data.map(item => {
|
||||
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") {
|
||||
return {
|
||||
title: item._source.original_name || item._source.original_title,
|
||||
id: item._source.id,
|
||||
adult: item._source.adult,
|
||||
type: index
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return removeDuplicates(results).map((el, index) => {
|
||||
return { ...el, index };
|
||||
});
|
||||
}
|
||||
|
||||
function removeDuplicates(searchResults) {
|
||||
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);
|
||||
});
|
||||
|
||||
return filteredResults;
|
||||
}
|
||||
// on load functions
|
||||
fetchAutocompleteResults();
|
||||
// end on load functions
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<template>
|
||||
<nav>
|
||||
<!-- eslint-disable-next-line vuejs-accessibility/anchor-has-content -->
|
||||
<a v-if="isHome" class="nav__logo" href="/">
|
||||
<TmdbLogo class="logo" />
|
||||
</a>
|
||||
|
||||
<router-link v-else class="nav__logo" to="/" exact>
|
||||
<TmdbLogo class="logo" />
|
||||
</router-link>
|
||||
@@ -13,7 +15,7 @@
|
||||
<NavigationIcon class="desktop-only" :route="profileRoute" />
|
||||
|
||||
<!-- <div class="navigation-icons-grid mobile-only" :class="{ open: isOpen }"> -->
|
||||
<div class="navigation-icons-grid mobile-only" v-if="isOpen">
|
||||
<div v-if="isOpen" class="navigation-icons-grid mobile-only">
|
||||
<NavigationIcons>
|
||||
<NavigationIcon :route="profileRoute" />
|
||||
</NavigationIcons>
|
||||
@@ -22,8 +24,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, defineProps, PropType } from "vue";
|
||||
import type { App } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useRoute } from "vue-router";
|
||||
import SearchInput from "@/components/header/SearchInput.vue";
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<router-link
|
||||
:to="{ path: route?.route }"
|
||||
:key="route?.title"
|
||||
v-if="route?.requiresAuth == undefined || (route?.requiresAuth && loggedIn)"
|
||||
:key="route?.title"
|
||||
:to="{ path: route?.route }"
|
||||
>
|
||||
<li class="navigation-link" :class="{ active: route.route == active }">
|
||||
<component class="navigation-icon" :is="route.icon"></component>
|
||||
<component :is="route.icon" class="navigation-icon"></component>
|
||||
<span>{{ route.title }}</span>
|
||||
</li>
|
||||
</router-link>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<ul class="navigation-icons">
|
||||
<NavigationIcon
|
||||
v-for="route in routes"
|
||||
:key="route.route"
|
||||
:route="route"
|
||||
v-for="_route in routes"
|
||||
:key="_route.route"
|
||||
:route="_route"
|
||||
:active="activeRoute"
|
||||
/>
|
||||
<slot></slot>
|
||||
@@ -66,7 +66,11 @@
|
||||
}
|
||||
];
|
||||
|
||||
watch(route, () => (activeRoute.value = window?.location?.pathname));
|
||||
function setActiveRoute(_route: string) {
|
||||
activeRoute.value = _route;
|
||||
}
|
||||
|
||||
watch(route, () => setActiveRoute(window?.location?.pathname || ""));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
<div class="search" :class="{ active: inputIsActive }">
|
||||
<IconSearch class="search-icon" tabindex="-1" />
|
||||
|
||||
<!-- eslint-disable-next-line vuejs-accessibility/form-control-has-label -->
|
||||
<input
|
||||
ref="inputElement"
|
||||
v-model="query"
|
||||
type="text"
|
||||
placeholder="Search for movie or show"
|
||||
aria-label="Search input for finding a movie or show"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
tabindex="0"
|
||||
v-model="query"
|
||||
@input="handleInput"
|
||||
@click="focus"
|
||||
@keydown.escape="handleEscape"
|
||||
@@ -23,20 +24,20 @@
|
||||
/>
|
||||
|
||||
<IconClose
|
||||
v-if="query && query.length"
|
||||
tabindex="0"
|
||||
aria-label="button"
|
||||
v-if="query && query.length"
|
||||
class="close-icon"
|
||||
@click="clearInput"
|
||||
@keydown.enter.stop="clearInput"
|
||||
class="close-icon"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AutocompleteDropdown
|
||||
v-if="showAutocompleteResults"
|
||||
v-model:results="dropdownResults"
|
||||
:query="query"
|
||||
:index="dropdownIndex"
|
||||
v-model:results="dropdownResults"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -44,14 +45,12 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useRoute } from "vue-router";
|
||||
import SeasonedButton from "@/components/ui/SeasonedButton.vue";
|
||||
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 config from "../../config";
|
||||
import type { Ref } from "vue";
|
||||
import config from "../../config";
|
||||
import type { MediaTypes } from "../../interfaces/IList";
|
||||
|
||||
interface ISearchResult {
|
||||
@@ -70,8 +69,7 @@
|
||||
const dropdownIndex: Ref<number> = ref(-1);
|
||||
const dropdownResults: Ref<ISearchResult[]> = ref([]);
|
||||
const inputIsActive: Ref<boolean> = ref(false);
|
||||
const showAutocomplete: Ref<boolean> = ref(false);
|
||||
const inputElement: Ref<any> = ref(null);
|
||||
const inputElement: Ref<HTMLInputElement> = ref(null);
|
||||
|
||||
const isOpen = computed(() => store.getters["popup/isOpen"]);
|
||||
const showAutocompleteResults = computed(() => {
|
||||
@@ -95,12 +93,12 @@
|
||||
|
||||
function navigateDown() {
|
||||
if (dropdownIndex.value < dropdownResults.value.length - 1) {
|
||||
dropdownIndex.value++;
|
||||
dropdownIndex.value += 1;
|
||||
}
|
||||
}
|
||||
|
||||
function navigateUp() {
|
||||
if (dropdownIndex.value > -1) dropdownIndex.value--;
|
||||
if (dropdownIndex.value > -1) dropdownIndex.value -= 1;
|
||||
|
||||
const textLength = inputElement.value.value.length;
|
||||
|
||||
@@ -122,12 +120,31 @@
|
||||
});
|
||||
}
|
||||
|
||||
function handleInput(e) {
|
||||
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 (!query.value || query.value.length === 0) return;
|
||||
|
||||
if (dropdownIndex.value >= 0) {
|
||||
const resultItem = dropdownResults.value[dropdownIndex.value];
|
||||
@@ -143,25 +160,6 @@
|
||||
reset();
|
||||
}
|
||||
|
||||
function focus() {
|
||||
inputIsActive.value = true;
|
||||
}
|
||||
|
||||
function blur(event: MouseEvent = null) {
|
||||
return setTimeout(reset, 150);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
inputElement.value.blur();
|
||||
dropdownIndex.value = -1;
|
||||
inputIsActive.value = false;
|
||||
}
|
||||
|
||||
function clearInput(event: MouseEvent) {
|
||||
query.value = "";
|
||||
inputElement.value.focus();
|
||||
}
|
||||
|
||||
function handleEscape() {
|
||||
if (!isOpen.value) reset();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<template>
|
||||
<li
|
||||
class="sidebar-list-element"
|
||||
@click="emit('click')"
|
||||
:class="{ active, disabled }"
|
||||
@click="emit('click')"
|
||||
@keydown.enter="emit('click')"
|
||||
>
|
||||
<slot></slot>
|
||||
</li>
|
||||
@@ -12,8 +13,8 @@
|
||||
import { defineProps, defineEmits } from "vue";
|
||||
|
||||
interface Props {
|
||||
active?: Boolean;
|
||||
disabled?: Boolean;
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface Emit {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
ref="descriptionElement"
|
||||
class="movie-description noselect"
|
||||
@click="overflow ? (truncated = !truncated) : null"
|
||||
@keydown.enter="overflow ? (truncated = !truncated) : null"
|
||||
>
|
||||
<span :class="{ truncated }">{{ description }}</span>
|
||||
|
||||
@@ -14,8 +15,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, onMounted } from "vue";
|
||||
import IconArrowDown from "../../icons/IconArrowDown.vue";
|
||||
import type { Ref } from "vue";
|
||||
import IconArrowDown from "../../icons/IconArrowDown.vue";
|
||||
|
||||
interface Props {
|
||||
description: string;
|
||||
@@ -26,7 +27,10 @@
|
||||
const overflow: Ref<boolean> = ref(false);
|
||||
const descriptionElement: Ref<HTMLElement> = ref(null);
|
||||
|
||||
onMounted(checkDescriptionOverflowing);
|
||||
// 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
|
||||
@@ -53,15 +57,13 @@
|
||||
|
||||
document.body.appendChild(descriptionComparisonElement);
|
||||
const elemWithoutOverflowHeight =
|
||||
descriptionComparisonElement.getBoundingClientRect()["height"];
|
||||
descriptionComparisonElement.getBoundingClientRect().height;
|
||||
|
||||
overflow.value = elemWithoutOverflowHeight > height;
|
||||
removeElements(document.querySelectorAll(".dummy-non-overflow"));
|
||||
}
|
||||
|
||||
function removeElements(elems: NodeListOf<Element>) {
|
||||
elems.forEach(el => el.remove());
|
||||
}
|
||||
onMounted(checkDescriptionOverflowing);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<section class="movie">
|
||||
<!-- HEADER w/ POSTER -->
|
||||
<!-- eslint-disable-next-line vuejs-accessibility/click-events-have-key-events -->
|
||||
<header
|
||||
ref="backdropElement"
|
||||
:class="compact ? 'compact' : ''"
|
||||
@@ -8,8 +9,9 @@
|
||||
>
|
||||
<figure class="movie__poster">
|
||||
<img
|
||||
class="movie-item__img is-loaded"
|
||||
ref="poster-image"
|
||||
class="movie-item__img is-loaded"
|
||||
alt="Movie poster"
|
||||
:src="poster"
|
||||
/>
|
||||
</figure>
|
||||
@@ -25,7 +27,7 @@
|
||||
<div class="movie__main">
|
||||
<div class="movie__wrap movie__wrap--main">
|
||||
<!-- SIDEBAR ACTIONS -->
|
||||
<div class="movie__actions" v-if="media">
|
||||
<div v-if="media" class="movie__actions">
|
||||
<action-button :active="media?.exists_in_plex" :disabled="true">
|
||||
<IconThumbsUp v-if="media?.exists_in_plex" />
|
||||
<IconThumbsDown v-else />
|
||||
@@ -36,7 +38,7 @@
|
||||
}}
|
||||
</action-button>
|
||||
|
||||
<action-button @click="sendRequest" :active="requested">
|
||||
<action-button :active="requested" @click="sendRequest">
|
||||
<transition name="fade" mode="out-in">
|
||||
<div v-if="!requested" key="request"><IconRequest /></div>
|
||||
<div v-else key="requested"><IconRequested /></div>
|
||||
@@ -63,8 +65,8 @@
|
||||
|
||||
<action-button
|
||||
v-if="admin === true"
|
||||
@click="showTorrents = !showTorrents"
|
||||
:active="showTorrents"
|
||||
@click="showTorrents = !showTorrents"
|
||||
>
|
||||
<IconBinoculars />
|
||||
Search for torrents
|
||||
@@ -80,11 +82,11 @@
|
||||
</div>
|
||||
|
||||
<!-- Loading placeholder -->
|
||||
<div class="movie__actions text-input__loading" v-else>
|
||||
<div v-else class="movie__actions text-input__loading">
|
||||
<div
|
||||
v-for="index in admin ? Array(4) : Array(3)"
|
||||
class="movie__actions-link"
|
||||
:key="index"
|
||||
class="movie__actions-link"
|
||||
>
|
||||
<div
|
||||
class="movie__actions-text text-input__loading--line"
|
||||
@@ -105,7 +107,7 @@
|
||||
:description="media.overview"
|
||||
/>
|
||||
|
||||
<div class="movie__details" v-if="media">
|
||||
<div v-if="media" class="movie__details">
|
||||
<Detail
|
||||
v-if="media.year"
|
||||
title="Release date"
|
||||
@@ -144,7 +146,7 @@
|
||||
|
||||
<!-- TODO: change this classname, this is general -->
|
||||
|
||||
<div class="movie__admin" v-if="showCast && cast?.length">
|
||||
<div v-if="showCast && cast?.length" class="movie__admin">
|
||||
<Detail title="cast">
|
||||
<CastList :cast="cast" />
|
||||
</Detail>
|
||||
@@ -156,7 +158,7 @@
|
||||
v-if="media && admin && showTorrents"
|
||||
class="torrents"
|
||||
:query="media?.title"
|
||||
:tmdb_id="id"
|
||||
:tmdb-id="id"
|
||||
></TorrentList>
|
||||
</div>
|
||||
</section>
|
||||
@@ -181,7 +183,7 @@
|
||||
import ActionButton from "@/components/popup/ActionButton.vue";
|
||||
import Description from "@/components/popup/Description.vue";
|
||||
import LoadingPlaceholder from "@/components/ui/LoadingPlaceholder.vue";
|
||||
import type { Ref, ComputedRef } from "vue";
|
||||
import type { Ref } from "vue";
|
||||
import type {
|
||||
IMovie,
|
||||
IShow,
|
||||
@@ -194,12 +196,11 @@
|
||||
import {
|
||||
getMovie,
|
||||
getShow,
|
||||
getPerson,
|
||||
getMovieCredits,
|
||||
getShowCredits,
|
||||
request,
|
||||
getRequestStatus,
|
||||
watchLink
|
||||
getRequestStatus
|
||||
// watchLink
|
||||
} from "../../api";
|
||||
|
||||
interface Props {
|
||||
@@ -222,45 +223,26 @@
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const loggedIn = computed(() => store.getters["user/loggedIn"]);
|
||||
const admin = computed(() => store.getters["user/admin"]);
|
||||
const plexId = computed(() => store.getters["user/plexId"]);
|
||||
const poster = computed(() => computePoster());
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
// On created functions
|
||||
fetchMedia();
|
||||
setBackdrop();
|
||||
store.dispatch("torrentModule/setResultCount", null);
|
||||
// End on create functions
|
||||
|
||||
function fetchMedia() {
|
||||
if (!props.id || !props.type) {
|
||||
console.error("Unable to fetch media, requires id & type");
|
||||
return;
|
||||
}
|
||||
|
||||
let apiFunction: Function;
|
||||
let parameters: object;
|
||||
|
||||
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(media => getCredits(props.type))
|
||||
.then(credits => (cast.value = credits?.cast))
|
||||
.then(() => getRequestStatus(props.id, props.type))
|
||||
.then(requestStatus => (requested.value = requestStatus || false));
|
||||
function setCast(_cast: ICast[]) {
|
||||
cast.value = _cast;
|
||||
}
|
||||
function setRequested(status: boolean) {
|
||||
requested.value = status;
|
||||
}
|
||||
|
||||
function getCredits(
|
||||
@@ -268,7 +250,8 @@
|
||||
): Promise<IMediaCredits> {
|
||||
if (type === MediaTypes.Movie) {
|
||||
return getMovieCredits(props.id);
|
||||
} else if (type === MediaTypes.Show) {
|
||||
}
|
||||
if (type === MediaTypes.Show) {
|
||||
return getShowCredits(props.id);
|
||||
}
|
||||
|
||||
@@ -280,35 +263,64 @@
|
||||
return _media;
|
||||
}
|
||||
|
||||
const computePoster = () => {
|
||||
if (!media.value) return "/assets/placeholder.png";
|
||||
else if (!media.value?.poster) return "/assets/no-image.svg";
|
||||
function fetchMedia() {
|
||||
if (!props.id || !props.type) {
|
||||
console.error("Unable to fetch media, requires id & type"); // eslint-disable-line no-console
|
||||
return;
|
||||
}
|
||||
|
||||
return `${ASSET_URL}${ASSET_SIZES[0]}${media.value.poster}`;
|
||||
};
|
||||
let apiFunction: typeof getMovie;
|
||||
let parameters: {
|
||||
checkExistance: boolean;
|
||||
credits: boolean;
|
||||
releaseDates?: boolean;
|
||||
};
|
||||
|
||||
function setBackdrop() {
|
||||
if (!media.value?.backdrop || !backdropElement.value?.style) return "";
|
||||
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 => (requested.value = resp?.success || false)
|
||||
request(props.id, props.type).then(resp =>
|
||||
setRequested(resp?.success || false)
|
||||
);
|
||||
}
|
||||
|
||||
function openInPlex() {
|
||||
return;
|
||||
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;
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
</h1>
|
||||
<div v-else>
|
||||
<loading-placeholder :count="1" />
|
||||
<loading-placeholder :count="1" lineClass="short" :top="3.5" />
|
||||
<loading-placeholder :count="1" line-class="short" :top="3.5" />
|
||||
</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"
|
||||
? "Actor"
|
||||
@@ -21,8 +21,9 @@
|
||||
|
||||
<figure class="person__poster">
|
||||
<img
|
||||
class="person-item__img is-loaded"
|
||||
ref="poster-image"
|
||||
class="person-item__img is-loaded"
|
||||
:alt="`Image of ${person.name}`"
|
||||
:src="poster"
|
||||
/>
|
||||
</figure>
|
||||
@@ -30,9 +31,9 @@
|
||||
|
||||
<div v-if="loading">
|
||||
<loading-placeholder :count="6" />
|
||||
<loading-placeholder lineClass="short" :top="3" />
|
||||
<loading-placeholder :count="6" lineClass="fullwidth" />
|
||||
<loading-placeholder lineClass="short" :top="4.5" />
|
||||
<loading-placeholder line-class="short" :top="3" />
|
||||
<loading-placeholder :count="6" line-class="fullwidth" />
|
||||
<loading-placeholder line-class="short" :top="4.5" />
|
||||
<loading-placeholder />
|
||||
</div>
|
||||
|
||||
@@ -50,17 +51,17 @@
|
||||
</Detail>
|
||||
|
||||
<Detail
|
||||
v-if="creditedShows.length"
|
||||
title="movies"
|
||||
:detail="`Credited in ${creditedMovies.length} movies`"
|
||||
v-if="creditedShows.length"
|
||||
>
|
||||
<CastList :cast="creditedMovies" />
|
||||
</Detail>
|
||||
|
||||
<Detail
|
||||
v-if="creditedShows.length"
|
||||
title="shows"
|
||||
:detail="`Credited in ${creditedShows.length} shows`"
|
||||
v-if="creditedShows.length"
|
||||
>
|
||||
<CastList :cast="creditedShows" />
|
||||
</Detail>
|
||||
@@ -70,17 +71,15 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, defineProps } from "vue";
|
||||
import img from "@/directives/v-image.vue";
|
||||
import CastList from "@/components/CastList.vue";
|
||||
import Detail from "@/components/popup/Detail.vue";
|
||||
import Description from "@/components/popup/Description.vue";
|
||||
import LoadingPlaceholder from "@/components/ui/LoadingPlaceholder.vue";
|
||||
import { getPerson, getPersonCredits } from "../../api";
|
||||
import type { Ref, ComputedRef } from "vue";
|
||||
import { getPerson, getPersonCredits } from "../../api";
|
||||
import type {
|
||||
IPerson,
|
||||
IPersonCredits,
|
||||
ICast,
|
||||
IMovie,
|
||||
IShow
|
||||
} from "../../interfaces/IList";
|
||||
@@ -100,38 +99,39 @@
|
||||
const creditedMovies: Ref<Array<IMovie | IShow>> = ref([]);
|
||||
const creditedShows: Ref<Array<IMovie | IShow>> = ref([]);
|
||||
|
||||
const poster: ComputedRef<string> = computed(() => computePoster());
|
||||
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;
|
||||
if (!person.value?.birthday) return "";
|
||||
|
||||
const today = new Date().getFullYear();
|
||||
const birthYear = new Date(person.value.birthday).getFullYear();
|
||||
return `${today - birthYear} years old`;
|
||||
});
|
||||
|
||||
// On create functions
|
||||
fetchPerson();
|
||||
//
|
||||
function setCredits(_credits: IPersonCredits) {
|
||||
credits.value = _credits;
|
||||
}
|
||||
|
||||
function fetchPerson() {
|
||||
if (!props.id) {
|
||||
console.error("Unable to fetch person, missing id!");
|
||||
return;
|
||||
}
|
||||
|
||||
getPerson(props.id)
|
||||
.then(_person => (person.value = _person))
|
||||
.then(() => getPersonCredits(person.value?.id))
|
||||
.then(_credits => (credits.value = _credits))
|
||||
.then(() => personCreditedFrom(credits.value?.cast));
|
||||
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: any[]) {
|
||||
const names = self.map(item => item.title);
|
||||
function alreadyExists(
|
||||
item: IMovie | IShow,
|
||||
pos: number,
|
||||
self: Array<IMovie | IShow>
|
||||
) {
|
||||
const names = self.map(_item => _item.title);
|
||||
return names.indexOf(item.title) === pos;
|
||||
}
|
||||
|
||||
@@ -147,12 +147,21 @@
|
||||
.sort(sortPopularity);
|
||||
}
|
||||
|
||||
const computePoster = () => {
|
||||
if (!person.value) return "/assets/placeholder.png";
|
||||
else if (!person.value?.poster) return "/assets/no-image.svg";
|
||||
function fetchPerson() {
|
||||
if (!props.id) {
|
||||
console.error("Unable to fetch person, missing id!"); // eslint-disable-line no-console
|
||||
return;
|
||||
}
|
||||
|
||||
return `${ASSET_URL}${ASSET_SIZES[0]}${person.value.poster}`;
|
||||
};
|
||||
getPerson(props.id)
|
||||
.then(setPerson)
|
||||
.then(() => getPersonCredits(person.value?.id))
|
||||
.then(setCredits)
|
||||
.then(() => personCreditedFrom(credits.value?.cast));
|
||||
}
|
||||
|
||||
// On create functions
|
||||
fetchPerson();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -3,24 +3,24 @@
|
||||
<h3 class="settings__header">Change password</h3>
|
||||
<form class="form">
|
||||
<seasoned-input
|
||||
v-model="oldPassword"
|
||||
placeholder="old password"
|
||||
icon="Keyhole"
|
||||
type="password"
|
||||
v-model="oldPassword"
|
||||
/>
|
||||
|
||||
<seasoned-input
|
||||
v-model="newPassword"
|
||||
placeholder="new password"
|
||||
icon="Keyhole"
|
||||
type="password"
|
||||
v-model="newPassword"
|
||||
/>
|
||||
|
||||
<seasoned-input
|
||||
v-model="newPasswordRepeat"
|
||||
placeholder="repeat new password"
|
||||
icon="Keyhole"
|
||||
type="password"
|
||||
v-model="newPasswordRepeat"
|
||||
/>
|
||||
|
||||
<seasoned-button @click="changePassword">change password</seasoned-button>
|
||||
@@ -34,24 +34,20 @@
|
||||
import SeasonedInput from "@/components/ui/SeasonedInput.vue";
|
||||
import SeasonedButton from "@/components/ui/SeasonedButton.vue";
|
||||
import SeasonedMessages from "@/components/ui/SeasonedMessages.vue";
|
||||
import { ErrorMessageTypes } from "../../interfaces/IErrorMessage";
|
||||
import type { Ref } from "vue";
|
||||
import { ErrorMessageTypes } from "../../interfaces/IErrorMessage";
|
||||
import type { IErrorMessage } from "../../interfaces/IErrorMessage";
|
||||
|
||||
interface ResetPasswordPayload {
|
||||
old_password: string;
|
||||
new_password: string;
|
||||
}
|
||||
// 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 clearMessages() {
|
||||
messages.value = [];
|
||||
}
|
||||
|
||||
function addWarningMessage(message: string, title?: string) {
|
||||
messages.value.push({
|
||||
message,
|
||||
@@ -64,12 +60,12 @@
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!oldPassword.value || oldPassword?.value?.length === 0) {
|
||||
addWarningMessage("Missing old password!", "Validation error");
|
||||
return reject();
|
||||
reject();
|
||||
}
|
||||
|
||||
if (!newPassword.value || newPassword?.value?.length === 0) {
|
||||
addWarningMessage("Missing new password!", "Validation error");
|
||||
return reject();
|
||||
reject();
|
||||
}
|
||||
|
||||
if (newPassword.value !== newPasswordRepeat.value) {
|
||||
@@ -77,7 +73,7 @@
|
||||
"Password and password repeat do not match!",
|
||||
"Validation error"
|
||||
);
|
||||
return reject();
|
||||
reject();
|
||||
}
|
||||
|
||||
resolve(true);
|
||||
@@ -89,15 +85,14 @@
|
||||
try {
|
||||
validate();
|
||||
} catch (error) {
|
||||
console.log("not valid!");
|
||||
return;
|
||||
console.log("not valid!"); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
const body: ResetPasswordPayload = {
|
||||
old_password: oldPassword.value,
|
||||
new_password: newPassword.value
|
||||
};
|
||||
const options = {};
|
||||
// const body: ResetPasswordPayload = {
|
||||
// old_password: oldPassword.value,
|
||||
// new_password: newPassword.value
|
||||
// };
|
||||
// const options = {};
|
||||
// fetch()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
|
||||
<form class="form">
|
||||
<seasoned-input
|
||||
v-model="username"
|
||||
placeholder="plex username"
|
||||
type="email"
|
||||
v-model="username"
|
||||
/>
|
||||
<seasoned-input
|
||||
v-model="password"
|
||||
placeholder="plex password"
|
||||
type="password"
|
||||
v-model="password"
|
||||
@enter="authenticatePlex"
|
||||
>
|
||||
</seasoned-input>
|
||||
@@ -48,9 +48,9 @@
|
||||
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 { Ref, ComputedRef } from "vue";
|
||||
import type { IErrorMessage } from "../../interfaces/IErrorMessage";
|
||||
|
||||
interface Emit {
|
||||
@@ -64,20 +64,11 @@
|
||||
const store = useStore();
|
||||
const emit = defineEmits<Emit>();
|
||||
|
||||
const loggedIn: ComputedRef<boolean> = computed(
|
||||
() => store.getters["user/loggedIn"]
|
||||
);
|
||||
const plexId: ComputedRef<boolean> = computed(
|
||||
() => store.getters["user/plexId"]
|
||||
);
|
||||
const settings: ComputedRef<boolean> = computed(
|
||||
() => store.getters["user/settings"]
|
||||
);
|
||||
|
||||
async function authenticatePlex() {
|
||||
let username = this.plexUsername;
|
||||
let password = this.plexPassword;
|
||||
|
||||
const { success, message } = await linkPlexAccount(
|
||||
username.value,
|
||||
password.value
|
||||
@@ -92,7 +83,7 @@
|
||||
messages.value.push({
|
||||
type: success ? ErrorMessageTypes.Success : ErrorMessageTypes.Error,
|
||||
title: success ? "Authenticated with plex" : "Something went wrong",
|
||||
message: message
|
||||
message
|
||||
} as IErrorMessage);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="container" v-if="query?.length">
|
||||
<div v-if="query?.length" class="container">
|
||||
<h2 class="torrent-header-text">
|
||||
Searching for: <span class="query">{{ query }}</span>
|
||||
</h2>
|
||||
@@ -22,22 +22,19 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, inject, defineProps } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { sortableSize } from "../../utils";
|
||||
import { searchTorrents, addMagnet } from "../../api";
|
||||
|
||||
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;
|
||||
tmdb_id?: number;
|
||||
tmdbId?: number;
|
||||
}
|
||||
|
||||
const loading: Ref<boolean> = ref(true);
|
||||
const torrents: Ref<ITorrent[]> = ref([]);
|
||||
const release_types: Ref<string[]> = ref(["all"]);
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const store = useStore();
|
||||
@@ -47,15 +44,12 @@
|
||||
error;
|
||||
} = inject("notifications");
|
||||
|
||||
fetchTorrents();
|
||||
function setTorrents(_torrents: ITorrent[]) {
|
||||
torrents.value = _torrents || [];
|
||||
}
|
||||
|
||||
function fetchTorrents() {
|
||||
loading.value = true;
|
||||
|
||||
searchTorrents(props.query)
|
||||
.then(torrentResponse => (torrents.value = torrentResponse?.results))
|
||||
.then(() => updateResultCountDisplay())
|
||||
.finally(() => (loading.value = false));
|
||||
function setLoading(state: boolean) {
|
||||
loading.value = state;
|
||||
}
|
||||
|
||||
function updateResultCountDisplay() {
|
||||
@@ -66,6 +60,15 @@
|
||||
);
|
||||
}
|
||||
|
||||
function fetchTorrents() {
|
||||
loading.value = true;
|
||||
|
||||
searchTorrents(props.query)
|
||||
.then(torrentResponse => setTorrents(torrentResponse?.results))
|
||||
.then(() => updateResultCountDisplay())
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
|
||||
function addTorrent(torrent: ITorrent) {
|
||||
const { name, magnet } = torrent;
|
||||
|
||||
@@ -75,16 +78,15 @@
|
||||
timeout: 3000
|
||||
});
|
||||
|
||||
addMagnet(magnet, name, props.tmdb_id)
|
||||
.then(resp => {
|
||||
addMagnet(magnet, name, props.tmdbId)
|
||||
.then(() => {
|
||||
notifications.success({
|
||||
title: "Torrent added 🎉",
|
||||
description: props.query,
|
||||
timeout: 3000
|
||||
});
|
||||
})
|
||||
.catch(resp => {
|
||||
console.log("Error while adding torrent:", resp?.data);
|
||||
.catch(() => {
|
||||
notifications.error({
|
||||
title: "Failed to add torrent 🙅♀️",
|
||||
description: "Check console for more info",
|
||||
@@ -92,6 +94,8 @@
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fetchTorrents();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<th
|
||||
v-for="column in columns"
|
||||
:key="column"
|
||||
@click="sortTable(column)"
|
||||
:class="column === selectedColumn ? 'active' : null"
|
||||
@click="sortTable(column)"
|
||||
>
|
||||
{{ column }}
|
||||
<span v-if="prevCol === column && direction">↑</span>
|
||||
@@ -18,13 +18,32 @@
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="torrent in torrents"
|
||||
class="table__content"
|
||||
:key="torrent.magnet"
|
||||
class="table__content"
|
||||
>
|
||||
<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="() => emit('magnet', torrent)" class="download">
|
||||
<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>
|
||||
@@ -35,8 +54,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, defineEmits } from "vue";
|
||||
import IconMagnet from "@/icons/IconMagnet.vue";
|
||||
import { sortableSize } from "../../utils";
|
||||
import type { Ref } from "vue";
|
||||
import { sortableSize } from "../../utils";
|
||||
import type ITorrent from "../../interfaces/ITorrent";
|
||||
|
||||
interface Props {
|
||||
@@ -87,18 +106,6 @@
|
||||
tableRow.insertAdjacentElement("afterend", expandedRow);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function sortName() {
|
||||
const torrentsCopy = [...torrents.value];
|
||||
if (direction.value) {
|
||||
@@ -112,11 +119,11 @@
|
||||
const torrentsCopy = [...torrents.value];
|
||||
if (direction.value) {
|
||||
torrents.value = torrentsCopy.sort(
|
||||
(a, b) => parseInt(a.seed) - parseInt(b.seed)
|
||||
(a, b) => parseInt(a.seed, 10) - parseInt(b.seed, 10)
|
||||
);
|
||||
} else {
|
||||
torrents.value = torrentsCopy.sort(
|
||||
(a, b) => parseInt(b.seed) - parseInt(a.seed)
|
||||
(a, b) => parseInt(b.seed, 10) - parseInt(a.seed, 10)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -133,6 +140,18 @@
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
<div>
|
||||
<torrent-search-results
|
||||
:query="query"
|
||||
:tmdb_id="tmdb_id"
|
||||
: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>
|
||||
@@ -32,7 +34,7 @@
|
||||
|
||||
interface Props {
|
||||
query: string;
|
||||
tmdb_id?: number;
|
||||
tmdbId?: number;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
<template>
|
||||
<div class="darkToggle">
|
||||
<span @click="toggleDarkmode">{{ darkmodeToggleIcon }}</span>
|
||||
<span @click="toggleDarkmode" @keydown.enter="toggleDarkmode">{{
|
||||
darkmodeToggleIcon
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
let darkmode = ref(systemDarkModeEnabled());
|
||||
function systemDarkModeEnabled() {
|
||||
const computedStyle = window.getComputedStyle(document.body);
|
||||
if (computedStyle?.colorScheme != null) {
|
||||
return computedStyle.colorScheme.includes("dark");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const darkmode = ref(systemDarkModeEnabled());
|
||||
const darkmodeToggleIcon = computed(() => {
|
||||
return darkmode.value ? "🌝" : "🌚";
|
||||
});
|
||||
@@ -16,14 +26,6 @@
|
||||
darkmode.value = !darkmode.value;
|
||||
document.body.className = darkmode.value ? "dark" : "light";
|
||||
}
|
||||
|
||||
function systemDarkModeEnabled() {
|
||||
const computedStyle = window.getComputedStyle(document.body);
|
||||
if (computedStyle["colorScheme"] != null) {
|
||||
return computedStyle.colorScheme.includes("dark");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<div
|
||||
class="nav__hamburger"
|
||||
:class="{ open: isOpen }"
|
||||
tabindex="0"
|
||||
@click="toggle"
|
||||
@keydown.enter="toggle"
|
||||
tabindex="0"
|
||||
>
|
||||
<div v-for="(_, index) in 3" :key="index" class="bar"></div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="`loader type-${type}`">
|
||||
<div :class="`loader type-${type || LoaderHeightType.Page}`">
|
||||
<i class="loader--icon">
|
||||
<i class="loader--icon-spinner" />
|
||||
</i>
|
||||
@@ -13,17 +13,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps } from "vue";
|
||||
|
||||
enum LoaderHeightType {
|
||||
Page = "page",
|
||||
Section = "section"
|
||||
}
|
||||
import LoaderHeightType from "../../interfaces/ILoader";
|
||||
|
||||
interface Props {
|
||||
type?: LoaderHeightType;
|
||||
}
|
||||
|
||||
const { type = LoaderHeightType.Page } = defineProps<Props>();
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="text-input__loading" :style="`margin-top: ${top}rem`">
|
||||
<div class="text-input__loading" :style="`margin-top: ${top || 0}rem`">
|
||||
<div
|
||||
class="text-input__loading--line"
|
||||
:class="lineClass"
|
||||
v-for="l in Array(count)"
|
||||
v-for="l in Array(count || 1)"
|
||||
:key="l"
|
||||
class="text-input__loading--line"
|
||||
:class="lineClass || ''"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -13,12 +13,12 @@
|
||||
import { defineProps } from "vue";
|
||||
|
||||
interface Props {
|
||||
count?: Number;
|
||||
lineClass?: String;
|
||||
top?: Number;
|
||||
count?: number;
|
||||
lineClass?: string;
|
||||
top?: number;
|
||||
}
|
||||
|
||||
const { count = 1, lineClass = "", top = 0 } = defineProps<Props>();
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
@click="emit('click')"
|
||||
:class="{ active: active, fullwidth: fullWidth }"
|
||||
@click="emit('click')"
|
||||
>
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, defineEmits } from "vue";
|
||||
import type { Ref } from "vue";
|
||||
import { defineProps, defineEmits } from "vue";
|
||||
|
||||
interface Props {
|
||||
active?: Boolean;
|
||||
fullWidth?: Boolean;
|
||||
active?: boolean;
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
interface Emit {
|
||||
(e: "click");
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
defineProps<Props>();
|
||||
const emit = defineEmits<Emit>();
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
<div class="group" :class="{ completed: modelValue, focus }">
|
||||
<component :is="inputIcon" v-if="inputIcon" />
|
||||
|
||||
<!-- eslint-disable-next-line vuejs-accessibility/form-control-has-label -->
|
||||
<input
|
||||
class="input"
|
||||
:type="toggledType || type"
|
||||
:type="toggledType || type || 'text'"
|
||||
:placeholder="placeholder"
|
||||
:value="modelValue"
|
||||
@input="handleInput"
|
||||
@@ -15,10 +16,10 @@
|
||||
|
||||
<i
|
||||
v-if="modelValue && type === 'password'"
|
||||
@click="toggleShowPassword"
|
||||
@keydown.enter="toggleShowPassword"
|
||||
class="show noselect"
|
||||
tabindex="0"
|
||||
@click="toggleShowPassword"
|
||||
@keydown.enter="toggleShowPassword"
|
||||
>{{ toggledType == "password" ? "show" : "hide" }}</i
|
||||
>
|
||||
</div>
|
||||
@@ -43,16 +44,16 @@
|
||||
(e: "update:modelValue", value: string);
|
||||
}
|
||||
|
||||
const { placeholder, type = "text", modelValue } = defineProps<Props>();
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emit>();
|
||||
|
||||
const toggledType: Ref<string> = ref(type);
|
||||
const toggledType: Ref<string> = ref(props.type);
|
||||
const focus: Ref<boolean> = ref(false);
|
||||
|
||||
const inputIcon = computed(() => {
|
||||
if (type === "password") return IconKey;
|
||||
if (type === "email") return IconEmail;
|
||||
if (type === "torrents") return IconBinoculars;
|
||||
if (props.type === "password") return IconKey;
|
||||
if (props.type === "email") return IconEmail;
|
||||
if (props.type === "torrents") return IconBinoculars;
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<transition-group name="fade">
|
||||
<div
|
||||
class="card"
|
||||
v-for="(message, index) in messages"
|
||||
:key="generateMessageKey(index, message)"
|
||||
class="card"
|
||||
:class="message.type || 'warning'"
|
||||
>
|
||||
<span class="pinstripe"></span>
|
||||
<div class="content">
|
||||
<h2 class="title">
|
||||
{{ message.title }}
|
||||
{{ message.title || titleFromType(message.type) }}
|
||||
</h2>
|
||||
<span v-if="message.message" class="message">{{
|
||||
message.message
|
||||
@@ -27,7 +27,6 @@
|
||||
ErrorMessageTypes,
|
||||
IErrorMessage
|
||||
} from "../../interfaces/IErrorMessage";
|
||||
import type { Ref } from "vue";
|
||||
|
||||
interface Props {
|
||||
messages: IErrorMessage[];
|
||||
@@ -52,8 +51,9 @@
|
||||
}
|
||||
|
||||
function dismiss(index: number) {
|
||||
props.messages.splice(index, 1);
|
||||
emit("update:messages", [...props.messages]);
|
||||
const _messages = [...props.messages];
|
||||
_messages.splice(index, 1);
|
||||
emit("update:messages", _messages);
|
||||
}
|
||||
|
||||
function generateMessageKey(
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
v-for="option in options"
|
||||
:key="option"
|
||||
class="toggle-button"
|
||||
@click="toggleTo(option)"
|
||||
:class="selected === option ? 'selected' : null"
|
||||
@click="() => toggleTo(option)"
|
||||
>
|
||||
{{ option }}
|
||||
</button>
|
||||
@@ -13,8 +13,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, defineEmits } from "vue";
|
||||
import type { Ref } from "vue";
|
||||
import { defineProps, defineEmits } from "vue";
|
||||
|
||||
interface Props {
|
||||
options: string[];
|
||||
|
||||
Reference in New Issue
Block a user