🧹 moved files around

This commit is contained in:
2022-07-26 20:18:12 +02:00
parent fe162eb081
commit d585af2193
22 changed files with 83 additions and 157 deletions

View File

@@ -0,0 +1,262 @@
<template>
<transition name="shut">
<ul class="dropdown">
<li
v-for="result in searchResults"
:key="`${result.index}-${result.title}-${result.type}`"
@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" />
<span class="title">{{ result.title }}</span>
</li>
<li
v-if="searchResults.length"
:class="`info di-${searchResults.length}`"
>
<span> Select from list or press enter to search </span>
</li>
</ul>
</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";
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();
}
};
</script>
<style lang="scss" scoped>
@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);
}
}
.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

@@ -0,0 +1,126 @@
<template>
<nav>
<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>
<SearchInput />
<Hamburger class="mobile-only" />
<NavigationIcon class="desktop-only" :route="profileRoute" />
<div class="navigation-icons-grid mobile-only" :class="{ open: isOpen }">
<NavigationIcons>
<NavigationIcon :route="profileRoute" />
</NavigationIcons>
</div>
</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";
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
};
}
}
};
</script>
<style lang="scss" scoped>
@import "src/scss/variables";
@import "src/scss/media-queries";
.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%;
background-color: $background-95;
visibility: hidden;
opacity: 0;
transition: opacity 0.4s ease;
&.open {
opacity: 1;
visibility: visible;
}
}
</style>

View File

@@ -0,0 +1,81 @@
<template>
<router-link
: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>
<span>{{ route.title }}</span>
</li>
</router-link>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
export default {
name: "NavigationIcon",
props: {
active: {
type: String,
required: false
},
route: {
type: Object,
required: true
}
},
computed: {
...mapGetters("user", ["loggedIn"])
}
};
</script>
<style lang="scss" scoped>
@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;
&:hover {
transform: scale(1.05);
}
&:hover,
&.active {
background-color: var(--background-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);
}
}
a {
text-decoration: none;
}
.navigation-icon {
width: 28px;
fill: var(--text-color-70);
transition: inherit;
}
</style>

View File

@@ -0,0 +1,103 @@
<template>
<ul class="navigation-icons">
<NavigationIcon
v-for="route in routes"
:key="route.route"
:route="route"
:active="activeRoute"
/>
<slot></slot>
</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";
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;
}
},
created() {
this.activeRoute = window.location.pathname;
}
};
</script>
<style lang="scss" scoped>
@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%;
@include desktop {
grid-template-rows: var(--header-size);
}
@include mobile {
grid-template-columns: 1fr 1fr;
}
}
</style>

View File

@@ -0,0 +1,281 @@
<template>
<div>
<div class="search" :class="{ active: focusingInput }">
<IconSearch class="search-icon" tabindex="-1" />
<input
ref="input"
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="focusingInput = true"
@keydown.escape="handleEscape"
@keyup.enter="handleSubmit"
@keydown.up="navigateUp"
@keydown.down="navigateDown"
@focus="focusingInput = true"
@blur="focusingInput = false"
/>
<IconClose
tabindex="0"
aria-label="button"
v-if="query && query.length"
@click="resetQuery"
@keydown.enter.stop="resetQuery"
class="close-icon"
/>
</div>
<AutocompleteDropdown
v-if="showAutocompleteResults"
:query="query"
:index="dropdownIndex"
:results.sync="dropdownResults"
/>
</div>
</template>
<script>
import { mapActions, mapGetters } from "vuex";
import SeasonedButton from "@/components/ui/SeasonedButton";
import AutocompleteDropdown from "@/components/header/AutocompleteDropdown";
import IconSearch from "src/icons/IconSearch";
import IconClose from "src/icons/IconClose";
import config from "@/config.json";
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 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 input = this.$refs.input;
const textLength = input.value.length;
setTimeout(() => {
input.focus();
input.setSelectionRange(textLength, textLength + 1);
}, 1);
},
search() {
const encodedQuery = encodeURI(this.query.replace('/ /g, "+"'));
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;
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;
}
}
}
};
</script>
<style lang="scss" scoped>
@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;
@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;
}
}
}
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;
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>