first commit

This commit is contained in:
work-sklizkiygad
2023-02-01 17:36:25 +03:00
commit ec014db3f9
134 changed files with 27305 additions and 0 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>