Upgraded all components to vue 3 & typescript

This commit is contained in:
2022-08-06 16:10:13 +02:00
parent 890d0c428d
commit d12dfc3c8e
34 changed files with 3508 additions and 3554 deletions

View File

@@ -5,9 +5,9 @@
v-for="result in searchResults"
:key="`${result.index}-${result.title}-${result.type}`"
@click="openPopup(result)"
:class="
`result di-${result.index} ${result.index === index ? 'active' : ''}`
"
: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" />
@@ -24,239 +24,229 @@
</transition>
</template>
<script>
import { mapActions } from "vuex";
import IconMovie from "src/icons/IconMovie";
import IconShow from "src/icons/IconShow";
import IconPerson from "src/icons/IconPerson";
import { elasticSearchMoviesAndShows } from "@/api";
<script setup lang="ts">
import { ref, watch, defineProps } from "vue";
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";
export default {
components: { IconMovie, IconShow, IconPerson },
props: {
query: {
type: String,
default: null,
required: false
},
index: {
type: Number,
default: -1,
required: false
},
results: {
type: Array,
default: [],
required: false
}
},
watch: {
query(newQuery) {
if (newQuery && newQuery.length > 1) this.fetchAutocompleteResults();
}
},
data() {
return {
searchResults: [],
keyboardNavigationIndex: 0,
numberOfResults: 10
};
},
methods: {
...mapActions("popup", ["open"]),
openPopup(result) {
const { id, type } = result;
this.open({ id, type });
},
fetchAutocompleteResults() {
this.keyboardNavigationIndex = 0;
this.searchResults = [];
elasticSearchMoviesAndShows(this.query, this.numberOfResults).then(
resp => {
const data = resp.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
};
}
});
results = this.removeDuplicates(results);
results = results.map((el, index) => {
return { ...el, index };
});
this.$emit("update:results", results);
this.searchResults = results;
}
);
},
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);
});
if (this.adult == false) {
filteredResults = filteredResults.filter(
result => result.adult == false
);
}
return filteredResults;
}
},
created() {
if (this.query) this.fetchAutocompleteResults();
interface Props {
query?: string;
index?: Number;
results?: Array<any>;
}
interface Emit {
(e: "update:results", value: Array<any>);
}
const numberOfResults: number = 10;
const props = defineProps<Props>();
const emit = defineEmits<Emit>();
const store = useStore();
const searchResults: Ref<Array<any>> = ref([]);
const keyboardNavigationIndex: Ref<number> = ref(0);
// on load functions
fetchAutocompleteResults();
// end on load functions
watch(
() => props.query,
newQuery => {
if (newQuery?.length > 0) fetchAutocompleteResults();
}
);
function openPopup(result) {
if (!result.id || !result.type) return;
store.dispatch("popup/open", { ...result });
}
function fetchAutocompleteResults() {
keyboardNavigationIndex.value = 0;
searchResults.value = [];
elasticSearchMoviesAndShows(props.query, numberOfResults)
.then(elasticResponse => parseElasticResponse(elasticResponse))
.then(_searchResults => {
emit("update:results", _searchResults);
searchResults.value = _searchResults;
});
}
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;
}
};
</script>
<style lang="scss" scoped>
@import "src/scss/variables";
@import "src/scss/media-queries";
@import "src/scss/main";
$sizes: 22;
@import "src/scss/variables";
@import "src/scss/media-queries";
@import "src/scss/main";
$sizes: 22;
@for $i from 0 through $sizes {
.dropdown .di-#{$i} {
visibility: visible;
transform-origin: top center;
animation: scaleZ 200ms calc(50ms * #{$i}) ease-in forwards;
}
}
@keyframes scaleZ {
0% {
opacity: 0;
transform: scale(0);
}
80% {
transform: scale(1.07);
}
100% {
opacity: 1;
transform: scale(1);
}
}
.dropdown {
top: var(--header-size);
position: relative;
height: 100%;
width: 100%;
max-width: 720px;
display: flex;
flex-direction: column;
flex-wrap: wrap;
z-index: 5;
margin-top: -1px;
border-top: none;
padding: 0;
@include mobile {
position: fixed;
left: 0;
max-width: 100vw;
}
@include tablet-min {
top: unset;
--gutter: 1.5rem;
max-width: calc(100% - (2 * var(--gutter)));
margin: -1px var(--gutter) 0 var(--gutter);
}
@include desktop {
max-width: 720px;
}
}
li.result {
background-color: var(--background-95);
color: var(--text-color-50);
padding: 0.5rem 2rem;
list-style: none;
opacity: 0;
height: 56px;
width: 100%;
visibility: hidden;
display: flex;
align-items: center;
padding: 0.5rem 2rem;
font-size: 1.4rem;
text-transform: capitalize;
list-style: none;
cursor: pointer;
white-space: nowrap;
transition: color 0.1s ease, fill 0.4s ease;
span {
overflow-x: hidden;
text-overflow: ellipsis;
transition: inherit;
}
&.active,
&:hover,
&:active {
color: var(--text-color);
border-bottom: 2px solid var(--color-green);
.type-icon {
fill: var(--text-color);
@for $i from 0 through $sizes {
.dropdown .di-#{$i} {
visibility: visible;
transform-origin: top center;
animation: scaleZ 200ms calc(50ms * #{$i}) ease-in forwards;
}
}
.type-icon {
width: 28px;
height: 28px;
margin-right: 1rem;
transition: inherit;
fill: var(--text-color-50);
@keyframes scaleZ {
0% {
opacity: 0;
transform: scale(0);
}
80% {
transform: scale(1.07);
}
100% {
opacity: 1;
transform: scale(1);
}
}
}
li.info {
visibility: hidden;
opacity: 0;
display: flex;
justify-content: center;
padding: 0 1rem;
color: var(--text-color-50);
background-color: var(--background-95);
color: var(--text-color-50);
font-size: 0.6rem;
height: 16px;
width: 100%;
}
.dropdown {
top: var(--header-size);
position: relative;
height: 100%;
width: 100%;
max-width: 720px;
display: flex;
flex-direction: column;
flex-wrap: wrap;
z-index: 5;
.shut-leave-to {
height: 0px;
transition: height 0.4s ease;
flex-wrap: no-wrap;
overflow: hidden;
}
margin-top: -1px;
border-top: none;
padding: 0;
@include mobile {
position: fixed;
left: 0;
max-width: 100vw;
}
@include tablet-min {
top: unset;
--gutter: 1.5rem;
max-width: calc(100% - (2 * var(--gutter)));
margin: -1px var(--gutter) 0 var(--gutter);
}
@include desktop {
max-width: 720px;
}
}
li.result {
background-color: var(--background-95);
color: var(--text-color-50);
padding: 0.5rem 2rem;
list-style: none;
opacity: 0;
height: 56px;
width: 100%;
visibility: hidden;
display: flex;
align-items: center;
padding: 0.5rem 2rem;
font-size: 1.4rem;
text-transform: capitalize;
list-style: none;
cursor: pointer;
white-space: nowrap;
transition: color 0.1s ease, fill 0.4s ease;
span {
overflow-x: hidden;
text-overflow: ellipsis;
transition: inherit;
}
&.active,
&:hover,
&:active {
color: var(--text-color);
border-bottom: 2px solid var(--color-green);
.type-icon {
fill: var(--text-color);
}
}
.type-icon {
width: 28px;
height: 28px;
margin-right: 1rem;
transition: inherit;
fill: var(--text-color-50);
}
}
li.info {
visibility: hidden;
opacity: 0;
display: flex;
justify-content: center;
padding: 0 1rem;
color: var(--text-color-50);
background-color: var(--background-95);
color: var(--text-color-50);
font-size: 0.6rem;
height: 16px;
width: 100%;
}
.shut-leave-to {
height: 0px;
transition: height 0.4s ease;
flex-wrap: no-wrap;
overflow: hidden;
}
</style>

View File

@@ -10,10 +10,10 @@
<SearchInput />
<Hamburger class="mobile-only" />
<NavigationIcon class="desktop-only" :route="profileRoute" />
<div class="navigation-icons-grid mobile-only" :class="{ open: isOpen }">
<!-- <div class="navigation-icons-grid mobile-only" :class="{ open: isOpen }"> -->
<div class="navigation-icons-grid mobile-only" v-if="isOpen">
<NavigationIcons>
<NavigationIcon :route="profileRoute" />
</NavigationIcons>
@@ -21,106 +21,104 @@
</nav>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
import TmdbLogo from "@/icons/tmdb-logo";
import IconProfile from "@/icons/IconProfile";
import IconProfileLock from "@/icons/IconProfileLock";
import IconSettings from "@/icons/IconSettings";
import IconActivity from "@/icons/IconActivity";
import SearchInput from "@/components/header/SearchInput";
import NavigationIcons from "src/components/header/NavigationIcons";
import NavigationIcon from "src/components/header/NavigationIcon";
import Hamburger from "@/components/ui/Hamburger";
<script setup lang="ts">
import { computed, defineProps, PropType } from "vue";
import type { App } from "vue";
import { useStore } from "vuex";
import { useRoute } from "vue-router";
import SearchInput from "@/components/header/SearchInput.vue";
import Hamburger from "@/components/ui/Hamburger.vue";
import NavigationIcons from "@/components/header/NavigationIcons.vue";
import NavigationIcon from "@/components/header/NavigationIcon.vue";
import TmdbLogo from "@/icons/tmdb-logo.vue";
import IconProfile from "@/icons/IconProfile.vue";
import IconProfileLock from "@/icons/IconProfileLock.vue";
import type INavigationIcon from "../../interfaces/INavigationIcon";
export default {
components: {
NavigationIcons,
NavigationIcon,
SearchInput,
TmdbLogo,
IconProfile,
IconProfileLock,
IconSettings,
IconActivity,
Hamburger
},
computed: {
...mapGetters("user", ["loggedIn"]),
...mapGetters("hamburger", ["isOpen"]),
isHome() {
return this.$route.path === "/";
},
profileRoute() {
return {
title: !this.loggedIn ? "Signin" : "Profile",
route: !this.loggedIn ? "/signin" : "/profile",
icon: !this.loggedIn ? IconProfileLock : IconProfile
};
}
}
};
const route = useRoute();
const store = useStore();
const signinNavigationIcon: INavigationIcon = {
title: "Signin",
route: "/signin",
icon: IconProfileLock
};
const profileNavigationIcon: INavigationIcon = {
title: "Profile",
route: "/profile",
icon: IconProfile
};
const isHome = computed(() => route.path === "/");
const isOpen = computed(() => store.getters["hamburger/isOpen"]);
const loggedIn = computed(() => store.getters["user/loggedIn"]);
const profileRoute = computed(() =>
!loggedIn.value ? signinNavigationIcon : profileNavigationIcon
);
</script>
<style lang="scss" scoped>
@import "src/scss/variables";
@import "src/scss/media-queries";
@import "src/scss/variables";
@import "src/scss/media-queries";
.spacer {
@include mobile-only {
.spacer {
@include mobile-only {
width: 100%;
height: $header-size;
}
}
nav {
display: grid;
grid-template-columns: var(--header-size) 1fr var(--header-size);
> * {
z-index: 10;
}
}
.nav__logo {
overflow: hidden;
}
.logo {
padding: 1rem;
fill: var(--color-green);
width: var(--header-size);
height: var(--header-size);
display: flex;
align-items: center;
justify-content: center;
background: $background-nav-logo;
transition: transform 0.3s ease;
&:hover {
transform: scale(1.08);
}
@include mobile {
padding: 0.5rem;
}
}
.navigation-icons-grid {
display: flex;
flex-wrap: wrap;
position: fixed;
top: var(--header-size);
left: 0;
width: 100%;
height: $header-size;
}
}
background-color: $background-95;
visibility: hidden;
opacity: 0;
transition: opacity 0.4s ease;
nav {
display: grid;
grid-template-columns: var(--header-size) 1fr var(--header-size);
> * {
z-index: 10;
}
}
.nav__logo {
overflow: hidden;
}
.logo {
padding: 1rem;
fill: var(--color-green);
width: var(--header-size);
height: var(--header-size);
display: flex;
align-items: center;
justify-content: center;
background: $background-nav-logo;
transition: transform 0.3s ease;
&:hover {
transform: scale(1.08);
}
@include mobile {
padding: 0.5rem;
}
}
.navigation-icons-grid {
display: flex;
flex-wrap: wrap;
position: fixed;
top: var(--header-size);
left: 0;
width: 100%;
background-color: $background-95;
visibility: hidden;
opacity: 0;
transition: opacity 0.4s ease;
&.open {
opacity: 1;
visibility: visible;
&.open {
}
}
}
</style>

View File

@@ -1,8 +1,8 @@
<template>
<router-link
:to="{ path: route.route }"
:key="route.title"
v-if="route.requiresAuth == undefined || (route.requiresAuth && loggedIn)"
:to="{ path: route?.route }"
:key="route?.title"
v-if="route?.requiresAuth == undefined || (route?.requiresAuth && loggedIn)"
>
<li class="navigation-link" :class="{ active: route.route == active }">
<component class="navigation-icon" :is="route.icon"></component>
@@ -11,71 +11,66 @@
</router-link>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
<script setup lang="ts">
import { useStore } from "vuex";
import { computed, defineProps } from "vue";
import type INavigationIcon from "../../interfaces/INavigationIcon";
export default {
name: "NavigationIcon",
props: {
active: {
type: String,
required: false
},
route: {
type: Object,
required: true
}
},
computed: {
...mapGetters("user", ["loggedIn"])
interface Props {
route: INavigationIcon;
active?: string;
}
};
defineProps<Props>();
const store = useStore();
const loggedIn = computed(() => store.getters["user/loggedIn"]);
</script>
<style lang="scss" scoped>
@import "src/scss/media-queries";
@import "src/scss/media-queries";
.navigation-link {
display: grid;
place-items: center;
min-height: var(--header-size);
list-style: none;
padding: 1rem 0.15rem;
text-align: center;
background-color: var(--background-color-secondary);
transition: transform 0.3s ease, color 0.3s ease, stoke 0.3s ease,
fill 0.3s ease, background-color 0.5s ease;
.navigation-link {
display: grid;
place-items: center;
min-height: var(--header-size);
list-style: none;
padding: 1rem 0.15rem;
text-align: center;
background-color: var(--background-color-secondary);
transition: transform 0.3s ease, color 0.3s ease, stoke 0.3s ease,
fill 0.3s ease, background-color 0.5s ease;
&:hover {
transform: scale(1.05);
}
&:hover {
transform: scale(1.05);
}
&:hover,
&.active {
background-color: var(--background-color);
&:hover,
&.active {
background-color: var(--background-color);
span,
.navigation-icon {
color: var(--text-color);
fill: var(--text-color);
span,
.navigation-icon {
color: var(--text-color);
fill: var(--text-color);
}
}
span {
text-transform: uppercase;
font-size: 11px;
margin-top: 0.25rem;
color: var(--text-color-70);
}
}
span {
text-transform: uppercase;
font-size: 11px;
margin-top: 0.25rem;
color: var(--text-color-70);
a {
text-decoration: none;
}
}
a {
text-decoration: none;
}
.navigation-icon {
width: 28px;
fill: var(--text-color-70);
transition: inherit;
}
.navigation-icon {
width: 28px;
fill: var(--text-color-70);
transition: inherit;
}
</style>

View File

@@ -10,94 +10,83 @@
</ul>
</template>
<script>
import NavigationIcon from "@/components/header/NavigationIcon";
import IconInbox from "@/icons/IconInbox";
import IconNowPlaying from "@/icons/IconNowPlaying";
import IconPopular from "@/icons/IconPopular";
import IconUpcoming from "@/icons/IconUpcoming";
import IconSettings from "@/icons/IconSettings";
import IconActivity from "@/icons/IconActivity";
<script setup lang="ts">
import { ref, watch } from "vue";
import { useRoute } from "vue-router";
import NavigationIcon from "@/components/header/NavigationIcon.vue";
import IconInbox from "@/icons/IconInbox.vue";
import IconNowPlaying from "@/icons/IconNowPlaying.vue";
import IconPopular from "@/icons/IconPopular.vue";
import IconUpcoming from "@/icons/IconUpcoming.vue";
import IconSettings from "@/icons/IconSettings.vue";
import IconActivity from "@/icons/IconActivity.vue";
import IconBinoculars from "@/icons/IconBinoculars.vue";
import type INavigationIcon from "../../interfaces/INavigationIcon";
export default {
name: "NavigationIcons",
components: {
NavigationIcon,
IconInbox,
IconPopular,
IconNowPlaying,
IconUpcoming,
IconSettings,
IconActivity
},
data() {
return {
routes: [
{
title: "Requests",
route: "/list/requests",
icon: IconInbox
},
{
title: "Now Playing",
route: "/list/now_playing",
icon: IconNowPlaying
},
{
title: "Popular",
route: "/list/popular",
icon: IconPopular
},
{
title: "Upcoming",
route: "/list/upcoming",
icon: IconUpcoming
},
{
title: "Activity",
route: "/activity",
requiresAuth: true,
icon: IconActivity
},
{
title: "Settings",
route: "/profile?settings=true",
requiresAuth: true,
icon: IconSettings
}
],
activeRoute: null
};
},
watch: {
$route() {
this.activeRoute = window.location.pathname;
const route = useRoute();
const activeRoute = ref(window?.location?.pathname);
const routes: INavigationIcon[] = [
{
title: "Requests",
route: "/list/requests",
icon: IconInbox
},
{
title: "Now Playing",
route: "/list/now_playing",
icon: IconNowPlaying
},
{
title: "Popular",
route: "/list/popular",
icon: IconPopular
},
{
title: "Upcoming",
route: "/list/upcoming",
icon: IconUpcoming
},
{
title: "Activity",
route: "/activity",
requiresAuth: true,
icon: IconActivity
},
{
title: "Torrents",
route: "/torrents",
requiresAuth: true,
icon: IconBinoculars
},
{
title: "Settings",
route: "/profile?settings=true",
requiresAuth: true,
icon: IconSettings
}
},
created() {
this.activeRoute = window.location.pathname;
}
};
];
watch(route, () => (activeRoute.value = window?.location?.pathname));
</script>
<style lang="scss" scoped>
@import "src/scss/media-queries";
@import "src/scss/media-queries";
.navigation-icons {
display: grid;
grid-column: 1fr;
padding-left: 0;
margin: 0;
background-color: var(--background-color-secondary);
z-index: 15;
width: 100%;
.navigation-icons {
display: grid;
grid-column: 1fr;
padding-left: 0;
margin: 0;
background-color: var(--background-color-secondary);
z-index: 15;
width: 100%;
@include desktop {
grid-template-rows: var(--header-size);
@include desktop {
grid-template-rows: var(--header-size);
}
@include mobile {
grid-template-columns: 1fr 1fr;
}
}
@include mobile {
grid-template-columns: 1fr 1fr;
}
}
</style>

View File

@@ -1,10 +1,10 @@
<template>
<div>
<div class="search" :class="{ active: focusingInput }">
<div class="search" :class="{ active: inputIsActive }">
<IconSearch class="search-icon" tabindex="-1" />
<input
ref="input"
ref="inputElement"
type="text"
placeholder="Search for movie or show"
aria-label="Search input for finding a movie or show"
@@ -13,21 +13,21 @@
tabindex="0"
v-model="query"
@input="handleInput"
@click="focusingInput = true"
@click="focus"
@keydown.escape="handleEscape"
@keyup.enter="handleSubmit"
@keydown.up="navigateUp"
@keydown.down="navigateDown"
@focus="focusingInput = true"
@blur="focusingInput = false"
@focus="focus"
@blur="blur"
/>
<IconClose
tabindex="0"
aria-label="button"
v-if="query && query.length"
@click="resetQuery"
@keydown.enter.stop="resetQuery"
@click="clearInput"
@keydown.enter.stop="clearInput"
class="close-icon"
/>
</div>
@@ -36,246 +36,258 @@
v-if="showAutocompleteResults"
:query="query"
:index="dropdownIndex"
:results.sync="dropdownResults"
v-model:results="dropdownResults"
/>
</div>
</template>
<script>
import { mapActions, mapGetters } from "vuex";
import SeasonedButton from "@/components/ui/SeasonedButton";
import AutocompleteDropdown from "@/components/header/AutocompleteDropdown";
<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 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 type { ListTypes } from "../../interfaces/IList";
import IconSearch from "src/icons/IconSearch";
import IconClose from "src/icons/IconClose";
import config from "@/config";
interface ISearchResult {
title: string;
id: number;
adult: boolean;
type: ListTypes;
}
export default {
name: "SearchInput",
components: {
SeasonedButton,
AutocompleteDropdown,
IconClose,
IconSearch
},
data() {
return {
query: null,
disabled: false,
dropdownIndex: -1,
dropdownResults: [],
focusingInput: false,
showAutocomplete: false
};
},
computed: {
...mapGetters("popup", ["isOpen"]),
showAutocompleteResults() {
return (
!this.disabled &&
this.focusingInput &&
this.query &&
this.query.length > 0
);
}
},
created() {
const params = new URLSearchParams(window.location.search);
if (params && params.has("query")) {
this.query = decodeURIComponent(params.get("query"));
}
const store = useStore();
const router = useRouter();
const route = useRoute();
const elasticUrl = config.ELASTIC_URL;
if (elasticUrl === undefined || elasticUrl === false || elasticUrl === "") {
this.disabled = true;
}
},
methods: {
...mapActions("popup", ["open"]),
navigateDown() {
if (this.dropdownIndex < this.dropdownResults.length - 1) {
this.dropdownIndex++;
}
},
navigateUp() {
if (this.dropdownIndex > -1) this.dropdownIndex--;
const query: Ref<string> = ref(null);
const disabled: Ref<boolean> = ref(false);
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 input = this.$refs.input;
const textLength = input.value.length;
const isOpen = computed(() => store.getters["popup/isOpen"]);
const showAutocompleteResults = computed(() => {
return (
!disabled.value &&
inputIsActive.value &&
query.value &&
query.value.length > 0
);
});
setTimeout(() => {
input.focus();
input.setSelectionRange(textLength, textLength + 1);
}, 1);
},
search() {
const encodedQuery = encodeURI(this.query.replace('/ /g, "+"'));
const params = new URLSearchParams(window.location.search);
if (params && params.has("query")) {
query.value = decodeURIComponent(params.get("query"));
}
this.$router.push({
name: "search",
query: {
...this.$route.query,
query: encodedQuery
}
});
},
resetQuery(event) {
this.query = "";
this.$refs.input.focus();
},
handleInput(e) {
this.$emit("input", this.query);
this.dropdownIndex = -1;
},
handleSubmit() {
if (!this.query || this.query.length == 0) return;
const elasticUrl = config.ELASTIC_URL;
if (elasticUrl === undefined || elasticUrl === "") {
disabled.value = true;
}
if (this.dropdownIndex >= 0) {
const resultItem = this.dropdownResults[this.dropdownIndex];
console.log("resultItem:", resultItem);
this.open({
id: resultItem.id,
type: resultItem.type
});
return;
}
this.search();
this.$refs.input.blur();
this.dropdownIndex = -1;
},
handleEscape() {
if (!this.isOpen) {
this.$refs.input.blur();
this.dropdownIndex = -1;
}
function navigateDown() {
if (dropdownIndex.value < dropdownResults.value.length - 1) {
dropdownIndex.value++;
}
}
};
function navigateUp() {
if (dropdownIndex.value > -1) dropdownIndex.value--;
const textLength = inputElement.value.value.length;
setTimeout(() => {
inputElement.value.focus();
inputElement.value.setSelectionRange(textLength, textLength + 1);
}, 1);
}
function search() {
const encodedQuery = encodeURI(query.value.replace("/ /g", "+"));
router.push({
name: "search",
query: {
...route.query,
query: encodedQuery
}
});
}
function handleInput(e) {
dropdownIndex.value = -1;
}
function handleSubmit() {
if (!query.value || query.value.length == 0) return;
if (dropdownIndex.value >= 0) {
const resultItem = dropdownResults.value[dropdownIndex.value];
store.dispatch("popup/open", {
id: resultItem?.id,
type: resultItem?.type
});
return;
}
search();
reset();
}
function 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();
}
</script>
<style lang="scss" scoped>
@import "src/scss/variables";
@import "src/scss/media-queries";
@import "src/scss/main";
@import "src/scss/variables";
@import "src/scss/media-queries";
@import "src/scss/main";
.close-icon {
position: absolute;
top: calc(50% - 12px);
right: 0;
cursor: pointer;
fill: var(--text-color);
height: 24px;
width: 24px;
.close-icon {
position: absolute;
top: calc(50% - 12px);
right: 0;
cursor: pointer;
fill: var(--text-color);
height: 24px;
width: 24px;
@include tablet-min {
right: 6px;
}
}
.filter {
width: 100%;
display: flex;
flex-direction: column;
margin: 1rem 2rem;
h2 {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
font-weight: 400;
}
&-items {
display: flex;
flex-direction: row;
align-items: center;
> :not(:first-child) {
margin-left: 1rem;
@include tablet-min {
right: 6px;
}
}
}
hr {
display: block;
height: 1px;
border: 0;
border-bottom: 1px solid $text-color-50;
margin-top: 10px;
margin-bottom: 10px;
width: 90%;
}
.search.active {
input {
border-color: var(--color-green);
}
.search-icon {
fill: var(--color-green);
}
}
.search {
height: $header-size;
display: flex;
position: fixed;
flex-wrap: wrap;
z-index: 5;
border: 0;
background-color: $background-color-secondary;
// TODO check if this is for mobile
width: calc(100% - 110px);
top: 0;
right: 55px;
@include tablet-min {
position: relative;
.filter {
width: 100%;
right: 0px;
display: flex;
flex-direction: column;
margin: 1rem 2rem;
h2 {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
font-weight: 400;
}
&-items {
display: flex;
flex-direction: row;
align-items: center;
> :not(:first-child) {
margin-left: 1rem;
}
}
}
input {
hr {
display: block;
width: 100%;
padding: 13px 28px 13px 45px;
outline: none;
margin: 0;
height: 1px;
border: 0;
background-color: $background-color-secondary;
font-weight: 300;
font-size: 18px;
color: $text-color;
border-bottom: 1px solid transparent;
border-bottom: 1px solid $text-color-50;
margin-top: 10px;
margin-bottom: 10px;
width: 90%;
}
&:focus {
// border-bottom: 1px solid var(--color-green);
.search.active {
input {
border-color: var(--color-green);
}
@include tablet-min {
font-size: 24px;
padding: 13px 40px 13px 60px;
.search-icon {
fill: var(--color-green);
}
}
&-icon {
width: 20px;
height: 20px;
fill: var(--text-color-50);
pointer-events: none;
position: absolute;
left: 15px;
top: calc(50% - 10px);
.search {
height: $header-size;
display: flex;
position: fixed;
flex-wrap: wrap;
z-index: 5;
border: 0;
background-color: $background-color-secondary;
// TODO check if this is for mobile
width: calc(100% - 110px);
top: 0;
right: 55px;
@include tablet-min {
width: 24px;
height: 24px;
top: calc(50% - 12px);
left: 22px;
position: relative;
width: 100%;
right: 0px;
}
input {
display: block;
width: 100%;
padding: 13px 28px 13px 45px;
outline: none;
margin: 0;
border: 0;
background-color: $background-color-secondary;
font-weight: 300;
font-size: 18px;
color: $text-color;
border-bottom: 1px solid transparent;
&:focus {
// border-bottom: 1px solid var(--color-green);
border-color: var(--color-green);
}
@include tablet-min {
font-size: 24px;
padding: 13px 40px 13px 60px;
}
}
&-icon {
width: 20px;
height: 20px;
fill: var(--text-color-50);
pointer-events: none;
position: absolute;
left: 15px;
top: calc(50% - 10px);
@include tablet-min {
width: 24px;
height: 24px;
top: calc(50% - 12px);
left: 22px;
}
}
}
}
</style>