first commit
This commit is contained in:
142
src/components/HomeView/Tabs/Home/HomeMain.vue
Normal file
142
src/components/HomeView/Tabs/Home/HomeMain.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="home">
|
||||
<div class="home__slider-promo">
|
||||
<div class="home__slider-promo-title">Акции</div>
|
||||
<SliderComp type="infinite" :automatic="true">
|
||||
<SliderTabComp>
|
||||
<ImageComp class="home__slider-promo-image" src="/media/images/promo.webp" @click="openModal(0)" />
|
||||
<ModalComp :ref="(elem: any) => {refModal.push(elem as InstanceType<typeof ModalComp>)}">
|
||||
<ImageComp class="home__slider-promo-image" src="/media/images/promo.webp" />
|
||||
<div class="home__slider-promo-text">Текст акции 1</div>
|
||||
</ModalComp>
|
||||
</SliderTabComp>
|
||||
<SliderTabComp>
|
||||
<ImageComp class="home__slider-promo-image" src="/media/images/promo.webp" @click="openModal(1)" />
|
||||
<ModalComp :ref="(elem: any) => {refModal.push(elem as InstanceType<typeof ModalComp>)}">
|
||||
<ImageComp class="home__slider-promo-image" src="/media/images/promo.webp" />
|
||||
<div class="home__slider-promo-text" home__slider-image>Текст акции 2</div>
|
||||
</ModalComp>
|
||||
</SliderTabComp>
|
||||
<SliderTabComp>
|
||||
<ImageComp class="home__slider-promo-image" src="/media/images/promo.webp" @click="openModal(2)" />
|
||||
<ModalComp :ref="(elem: any) => {refModal.push(elem as InstanceType<typeof ModalComp>)}">
|
||||
<ImageComp class="home__slider-promo-image" src="/media/images/promo.webp" />
|
||||
<div class="home__slider-promo-text">Текст акции 3</div>
|
||||
</ModalComp>
|
||||
</SliderTabComp>
|
||||
</SliderComp>
|
||||
</div>
|
||||
<div class="home__slider" v-if="ordersStore.orders.length > 0">
|
||||
<div class="home__slider-title">Последние заказы</div>
|
||||
<div class="home__slider-list">
|
||||
<div class="home__slider-item" v-for="order in ordersStore.orders.sort((a, b) => b.id - a.id)" :key="order.id">
|
||||
<div>Заказ № {{ order.id }}</div>
|
||||
<div>Стол: {{ order.table }}</div>
|
||||
<div>Тяжесть: {{ order.heaviness.title }}</div>
|
||||
<div>Вкус: {{ order.tastes.map((x) => x.title).join(", ") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="home__slider-footer"></div>
|
||||
</div>
|
||||
<div class="home__slider" v-if="ordersStore.favouriteOrders.length > 0">
|
||||
<div class="home__slider-title">Любимые кальяны</div>
|
||||
<div class="home__slider-list">
|
||||
<div class="home__slider-item" v-for="order in ordersStore.favouriteOrders.sort((a, b) => b.id - a.id)" :key="order.id">
|
||||
<div>Тяжесть: {{ order.heaviness.title }}</div>
|
||||
<div>Вкус: {{ order.tastes.map((x) => x.title).join(", ") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="home__slider-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useOrdersStore } from "@/store";
|
||||
import { ref } from "vue";
|
||||
import ModalComp from "@/components/UI/ModalComp.vue";
|
||||
|
||||
const refModal = ref<InstanceType<typeof ModalComp>[]>([]);
|
||||
|
||||
const ordersStore = useOrdersStore();
|
||||
|
||||
function openModal(index: number) {
|
||||
refModal.value[index].open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.home {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: r(20);
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
padding: r(20) 0;
|
||||
|
||||
&__slider-promo {
|
||||
background: color("main-background");
|
||||
box-shadow: 0 r(2) r(2) 0 rgba(0, 0, 0, 0.14), 0 r(3) r(1) r(-2) rgba(0, 0, 0, 0.2), 0 r(1) r(5) 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
&__slider-promo-title {
|
||||
font-size: r(25);
|
||||
padding: r(10) r(20);
|
||||
}
|
||||
|
||||
&__slider-promo-image {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__slider-promo-text {
|
||||
margin-top: r(20);
|
||||
}
|
||||
|
||||
&__slider-title {
|
||||
padding: r(10) r(30);
|
||||
//background: color("main-background");
|
||||
//box-shadow: 0 r(2) r(2) 0 rgba(0, 0, 0, 0.14), 0 r(3) r(1) r(-2) rgba(0, 0, 0, 0.2), 0 r(1) r(5) 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
&__slider {
|
||||
@include main-container;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: r(1);
|
||||
background: color("main-background");
|
||||
}
|
||||
|
||||
&__slider-list {
|
||||
display: flex;
|
||||
gap: r(20);
|
||||
padding: r(5) r(20);
|
||||
overflow-x: auto;
|
||||
|
||||
@media (max-width: $mobileWidth) {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__slider-footer {
|
||||
height: r(15);
|
||||
//background: color("main-background");
|
||||
//box-shadow: 0 r(2) r(2) 0 rgba(0, 0, 0, 0.14), 0 r(3) r(1) r(-2) rgba(0, 0, 0, 0.2), 0 r(1) r(5) 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
&__slider-item {
|
||||
@include main-container;
|
||||
|
||||
min-width: 60%;
|
||||
padding: r(10);
|
||||
box-shadow: 0 r(2) r(2) 0 rgba(0, 0, 0, 0.34), 0 r(3) r(1) r(-2) rgba(0, 0, 0, 0.5), 0 r(1) r(5) 0 rgba(0, 0, 0, 0.32);
|
||||
}
|
||||
}
|
||||
</style>
|
62
src/components/HomeView/Tabs/Hookah/HeavinessTab.vue
Normal file
62
src/components/HomeView/Tabs/Hookah/HeavinessTab.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div class="hookah-heaviness">
|
||||
<div class="hookah-heaviness__container">
|
||||
<div
|
||||
class="hookah-heaviness__item"
|
||||
v-for="item in hookahStore.heavinessList"
|
||||
:key="item.title"
|
||||
:class="{ 'hookah-heaviness__item--active': hookahStore.selectedHeaviness === item }"
|
||||
@click="changeHeaviness(item)"
|
||||
>
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useHookahStore } from "@/store";
|
||||
import { heaviness } from "@/types";
|
||||
|
||||
const hookahStore = useHookahStore();
|
||||
|
||||
function changeHeaviness(heaviness: heaviness) {
|
||||
hookahStore.selectedHeaviness = heaviness;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.hookah-heaviness {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&__container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: r(10);
|
||||
width: 80%;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
&__item {
|
||||
@include main-container;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: r(10) r(10);
|
||||
cursor: pointer;
|
||||
transition: 0.18s ease;
|
||||
transition-property: background-color, color, box-shadow;
|
||||
|
||||
&--active {
|
||||
background: color("theme-color");
|
||||
color: color("ui-color-on-background");
|
||||
box-shadow: 0 r(2) r(2) 0 color("theme-color", $alpha: -0.86), 0 r(3) r(1) r(-2) color("theme-color", $alpha: -0.8),
|
||||
0 r(1) r(5) 0 color("theme-color", $alpha: -0.88);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
416
src/components/HomeView/Tabs/Hookah/HookahMain.vue
Normal file
416
src/components/HomeView/Tabs/Hookah/HookahMain.vue
Normal file
@@ -0,0 +1,416 @@
|
||||
<template>
|
||||
<template v-if="userStore.isLoggedIn">
|
||||
<div class="hookah" :class="'hookah--transition-direction--' + direction">
|
||||
<div class="hookah__navigation">
|
||||
<div class="hookah__navigation-title">Создаём заказ. {{ currentTabIndex < 3 ? "Выберите..." : "Проверьте..." }}</div>
|
||||
<div class="hookah__navigation-buttons">
|
||||
<RouterLink
|
||||
class="hookah__navigation-item hookah-navigation-item"
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="tab.name"
|
||||
:to="tab.path"
|
||||
active-class="hookah-navigation-item--active"
|
||||
:class="{ 'hookah-navigation-item--completed': isTabCompleted(index) }"
|
||||
@click.prevent="changeTab(index)"
|
||||
>
|
||||
<div class="hookah-navigation-item__side" v-html="getSideShape('rect')" v-if="index === 0"></div>
|
||||
<div class="hookah-navigation-item__side" v-html="getSideShape('left')" v-else></div>
|
||||
<div class="hookah-navigation-item__text">{{ tab.name }}</div>
|
||||
<div class="hookah-navigation-item__side" v-html="getSideShape('rect')" v-if="index === tabs.length - 1"></div>
|
||||
<div class="hookah-navigation-item__side" v-html="getSideShape('right')" v-else></div>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hookah__view">
|
||||
<RouterView v-slot="{ Component }" class="hookah__page">
|
||||
<Transition name="hookah--transition">
|
||||
<Component :is="Component" />
|
||||
</Transition>
|
||||
</RouterView>
|
||||
|
||||
<Transition name="hookah__view-button-container--transition">
|
||||
<div class="hookah__view-button-container" v-show="navigationButtonShowed" @click="nextButtonClicked()">
|
||||
<ButtonComp class="hookah__view-button" type="primary" value="Далее" />
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="num in 3"
|
||||
:key="num"
|
||||
class="hookah__background-smoke"
|
||||
:class="`hookah__background-smoke--${num}`"
|
||||
:style="`opacity: ${getSmokeOpacity(num)}`"
|
||||
></div>
|
||||
<div class="hookah__background-table" :style="`opacity: ${backgroundTableOpacity}`">
|
||||
{{ backgroundTableNumber }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<AuthorizationNeeded />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, ref, watch } from "vue";
|
||||
import AuthorizationNeeded from "@/components/HomeView/AuthorizationNeeded.vue";
|
||||
import { onBeforeRouteUpdate, useRoute, useRouter } from "vue-router";
|
||||
import { useHookahStore, useUIStore, useUserStore } from "@/store";
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const hookahStore = useHookahStore();
|
||||
const userStore = useUserStore();
|
||||
const uiStore = useUIStore();
|
||||
|
||||
const html = (val: any) => val;
|
||||
|
||||
const tabs = [
|
||||
{ name: "Стол", path: "/hookah/table" },
|
||||
{ name: "Тяжесть", path: "/hookah/heaviness" },
|
||||
{ name: "Вкус", path: "/hookah/taste" },
|
||||
{ name: "Итог", path: "/hookah/summary" },
|
||||
];
|
||||
|
||||
const direction = ref("none");
|
||||
const currentTabIndex = computed(() => {
|
||||
return getTabIndexByPath(route.path);
|
||||
});
|
||||
|
||||
init();
|
||||
function init() {
|
||||
router.push(getPreviousNotCompletedTab(3)?.path ?? tabs[3].path);
|
||||
}
|
||||
onActivated(() => init());
|
||||
|
||||
function getTabIndexByPath(path: string) {
|
||||
let tab = tabs.find((x) => path === x.path);
|
||||
return tab ? tabs.indexOf(tab) : -1;
|
||||
}
|
||||
|
||||
function changeTab(index: number) {
|
||||
if (currentTabIndex.value === index) return;
|
||||
|
||||
if (currentTabIndex.value > index) {
|
||||
direction.value = "left";
|
||||
} else {
|
||||
direction.value = "right";
|
||||
}
|
||||
|
||||
router.push(tabs[index].path);
|
||||
|
||||
setTimeout(() => {
|
||||
direction.value = "none";
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function getSideShape(type: "right" | "left" | "rect") {
|
||||
switch (type) {
|
||||
case "right":
|
||||
return html`
|
||||
<svg class="hookah-navigation-item__shape" viewbox="0 0 55 100">
|
||||
<polygon points="0,0 5,0 55,50 5,100 0,100" />
|
||||
</svg>
|
||||
`;
|
||||
case "left":
|
||||
return html`
|
||||
<svg class="hookah-navigation-item__shape" viewbox="0 0 55 100">
|
||||
<polygon points="0,0 55,0 55,100 0,100 50,50" />
|
||||
</svg>
|
||||
`;
|
||||
case "rect":
|
||||
return html`
|
||||
<svg class="hookah-navigation-item__shape" viewbox="0 0 51 100">
|
||||
<polygon points="1,0 51,0 51,100 1,100" />
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const navigationButtonLeftShowed = computed(() => {
|
||||
if (direction.value != "none") return false;
|
||||
return currentTabIndex.value > 0;
|
||||
});
|
||||
|
||||
const navigationButtonRightShowed = computed(() => {
|
||||
if (currentTabIndex.value >= tabs.length - 1) return false;
|
||||
if (direction.value != "none") return false;
|
||||
|
||||
return isTabCompleted(currentTabIndex.value);
|
||||
});
|
||||
|
||||
const navigationButtonShowed = computed(() => {
|
||||
if (currentTabIndex.value >= tabs.length - 1) return false;
|
||||
if (direction.value !== "none") return false;
|
||||
|
||||
return isTabCompleted(currentTabIndex.value);
|
||||
});
|
||||
|
||||
function nextButtonClicked() {
|
||||
const nextIncompletedTab = getNextNotCompletedTab(currentTabIndex.value);
|
||||
changeTab(tabs.indexOf(nextIncompletedTab));
|
||||
}
|
||||
|
||||
function isTabCompleted(index: number) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return hookahStore.selectedTableNumber > 0;
|
||||
case 1:
|
||||
return hookahStore.selectedHeaviness != null;
|
||||
case 2:
|
||||
return hookahStore.selectedTastes.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
function getPreviousNotCompletedTab(index: number) {
|
||||
for (let i = 0; i < index; i++) {
|
||||
if (!isTabCompleted(i)) {
|
||||
return tabs[i];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getNextNotCompletedTab(index: number) {
|
||||
for (let i = index; i < 3; i++) {
|
||||
if (!isTabCompleted(i)) {
|
||||
return tabs[i];
|
||||
}
|
||||
}
|
||||
return tabs[3];
|
||||
}
|
||||
|
||||
function getSmokeOpacity(num: number) {
|
||||
if (hookahStore.selectedHeaviness && hookahStore.selectedHeaviness.id >= num - 1) {
|
||||
return uiStore.theme === "light" ? 1 : 0.1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const backgroundTableOpacity = ref(0);
|
||||
const backgroundTableNumber = ref(0);
|
||||
|
||||
let backgroundTableTimeout: number;
|
||||
|
||||
function changeBackgroundTableNumber(to: number) {
|
||||
clearTimeout(backgroundTableTimeout);
|
||||
backgroundTableOpacity.value = 0;
|
||||
backgroundTableTimeout = setTimeout(() => {
|
||||
backgroundTableNumber.value = to;
|
||||
backgroundTableOpacity.value = to > 0 ? 1 : 0;
|
||||
}, 380);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => hookahStore.selectedTableNumber,
|
||||
(to) => {
|
||||
changeBackgroundTableNumber(to);
|
||||
}
|
||||
);
|
||||
|
||||
onBeforeRouteUpdate((to) => {
|
||||
const tab = getPreviousNotCompletedTab(getTabIndexByPath(to.path));
|
||||
if (tab && to.path !== tab.path) {
|
||||
return tab.path;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.hookah {
|
||||
$p: &;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__navigation {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: r(10) 0;
|
||||
gap: r(10);
|
||||
|
||||
background: color("main-background");
|
||||
box-shadow: 0 r(2) r(2) 0 rgba(0, 0, 0, 0.14), 0 r(3) r(1) r(-2) rgba(0, 0, 0, 0.2), 0 r(1) r(5) 0 rgba(0, 0, 0, 0.12);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
&__navigation-buttons {
|
||||
--height: 4rem;
|
||||
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: var(--height);
|
||||
}
|
||||
|
||||
&__navigation-item {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
&__view {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
&__view-button-container {
|
||||
position: absolute;
|
||||
inset: auto 0 r(40) 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
color: color("ui-color");
|
||||
cursor: pointer;
|
||||
|
||||
&--transition {
|
||||
&-enter-active,
|
||||
&-leave-active {
|
||||
transition: opacity 0.18s ease;
|
||||
}
|
||||
|
||||
&-enter-from,
|
||||
&-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__view-button {
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
&__page {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
padding: r(20);
|
||||
}
|
||||
|
||||
&__background-smoke {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("@/assets/images/smoke.webp");
|
||||
background-size: r(1200);
|
||||
background-repeat: no-repeat;
|
||||
transition: opacity 0.38s ease;
|
||||
|
||||
&--1 {
|
||||
background-position: 50% 40vh;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
&--2 {
|
||||
background-position: 25% 10vh;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&--3 {
|
||||
background-position: 75% -20vh;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__background-table {
|
||||
position: absolute;
|
||||
inset: auto 0 0 auto;
|
||||
padding: r(20);
|
||||
|
||||
color: transparent;
|
||||
font-size: 30vh;
|
||||
font-style: italic;
|
||||
text-shadow: 0 0 r(8) color("ui-color", $alpha: -0.9);
|
||||
|
||||
transition: opacity 0.38s ease;
|
||||
z-index: 4;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&--transition-direction {
|
||||
&--left {
|
||||
#{$p}--transition {
|
||||
&-enter-from {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
&-leave-to {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--right {
|
||||
#{$p}--transition {
|
||||
&-enter-from {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
&-leave-to {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--transition {
|
||||
&-enter-active,
|
||||
&-leave-active {
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hookah-navigation-item {
|
||||
$p: &;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&__side {
|
||||
height: var(--height);
|
||||
width: calc(var(--height + r(5)) / 2);
|
||||
margin: 0 r(-1);
|
||||
}
|
||||
|
||||
&__shape {
|
||||
display: block;
|
||||
height: 100%;
|
||||
fill: color("ui-background");
|
||||
transition: fill 0.18s ease;
|
||||
}
|
||||
|
||||
&__text {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
background: color("ui-background");
|
||||
color: color("ui-color-on-background");
|
||||
font-size: r(24);
|
||||
transition: background-color 0.18s ease;
|
||||
}
|
||||
|
||||
&--completed {
|
||||
#{$p}__text {
|
||||
background: color("theme-color", $saturation: -50%);
|
||||
}
|
||||
|
||||
#{$p}__shape {
|
||||
fill: color("theme-color", $saturation: -50%);
|
||||
}
|
||||
}
|
||||
|
||||
&--active {
|
||||
#{$p}__text {
|
||||
background: color("theme-color");
|
||||
}
|
||||
|
||||
#{$p}__shape {
|
||||
fill: color("theme-color");
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
68
src/components/HomeView/Tabs/Hookah/SummaryTab.vue
Normal file
68
src/components/HomeView/Tabs/Hookah/SummaryTab.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div class="hookah-summary">
|
||||
<div class="hookah-summary__container">
|
||||
<div>Стол: {{ hookahStore.selectedTableNumber }}</div>
|
||||
<div>Тяжесть: {{ hookahStore.selectedHeaviness?.title }}</div>
|
||||
<div>Вкус: {{ hookahStore.selectedTastes.map((x) => x.title).join(", ") }}</div>
|
||||
<div>Цена: {{ 123 }}</div>
|
||||
<ButtonComp :type="'primary'" :value="'Заказать'" @click="createOrder()" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useHookahStore, useOrdersStore } from "@/store";
|
||||
import { useOrdersApi } from "@/composables/api";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const hookahStore = useHookahStore();
|
||||
const ordersStore = useOrdersStore();
|
||||
const ordersApi = useOrdersApi();
|
||||
const router = useRouter();
|
||||
|
||||
async function createOrder() {
|
||||
const order = await tryCreateOrder();
|
||||
if (order) {
|
||||
ordersStore.createOrder(order);
|
||||
setTimeout(() => {
|
||||
hookahStore.clearSelected();
|
||||
}, 200);
|
||||
|
||||
router.push("/orders");
|
||||
}
|
||||
}
|
||||
|
||||
async function tryCreateOrder() {
|
||||
if (hookahStore.selectedHeaviness == null) return;
|
||||
|
||||
const resp = await ordersApi.createOrder({
|
||||
table: hookahStore.selectedTableNumber,
|
||||
heavinessId: hookahStore.selectedHeaviness.id,
|
||||
tasteIds: hookahStore.selectedTastes.map((x) => x.id),
|
||||
});
|
||||
|
||||
return resp;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.hookah-summary {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&__container {
|
||||
@include main-container;
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: r(10);
|
||||
width: 80%;
|
||||
padding: r(15);
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
</style>
|
62
src/components/HomeView/Tabs/Hookah/TableTab.vue
Normal file
62
src/components/HomeView/Tabs/Hookah/TableTab.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div class="hookah-table">
|
||||
<div class="hookah-table__container">
|
||||
<div
|
||||
class="hookah-table__item"
|
||||
:class="{ 'hookah-table__item--active': hookahStore.selectedTableNumber === num }"
|
||||
v-for="num in 15"
|
||||
:key="num"
|
||||
@click="setTable(num)"
|
||||
>
|
||||
{{ num }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useHookahStore } from "@/store";
|
||||
|
||||
const hookahStore = useHookahStore();
|
||||
|
||||
function setTable(num: number) {
|
||||
hookahStore.selectedTableNumber = num;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.hookah-table {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
flex-wrap: wrap;
|
||||
gap: r(10);
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
&__item {
|
||||
@include main-container;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: r(45);
|
||||
height: r(45);
|
||||
cursor: pointer;
|
||||
transition: 0.18s ease;
|
||||
transition-property: background-color, color, box-shadow;
|
||||
|
||||
&--active {
|
||||
background: color("theme-color");
|
||||
color: color("ui-color-on-background");
|
||||
box-shadow: 0 r(2) r(2) 0 color("theme-color", $alpha: -0.86), 0 r(3) r(1) r(-2) color("theme-color", $alpha: -0.8),
|
||||
0 r(1) r(5) 0 color("theme-color", $alpha: -0.88);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
73
src/components/HomeView/Tabs/Hookah/TasteTab.vue
Normal file
73
src/components/HomeView/Tabs/Hookah/TasteTab.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="hookah-taste">
|
||||
<div class="hookah-taste__container">
|
||||
<div
|
||||
class="hookah-taste__item"
|
||||
:class="{ 'hookah-taste__item--active': hookahStore.selectedTastes.includes(item) }"
|
||||
v-for="item in hookahStore.tastesList"
|
||||
:key="item.id"
|
||||
@click="toggleTaste(item)"
|
||||
>
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useHookahStore } from "@/store";
|
||||
import { taste } from "@/types";
|
||||
|
||||
const hookahStore = useHookahStore();
|
||||
|
||||
let maxSelectedTastesCount = 2;
|
||||
|
||||
function toggleTaste(taste: taste) {
|
||||
if (hookahStore.selectedTastes.includes(taste)) {
|
||||
hookahStore.selectedTastes = hookahStore.selectedTastes.filter((x) => x !== taste);
|
||||
} else {
|
||||
if (hookahStore.selectedTastes.length < maxSelectedTastesCount) {
|
||||
hookahStore.selectedTastes.push(taste);
|
||||
} else {
|
||||
console.log("can't select more than ", maxSelectedTastesCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.hookah-taste {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&__container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: r(10);
|
||||
width: 80%;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
&__item {
|
||||
@include main-container;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: r(5) r(10);
|
||||
cursor: pointer;
|
||||
transition: 0.18s ease;
|
||||
transition-property: background-color, color, box-shadow;
|
||||
|
||||
&--active {
|
||||
background: color("theme-color");
|
||||
color: color("ui-color-on-background");
|
||||
box-shadow: 0 r(2) r(2) 0 color("theme-color", $alpha: -0.86), 0 r(3) r(1) r(-2) color("theme-color", $alpha: -0.8),
|
||||
0 r(1) r(5) 0 color("theme-color", $alpha: -0.88);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
75
src/components/HomeView/Tabs/Menu/MenuItem.vue
Normal file
75
src/components/HomeView/Tabs/Menu/MenuItem.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="menu-item" @click="openModal()">
|
||||
<ImageComp class="menu-item__image" src="/media/images/tobaco1.webp" />
|
||||
<div class="menu-item__title">{{ item.title }}</div>
|
||||
<div class="menu-item__description">{{ item.description }}</div>
|
||||
<div class="menu-item__price">
|
||||
{{ item.price }}
|
||||
<FontAwesomeIcon class="menu-item__price-icon" icon="ruble-sign" />
|
||||
</div>
|
||||
<ModalComp ref="refModal">
|
||||
<div class="menu-item__modal">
|
||||
<ImageComp class="menu-item__image" src="/media/images/tobaco1.webp" />
|
||||
<div class="menu-item__title">{{ item.title }}</div>
|
||||
<div class="menu-item__description">{{ item.description }}</div>
|
||||
<div class="menu-item__price">
|
||||
{{ item.price }}
|
||||
<FontAwesomeIcon class="menu-item__price-icon" icon="ruble-sign" />
|
||||
</div>
|
||||
</div>
|
||||
</ModalComp>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { menuItem } from "@/types";
|
||||
import { onDeactivated, ref } from "vue";
|
||||
import ModalComp from "@/components/UI/ModalComp.vue";
|
||||
|
||||
const refModal = ref<InstanceType<typeof ModalComp> | undefined>();
|
||||
const props = defineProps<{ item: menuItem }>();
|
||||
|
||||
onDeactivated(() => {
|
||||
refModal.value?.close();
|
||||
});
|
||||
|
||||
function openModal() {
|
||||
refModal.value?.open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.menu-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: r(10);
|
||||
width: 90%;
|
||||
margin: r(10) 0;
|
||||
padding: r(15);
|
||||
border-radius: r(6);
|
||||
background: color("main-background");
|
||||
box-shadow: 0 r(2) r(2) 0 rgba(0, 0, 0, 0.14), 0 r(3) r(1) r(-2) rgba(0, 0, 0, 0.2), 0 r(1) r(5) 0 rgba(0, 0, 0, 0.12);
|
||||
|
||||
&__image {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: r(23);
|
||||
}
|
||||
|
||||
&__price {
|
||||
font-size: r(23);
|
||||
}
|
||||
|
||||
&__price-icon {
|
||||
font-size: r(20);
|
||||
}
|
||||
|
||||
&__modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: r(10);
|
||||
}
|
||||
}
|
||||
</style>
|
163
src/components/HomeView/Tabs/Menu/MenuMain.vue
Normal file
163
src/components/HomeView/Tabs/Menu/MenuMain.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<div class="menu">
|
||||
<div class="menu__navigation">
|
||||
<div class="menu__navigation-container">
|
||||
<div
|
||||
class="menu__navigation-item"
|
||||
v-for="category in menuStore.categories"
|
||||
:class="{ 'menu__navigation-item--active': selectedCategory === category }"
|
||||
:key="category.id"
|
||||
@click="changeCategory(category)"
|
||||
:ref="(el)=>refCategories.push(el as Element)"
|
||||
>
|
||||
{{ category.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu__list" ref="refMenuList">
|
||||
<TransitionGroup name="menu__list-item--transition">
|
||||
<MenuItem
|
||||
class="menu__list-item"
|
||||
v-for="item in selectedMenuItems"
|
||||
:key="Math.random() * (item.id + 1)"
|
||||
:item="item"
|
||||
></MenuItem>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { category } from "@/types";
|
||||
import { useMenuApi } from "@/composables/api";
|
||||
import { useMenuStore } from "@/store";
|
||||
import MenuItem from "./MenuItem.vue";
|
||||
|
||||
const menuStore = useMenuStore();
|
||||
const menuApi = useMenuApi();
|
||||
|
||||
const refCategories = ref<Element[]>([]);
|
||||
const refMenuList = ref<Element>();
|
||||
|
||||
init();
|
||||
function init() {
|
||||
menuStore.categories = menuApi.getCategories();
|
||||
menuStore.menuItems = menuApi.getMenuItems();
|
||||
}
|
||||
|
||||
const selectedMenuItems = computed(() =>
|
||||
selectedCategory.value == null ? menuStore.menuItems : menuStore.menuItems.filter((x) => x.categoryId === selectedCategory.value?.id)
|
||||
);
|
||||
const selectedCategory = ref<category | undefined>(undefined);
|
||||
|
||||
function changeCategory(category: category) {
|
||||
if (selectedCategory.value === category) {
|
||||
selectedCategory.value = undefined;
|
||||
} else {
|
||||
selectedCategory.value = category;
|
||||
const selectedCategoryElement = refCategories.value[menuStore.categories.indexOf(selectedCategory.value)];
|
||||
selectedCategoryElement.scrollIntoView({ behavior: "smooth", block: "center", inline: "center" });
|
||||
}
|
||||
setTimeout(() => {
|
||||
refMenuList.value?.scroll(0, 0);
|
||||
}, 180);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.menu {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
&__navigation {
|
||||
position: relative;
|
||||
flex: 0 1 auto;
|
||||
background: color("main-background");
|
||||
box-shadow: 0 r(4) r(18) 0 rgba(0, 0, 0, 0.12), 0 r(7) r(10) r(-5) rgba(0, 0, 0, 0.15);
|
||||
z-index: 1;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&__navigation-container {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
padding: 0 r(35);
|
||||
|
||||
@media (max-width: $mobileWidth) {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__navigation-item {
|
||||
position: relative;
|
||||
padding: r(15);
|
||||
font-size: r(24);
|
||||
font-weight: 300;
|
||||
text-transform: capitalize;
|
||||
white-space: nowrap;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: r(4);
|
||||
border-radius: r(2);
|
||||
background: color("theme-color");
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
transition: opacity 0.18s ease;
|
||||
}
|
||||
|
||||
&--active {
|
||||
&::after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__list {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: r(10) 0 r(20) 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&__list-item {
|
||||
flex: 0 0 auto;
|
||||
|
||||
--transition-duration: 0.18s;
|
||||
|
||||
&--transition {
|
||||
&-enter-active {
|
||||
transition: opacity var(--transition-duration) var(--transition-duration) ease;
|
||||
}
|
||||
&-leave-active {
|
||||
transition: opacity var(--transition-duration) ease;
|
||||
}
|
||||
|
||||
&-enter-from,
|
||||
&-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
159
src/components/HomeView/Tabs/Orders/OrdersMain.vue
Normal file
159
src/components/HomeView/Tabs/Orders/OrdersMain.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<template v-if="userStore.isLoggedIn">
|
||||
<div class="orders">
|
||||
<div class="orders__list">
|
||||
<div class="orders__item" v-for="(order, index) in ordersStore.orders" :key="order.id">
|
||||
<div class="orders__item-main">
|
||||
<div class="orders__item-head">
|
||||
<div>Заказ № {{ order.id }}</div>
|
||||
<div>статус: {{ order.status }}</div>
|
||||
</div>
|
||||
<div>Стол: {{ order.table }}</div>
|
||||
<div>Тяжесть: {{ order.heaviness.title }}</div>
|
||||
<div>Вкус: {{ order.tastes.map((x) => x.title).join(", ") }}</div>
|
||||
<div>Цена: {{ order.price }}</div>
|
||||
<div
|
||||
class="orders__item-favourite"
|
||||
:class="{ 'orders__item-favourite--active': order.favourite }"
|
||||
@click="toggleFavourite(order)"
|
||||
>
|
||||
<FontAwesomeIcon icon="star" />
|
||||
</div>
|
||||
<FontAwesomeIcon
|
||||
class="orders__item-expand"
|
||||
:class="{ 'orders__item-expand--active': index === expandedIndex }"
|
||||
icon="chevron-down"
|
||||
@click="toggleExpanded(index)"
|
||||
/>
|
||||
</div>
|
||||
<div class="orders__item-add" v-show="index === expandedIndex">
|
||||
<ButtonComp type="primary" value="Повторить заказ" @click="repeatOrder(order)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="orders__item" v-if="ordersStore.orders.length === 0">
|
||||
У вас пока нет ни одного заказа...
|
||||
<RouterLink class="orders__item-link" to="/hookah/table">Сделать заказ</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<AuthorizationNeeded />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AuthorizationNeeded from "@/components/HomeView/AuthorizationNeeded.vue";
|
||||
import { useOrdersApi } from "@/composables/api";
|
||||
import { useUserStore, useOrdersStore, useHookahStore } from "@/store";
|
||||
import { order } from "@/types";
|
||||
import { ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const ordersStore = useOrdersStore();
|
||||
const hookahStore = useHookahStore();
|
||||
const ordersApi = useOrdersApi();
|
||||
const router = useRouter();
|
||||
|
||||
const expandedIndex = ref(-1);
|
||||
|
||||
async function toggleFavourite(order: order) {
|
||||
const resp = await ordersApi.toggleFavourite(order);
|
||||
|
||||
if (resp) {
|
||||
order.favourite = !order.favourite;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleExpanded(index: number) {
|
||||
if (index === expandedIndex.value) {
|
||||
expandedIndex.value = -1;
|
||||
} else {
|
||||
expandedIndex.value = index;
|
||||
}
|
||||
}
|
||||
|
||||
function repeatOrder(order: order) {
|
||||
hookahStore.selectedHeaviness = order.heaviness;
|
||||
hookahStore.selectedTastes = order.tastes;
|
||||
router.push("/hookah/table");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.orders {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
padding: r(20);
|
||||
|
||||
&__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: r(20);
|
||||
}
|
||||
|
||||
&__item {
|
||||
@include main-container;
|
||||
|
||||
position: relative;
|
||||
padding: r(10);
|
||||
}
|
||||
|
||||
&__item-main {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: r(5);
|
||||
}
|
||||
|
||||
&__item-add {
|
||||
margin-top: r(10);
|
||||
}
|
||||
|
||||
&__item-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: calc(100% - r(30));
|
||||
}
|
||||
|
||||
&__item-favourite {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: r(20);
|
||||
height: r(20);
|
||||
color: color("ui-background");
|
||||
transition: color 0.18s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&--active {
|
||||
color: color("theme-color");
|
||||
}
|
||||
}
|
||||
|
||||
&__item-expand {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: r(20);
|
||||
height: r(20);
|
||||
color: color("ui-background");
|
||||
transition: transform 0.18s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&--active {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__item-link {
|
||||
color: color("theme-color");
|
||||
}
|
||||
}
|
||||
</style>
|
340
src/components/HomeView/Tabs/User/UserMain.vue
Normal file
340
src/components/HomeView/Tabs/User/UserMain.vue
Normal file
@@ -0,0 +1,340 @@
|
||||
<template>
|
||||
<template v-if="userStore.isLoggedIn">
|
||||
<div class="user">
|
||||
<div class="user__container">
|
||||
<div class="user__user-data">
|
||||
<SwitchableImage
|
||||
class="user__image"
|
||||
:value="localUserData.image"
|
||||
:edited="userDataEdited"
|
||||
@change="changeLocalImageValue"
|
||||
/>
|
||||
<InputComp
|
||||
class="user__name"
|
||||
v-model="localUserData.name"
|
||||
placeholder="Имя"
|
||||
:hasWarning="fieldHasErrors('Имя')"
|
||||
:readOnly="!userDataEdited"
|
||||
></InputComp>
|
||||
<InputComp
|
||||
class="user__surname"
|
||||
v-model="localUserData.surname"
|
||||
placeholder="Фамилия"
|
||||
:hasWarning="fieldHasErrors('Фамилия')"
|
||||
:readOnly="!userDataEdited"
|
||||
></InputComp>
|
||||
|
||||
<InputComp class="user__email" v-model="userStore.userData.email" :placeholder="'Почта'" :readOnly="true"></InputComp>
|
||||
|
||||
<ButtonComp
|
||||
class="user__button"
|
||||
v-if="userDataEdited"
|
||||
value="Сохранить изменения"
|
||||
type="primary"
|
||||
@click="tryChangeUserData()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="user__password" v-if="userDataEdited">
|
||||
<template v-if="userPasswordEdited">
|
||||
<InputComp
|
||||
class="user__name"
|
||||
v-model="localPasswords.oldPassword"
|
||||
type="password"
|
||||
:hasWarning="fieldHasErrors('Старый пароль')"
|
||||
placeholder="Старый пароль"
|
||||
></InputComp>
|
||||
<InputComp
|
||||
class="user__surname"
|
||||
v-model="localPasswords.newPassword"
|
||||
type="password"
|
||||
:hasWarning="fieldHasErrors('Новый пароль')"
|
||||
placeholder="Новый пароль"
|
||||
></InputComp>
|
||||
<InputComp
|
||||
class="user__surname"
|
||||
v-model="localPasswords.newPassword2"
|
||||
type="password"
|
||||
:hasWarning="fieldHasErrors('Новый пароль')"
|
||||
placeholder="Повторите новый пароль"
|
||||
></InputComp>
|
||||
</template>
|
||||
|
||||
<ButtonComp
|
||||
class="user__button"
|
||||
value="Сменить пароль"
|
||||
:type="passwordButtonStyle"
|
||||
@click="changePasswordButtonHandler()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="user__edit-toggle">
|
||||
<FontAwesomeIcon class="fa-icon" icon="user-pen" v-if="!userDataEdited" @click="startEdit()" />
|
||||
<FontAwesomeIcon class="fa-icon" icon="xmark" v-else @click="stopEdit()" />
|
||||
</div>
|
||||
|
||||
<ButtonComp class="user__button" v-if="!userDataEdited" value="Выйти" type="primary" @click="tryLogout()" />
|
||||
<ThemeSwitcher class="user__theme-switcher" />
|
||||
|
||||
<NotificationsMain class="user__notifications" v-model="notifications" />
|
||||
<ModalComp ref="refModal">Вы действительно хотите выйти?</ModalComp>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<AuthorizationNeeded />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onDeactivated, ref, watch } from "vue";
|
||||
import SwitchableImage from "@/components/Common/SwitchableImage.vue";
|
||||
import AuthorizationNeeded from "@/components/HomeView/AuthorizationNeeded.vue";
|
||||
import NotificationsMain from "@/components/Notifications/NotificationsMain.vue";
|
||||
import { useUserStore } from "@/store";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useUserApi } from "@/composables/api";
|
||||
import { convertFileToBase64String } from "@/composables/utils";
|
||||
import { FieldChecks, FieldsContainer } from "@/modules/validator";
|
||||
import { notification } from "@/types";
|
||||
import ModalComp from "@/components/UI/ModalComp.vue";
|
||||
import ThemeSwitcher from "@/components/Common/ThemeSwitcher.vue";
|
||||
|
||||
const refModal = ref<InstanceType<typeof ModalComp> | undefined>();
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const userApi = useUserApi();
|
||||
|
||||
async function tryLogout() {
|
||||
const result = await refModal.value?.open({ type: "cancel_leave" });
|
||||
if (result === "leave") {
|
||||
logout();
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
router.push("/login");
|
||||
userStore.resetData();
|
||||
}
|
||||
|
||||
const userDataEdited = ref(false);
|
||||
const userPasswordEdited = ref(false);
|
||||
|
||||
const localUserData = ref<{ name: string; surname: string; image: string; imageFile?: File }>({
|
||||
...userStore.userData,
|
||||
imageFile: undefined,
|
||||
});
|
||||
const localPasswords = ref({ oldPassword: "", newPassword: "", newPassword2: "" });
|
||||
|
||||
const passwordButtonStyle = computed(() =>
|
||||
!userPasswordEdited.value ||
|
||||
(localPasswords.value.oldPassword !== "" && localPasswords.value.newPassword !== "" && localPasswords.value.newPassword2 !== "")
|
||||
? "primary"
|
||||
: "disabled"
|
||||
);
|
||||
|
||||
function startEdit() {
|
||||
userDataEdited.value = true;
|
||||
}
|
||||
function stopEdit() {
|
||||
userDataEdited.value = false;
|
||||
userPasswordEdited.value = false;
|
||||
localUserData.value = { ...userStore.userData };
|
||||
localPasswords.value = { oldPassword: "", newPassword: "", newPassword2: "" };
|
||||
}
|
||||
|
||||
const notifications = ref<notification[]>([]);
|
||||
|
||||
async function tryChangeUserData() {
|
||||
let errors = getUserDataFieldErrors();
|
||||
|
||||
notifications.value = errors;
|
||||
|
||||
if (errors.length > 0) return;
|
||||
|
||||
const resp = await userApi.changeUserData(localUserData.value);
|
||||
userStore.setFieldValue("name", resp.name);
|
||||
userStore.setFieldValue("surname", resp.surname);
|
||||
userStore.setFieldValue("image", resp.image);
|
||||
|
||||
notifications.value = [{ text: "Данные успешно изменены", type: "success" }];
|
||||
|
||||
stopEdit();
|
||||
}
|
||||
|
||||
async function changeLocalImageValue(file: File) {
|
||||
console.log(file);
|
||||
if (!file.type.includes("image")) {
|
||||
alert("Необходимо выбрать изображение");
|
||||
return;
|
||||
}
|
||||
|
||||
localUserData.value.imageFile = file;
|
||||
localUserData.value.image = await convertFileToBase64String(file);
|
||||
}
|
||||
|
||||
function getUserDataFieldErrors() {
|
||||
const container = new FieldsContainer();
|
||||
|
||||
container.addField("Имя", localUserData.value.name, [
|
||||
FieldChecks.nonEmpty,
|
||||
FieldChecks.onlyLettersWhitespaces,
|
||||
FieldChecks.minLength(2),
|
||||
FieldChecks.maxLength(20),
|
||||
]);
|
||||
container.addField("Фамилия", localUserData.value.surname, [
|
||||
FieldChecks.nonEmpty,
|
||||
FieldChecks.onlyLettersWhitespaces,
|
||||
FieldChecks.minLength(2),
|
||||
FieldChecks.maxLength(20),
|
||||
]);
|
||||
|
||||
let errors: notification[] = [];
|
||||
for (let [key, value] of container.fields) {
|
||||
const error = value.tryGetFirstError();
|
||||
if (error !== "") {
|
||||
errors.push({ field: key, text: error, type: "warning" });
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function changePasswordButtonHandler() {
|
||||
if (!userPasswordEdited.value) {
|
||||
userPasswordEdited.value = true;
|
||||
} else {
|
||||
tryChangeUserPassword();
|
||||
}
|
||||
}
|
||||
|
||||
async function tryChangeUserPassword() {
|
||||
let errors = getUserPasswordFieldErrors();
|
||||
|
||||
notifications.value = errors;
|
||||
|
||||
if (errors.length > 0) return;
|
||||
|
||||
const resp = await userApi.changeUserPassword(localPasswords.value);
|
||||
|
||||
notifications.value = [{ text: "Пароль успешно изменён", type: "success" }];
|
||||
|
||||
stopEdit();
|
||||
}
|
||||
|
||||
function getUserPasswordFieldErrors() {
|
||||
const container = new FieldsContainer();
|
||||
|
||||
container.addField("Старый пароль", localPasswords.value.oldPassword, [FieldChecks.nonEmpty]);
|
||||
|
||||
container.addField("Новый пароль", localPasswords.value.newPassword, [
|
||||
FieldChecks.nonEmpty,
|
||||
FieldChecks.minLength(6),
|
||||
FieldChecks.maxLength(32),
|
||||
]);
|
||||
|
||||
let errors: notification[] = [];
|
||||
for (let [key, value] of container.fields) {
|
||||
const error = value.tryGetFirstError();
|
||||
if (error !== "") {
|
||||
errors.push({ field: key, text: error, type: "warning" });
|
||||
}
|
||||
}
|
||||
|
||||
if (localPasswords.value.newPassword !== localPasswords.value.newPassword2) {
|
||||
errors.push({ field: "Новый пароль", text: "Пароли не совпадают.", type: "warning" });
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function fieldHasErrors(field: string) {
|
||||
return notifications.value.some((x) => x.field === field);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => userStore.userData.name,
|
||||
(to) => (localUserData.value.name = to)
|
||||
);
|
||||
watch(
|
||||
() => userStore.userData.surname,
|
||||
(to) => (localUserData.value.surname = to)
|
||||
);
|
||||
|
||||
onDeactivated(() => {
|
||||
stopEdit();
|
||||
notifications.value = [];
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.user {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
padding: r(20);
|
||||
|
||||
&__container {
|
||||
@include main-container;
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: r(20);
|
||||
}
|
||||
|
||||
&__user-data {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__image {
|
||||
flex: 0 0 auto;
|
||||
margin: 0 auto r(15) auto;
|
||||
}
|
||||
|
||||
&__email {
|
||||
//display: flex;
|
||||
//align-items: center;
|
||||
}
|
||||
|
||||
&__password {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: r(25);
|
||||
}
|
||||
|
||||
&__button {
|
||||
align-self: center;
|
||||
margin-top: r(15);
|
||||
}
|
||||
|
||||
&__edit-toggle {
|
||||
position: absolute;
|
||||
top: r(20);
|
||||
right: r(20);
|
||||
width: r(30);
|
||||
height: r(30);
|
||||
color: color("ui-background");
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__theme-switcher {
|
||||
position: absolute;
|
||||
top: r(20);
|
||||
left: r(20);
|
||||
width: r(30);
|
||||
height: r(30);
|
||||
color: color("ui-background");
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__notifications {
|
||||
position: fixed;
|
||||
inset: auto 0 r(90) 0;
|
||||
}
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user