Compare commits
1 Commits
8e01576896
...
925b6197f2
Author | SHA1 | Date | |
---|---|---|---|
925b6197f2 |
16
.env
Normal file
16
.env
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
DB_USER=
|
||||||
|
DB_HOST=
|
||||||
|
DB_NAME=
|
||||||
|
DB_PASSWORD=
|
||||||
|
DB_PORT=
|
||||||
|
|
||||||
|
YOOKASSA_SHOP_ID=
|
||||||
|
YOOKASSA_SECRET_KEY=
|
||||||
|
|
||||||
|
APP_URL=
|
||||||
|
|
||||||
|
|
||||||
|
# SMTP Configuration for Yandex
|
||||||
|
SMTP_USER=levishub@yandex.com
|
||||||
|
SMTP_PASS=avhpihoudpyvibtx
|
||||||
|
DEFAULT_TO_EMAIL=miduway@yandex.ru
|
18
.env.example
Normal file
18
.env.example
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
DB_USER=
|
||||||
|
DB_HOST=
|
||||||
|
DB_NAME=
|
||||||
|
DB_PASSWORD=
|
||||||
|
DB_PORT=
|
||||||
|
|
||||||
|
YOOKASSA_SHOP_ID=
|
||||||
|
YOOKASSA_SECRET_KEY=
|
||||||
|
|
||||||
|
APP_URL=
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# SMTP Configuration for Yandex
|
||||||
|
SMTP_USER=levishub@yandex.ru
|
||||||
|
SMTP_PASS=avhpihoudpyvibtx
|
||||||
|
DEFAULT_TO_EMAIL=miduway@yandex.ru
|
78
components/EmailForm.vue
Normal file
78
components/EmailForm.vue
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<template>
|
||||||
|
<div class="max-w-md mx-auto p-6 rounded-lg shadow-md text-gray-900">
|
||||||
|
<h2 class="text-2xl font-bold mb-6 text-primary text-center">Заполните контактные данные</h2>
|
||||||
|
|
||||||
|
<form @submit.prevent="onSubmit" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="to" class="block text-sm font-medium text-primary mb-2"> Ваш e-mail: </label>
|
||||||
|
<input
|
||||||
|
id="to"
|
||||||
|
v-model="to"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
placeholder="email@example.com"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UiButton type="submit" :disabled="isLoading" class="w-[400px]">
|
||||||
|
<span v-if="isLoading">Перехожу...</span>
|
||||||
|
<span v-else>Перейти к оплате</span>
|
||||||
|
</UiButton>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div v-if="status" class="mt-4 p-3 rounded-md" :class="statusClass">
|
||||||
|
{{ status }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
const mail = useMail()
|
||||||
|
|
||||||
|
const to = ref('')
|
||||||
|
const status = ref('')
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
const statusClass = computed(() => {
|
||||||
|
if (status.value.includes('Ошибка')) {
|
||||||
|
return 'bg-red-100 text-red-700 border border-red-200'
|
||||||
|
}
|
||||||
|
if (status.value.includes('отправлено') || status.value.includes('подписка')) {
|
||||||
|
return 'bg-green-100 text-green-700 border border-green-200'
|
||||||
|
}
|
||||||
|
return 'bg-blue-100 text-blue-700 border border-blue-200'
|
||||||
|
})
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
if (!to.value) {
|
||||||
|
status.value = 'Пожалуйста, введите ваш e-mail'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
status.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const emailData = {
|
||||||
|
to: to.value,
|
||||||
|
subject: 'Новая подписка на рассылку книги!',
|
||||||
|
text: `Пользователь с e-mail: ${to.value} подписался на рассылку книги.`,
|
||||||
|
}
|
||||||
|
|
||||||
|
await mail.send(emailData)
|
||||||
|
|
||||||
|
status.value = 'Успешная подписка! Проверьте вашу почту.'
|
||||||
|
|
||||||
|
// Очищаем форму
|
||||||
|
to.value = ''
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка отправки письма:', error)
|
||||||
|
status.value = `Ошибка отправки: ${error.message || 'Неизвестная ошибка'}`
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -2,6 +2,8 @@
|
|||||||
<component
|
<component
|
||||||
:is="tag"
|
:is="tag"
|
||||||
:type="tag === 'button' ? 'button' : ''"
|
:type="tag === 'button' ? 'button' : ''"
|
||||||
|
:to="path"
|
||||||
|
:href="path"
|
||||||
class="px-14 py-4 rounded-[20px] text-[30px] cursor-pointer shadow-[0px_16px_50px_-16px_rgba(229,30,125,1)]"
|
class="px-14 py-4 rounded-[20px] text-[30px] cursor-pointer shadow-[0px_16px_50px_-16px_rgba(229,30,125,1)]"
|
||||||
:class="[baseStyle, size]"
|
:class="[baseStyle, size]"
|
||||||
data-ui="ui-button"
|
data-ui="ui-button"
|
||||||
@ -11,36 +13,40 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { toRefs, computed } from "vue";
|
import { toRefs, computed } from 'vue'
|
||||||
import { colorVariants } from "./UiButton.params.js";
|
import { colorVariants } from './UiButton.params.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
tag: {
|
tag: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "button",
|
default: 'button',
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
type: String as () => "primary" | "secondary",
|
type: String as () => 'primary' | 'secondary',
|
||||||
default: "primary",
|
default: 'primary',
|
||||||
validator: (value) => {
|
validator: (value) => {
|
||||||
return ["primary", "secondary"].includes(value as string);
|
return ['primary', 'secondary'].includes(value as string)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "font-bold",
|
default: 'font-bold',
|
||||||
},
|
},
|
||||||
});
|
path: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const { tag, variants, size } = toRefs(props);
|
const { tag, variants, size } = toRefs(props)
|
||||||
|
|
||||||
const colorClasses: Record<"primary" | "secondary", string[]> = {
|
const colorClasses: Record<'primary' | 'secondary', string[]> = {
|
||||||
primary: colorVariants.primary,
|
primary: colorVariants.primary,
|
||||||
secondary: colorVariants.secondary,
|
secondary: colorVariants.secondary,
|
||||||
};
|
}
|
||||||
|
|
||||||
const baseStyle = computed(() => {
|
const baseStyle = computed(() => {
|
||||||
const variant = variants.value as "primary" | "secondary";
|
const variant = variants.value as 'primary' | 'secondary'
|
||||||
return colorClasses[variant]?.join(" ") || "";
|
return colorClasses[variant]?.join(' ') || ''
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
VITE_MODE=DEVELOP
|
|
||||||
VITE_API_BASE_PATH=http://localhost:5173/
|
|
@ -25,11 +25,13 @@ export default defineNuxtConfig({
|
|||||||
|
|
||||||
css: ['@/assets/css/tailwind.css'],
|
css: ['@/assets/css/tailwind.css'],
|
||||||
...config,
|
...config,
|
||||||
|
|
||||||
htmlValidator: {
|
htmlValidator: {
|
||||||
usePrettier: true,
|
usePrettier: true,
|
||||||
logLevel: 'error',
|
logLevel: 'error',
|
||||||
failOnError: false,
|
failOnError: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
title: 'Vino Galante',
|
title: 'Vino Galante',
|
||||||
@ -39,4 +41,25 @@ export default defineNuxtConfig({
|
|||||||
...head,
|
...head,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
modules: [
|
||||||
|
...config.modules,
|
||||||
|
[
|
||||||
|
'nuxt-mail',
|
||||||
|
{
|
||||||
|
smtp: {
|
||||||
|
host: 'smtp.yandex.ru',
|
||||||
|
port: 465,
|
||||||
|
secure: true,
|
||||||
|
auth: {
|
||||||
|
user: process.env.SMTP_USER || 'levishub@yandex.com',
|
||||||
|
pass: process.env.SMTP_PASS || 'avhpihoudpyvibtx',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
to: process.env.DEFAULT_TO_EMAIL || 'default@example.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
"format": "node_modules/.bin/prettier --write ./"
|
"format": "node_modules/.bin/prettier --write ./"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@a2seven/yoo-checkout": "^1.1.4",
|
||||||
"@nuxt/content": "^3.6.0",
|
"@nuxt/content": "^3.6.0",
|
||||||
"@nuxt/fonts": "^0.11.4",
|
"@nuxt/fonts": "^0.11.4",
|
||||||
"@nuxt/icon": "^1.13.0",
|
"@nuxt/icon": "^1.13.0",
|
||||||
@ -23,9 +24,15 @@
|
|||||||
"@pinia/nuxt": "^0.5.5",
|
"@pinia/nuxt": "^0.5.5",
|
||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
"better-sqlite3": "^11.10.0",
|
"better-sqlite3": "^11.10.0",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
|
"nodemailer": "^7.0.3",
|
||||||
"nuxt": "^3.17.5",
|
"nuxt": "^3.17.5",
|
||||||
|
"nuxt-mail": "^6.0.2",
|
||||||
"nuxt-schema-org": "^5.0.5",
|
"nuxt-schema-org": "^5.0.5",
|
||||||
|
"pg": "^8.16.2",
|
||||||
|
"postgres": "^3.4.7",
|
||||||
"swiper": "^11.2.8",
|
"swiper": "^11.2.8",
|
||||||
"vue": "^3.5.16"
|
"vue": "^3.5.16"
|
||||||
},
|
},
|
||||||
@ -34,6 +41,7 @@
|
|||||||
"@nuxt/test-utils": "^3.11.3",
|
"@nuxt/test-utils": "^3.11.3",
|
||||||
"@nuxtjs/tailwindcss": "^6.11.4",
|
"@nuxtjs/tailwindcss": "^6.11.4",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
|
"@types/pg": "^8.15.4",
|
||||||
"@vue/eslint-config-prettier": "^10.2.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
"@vue/eslint-config-typescript": "^14.5.0",
|
"@vue/eslint-config-typescript": "^14.5.0",
|
||||||
"autoprefixer": "^10.4.18",
|
"autoprefixer": "^10.4.18",
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
<!--средний блок-->
|
<!--средний блок-->
|
||||||
<section class="flex flex-row items-center ml-12 gap-24">
|
<section class="flex flex-row items-center ml-12 gap-24 relative z-50">
|
||||||
<!--левый-->
|
<!--левый-->
|
||||||
<div class="flex flex-col items-center min-h-[310px]">
|
<div class="flex flex-col items-center min-h-[310px]">
|
||||||
<div class="flex flex-row">
|
<div class="flex flex-row">
|
||||||
@ -41,7 +41,12 @@
|
|||||||
<img src="/img/svg/books/ruble.svg" alt="ruble" />
|
<img src="/img/svg/books/ruble.svg" alt="ruble" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center flex-col gap-3">
|
<div class="flex items-center flex-col gap-3">
|
||||||
<UiButton class="max-w-[440px] !font-normal !px-2 !py-4 mt-16">
|
<UiButton
|
||||||
|
@click="handleSelect"
|
||||||
|
:id="book.id"
|
||||||
|
variants="primary"
|
||||||
|
class="max-w-[440px] !font-normal !px-2 !py-4 mt-16"
|
||||||
|
>
|
||||||
{{ book.buttonText }}
|
{{ book.buttonText }}
|
||||||
</UiButton>
|
</UiButton>
|
||||||
<UiParagraph size="200">
|
<UiParagraph size="200">
|
||||||
@ -153,10 +158,11 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { useRoute } from '#app'
|
import { useRoute, useRouter } from '#app'
|
||||||
import UiHeading from '@/components/Typography/UiHeading.vue'
|
import UiHeading from '@/components/Typography/UiHeading.vue'
|
||||||
import UiParagraph from '@/components/Typography/UiParagraph.vue'
|
import UiParagraph from '@/components/Typography/UiParagraph.vue'
|
||||||
import UiButton from '@/components/UiButton/UiButton.vue'
|
import UiButton from '@/components/UiButton/UiButton.vue'
|
||||||
|
import { useSelectedBook } from '@/store/useSelectedBook'
|
||||||
|
|
||||||
interface BookDetail {
|
interface BookDetail {
|
||||||
id: number
|
id: number
|
||||||
@ -182,6 +188,8 @@ interface BookDetail {
|
|||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const currentBookData = ref<BookDetail | null>(null)
|
const currentBookData = ref<BookDetail | null>(null)
|
||||||
|
|
||||||
const book = computed(() => currentBookData.value)
|
const book = computed(() => currentBookData.value)
|
||||||
@ -196,6 +204,10 @@ const loadBookData = async (slug: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSelect = () => {
|
||||||
|
router.push('/cart/')
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.params.slug,
|
() => route.params.slug,
|
||||||
async (newSlug) => {
|
async (newSlug) => {
|
||||||
|
@ -1,17 +1,21 @@
|
|||||||
{
|
{
|
||||||
"title": "Корзина",
|
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
|
"id": 1,
|
||||||
"name": "Как влюбить в себя любого \n Книга I. \n Откровения бывшего Казановы",
|
"name": "Как влюбить в себя любого \n Книга I. \n Откровения бывшего Казановы",
|
||||||
"src": "/assets/img/png/book1.png",
|
"src": "/img/books/book1.png",
|
||||||
"buy": "добавить Книгу I"
|
"buy": "добавить Книгу I",
|
||||||
|
"price": 520,
|
||||||
|
"message": "💡 Купи обе книги и получи \n скидку 10% - 936 за комплект"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": 2,
|
||||||
"name": "Как влюбить в себя любого \n Книга II. \n Тонкая игра",
|
"name": "Как влюбить в себя любого \n Книга II. \n Тонкая игра",
|
||||||
"src": "/assets/img/png/book2.png",
|
"src": "/img/books/book2.png",
|
||||||
"buy": "добавить Книгу II"
|
"buy": "добавить Книгу II",
|
||||||
|
"price": 520,
|
||||||
|
"message": "💡 Купи обе книги и получи \n скидку 10% - 936 за комплект"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message": "💡 Купи обе книги и получи скидку 10% - 936 за комплект",
|
"message": "💡 Купи обе книги и получи скидку 10% - 936 за комплект"
|
||||||
"price": "520 ₽"
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,113 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="relative z-50">
|
<section class="relative z-50 ml-4">
|
||||||
<UiHeading tag="h1" size="300"> Корзина </UiHeading>
|
<UiHeading tag="h1" size="300" class="font-bold mb-16"> Корзина </UiHeading>
|
||||||
|
<div class="-ml-6">
|
||||||
|
<div
|
||||||
|
v-for="({ name, src, price, message, buy, id }, index) in cartList.items"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-center mb-24"
|
||||||
|
>
|
||||||
|
<div class="w-40 h-40">
|
||||||
|
<img :src="`${src}`" alt="book" />
|
||||||
|
</div>
|
||||||
|
<UiParagraph size="300" class="whitespace-pre mb-10 mr-12 w-80">{{ name }}</UiParagraph>
|
||||||
|
<UiParagraph size="300" class="whitespace-pre mb-10 mr-20">{{ message }}</UiParagraph>
|
||||||
|
<UiButton
|
||||||
|
:id="id"
|
||||||
|
class="mb-10"
|
||||||
|
v-if="store.getQuantity(id) === 0"
|
||||||
|
@click="handleAddToCart(id)"
|
||||||
|
>{{ buy }}</UiButton
|
||||||
|
>
|
||||||
|
<template v-else>
|
||||||
|
<div class="flex items-center gap-8 mr-20">
|
||||||
|
<button
|
||||||
|
class="w-8 h-8 flex items-center justify-center rounded-full border border-white text-white text-2xl"
|
||||||
|
@click="handleRemove(id)"
|
||||||
|
aria-label="Уменьшить количество"
|
||||||
|
>
|
||||||
|
−
|
||||||
|
</button>
|
||||||
|
<span class="text-white text-2xl">{{ store.getQuantity(id) }}</span>
|
||||||
|
<button
|
||||||
|
class="w-8 h-8 flex items-center justify-center rounded-full border border-white text-white text-2xl"
|
||||||
|
@click="handleIncrement(id)"
|
||||||
|
aria-label="Увеличить количество"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<span class="text-white text-2xl font-bold mr-20 whitespace-nowrap">
|
||||||
|
{{ Number(price) * store.getQuantity(id) }} ₽
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
class="w-8 h-8 flex items-center justify-center rounded-full border border-white text-white text-xl"
|
||||||
|
@click="handleRemove(id)"
|
||||||
|
aria-label="Удалить товар"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-end mt-8">
|
||||||
|
<UiParagraph is="span" size="300">Общая стоимость</UiParagraph>
|
||||||
|
<template v-if="isSpecialPrice">
|
||||||
|
<span class="text-white text-2xl font-bold ml-4 line-through select-none">
|
||||||
|
{{ regularTotalPrice }} ₽
|
||||||
|
</span>
|
||||||
|
<span class="text-primary text-3xl font-bold ml-4">
|
||||||
|
{{ store.getTotalPrice(cartList.items) }} ₽
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="text-primary text-3xl font-bold ml-4">
|
||||||
|
{{ store.getTotalPrice(cartList.items) }} ₽
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-10 flex flex-col items-center justify-center">
|
||||||
|
<UiButton class="w-[660px]"> перейти к оформлению </UiButton>
|
||||||
|
<UiParagraph is="span" size="300" class="mb-10 mt-5"
|
||||||
|
>После оплаты книги сразу будут доступны для скачивания</UiParagraph
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import cartList from './_data/cart.json'
|
||||||
import UiHeading from '~/components/Typography/UiHeading.vue'
|
import UiHeading from '~/components/Typography/UiHeading.vue'
|
||||||
|
import UiParagraph from '~/components/Typography/UiParagraph.vue'
|
||||||
|
import { useSelectedBook } from '@/store/useSelectedBook'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const store = useSelectedBook()
|
||||||
|
|
||||||
|
function handleIncrement(id: number) {
|
||||||
|
store.increment(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDecrement(id: number) {
|
||||||
|
store.decrement(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRemove(id: number) {
|
||||||
|
store.reset(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddToCart(id: number) {
|
||||||
|
store.addToCart(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedBooksCount = computed(() => store.cartQuantities.filter((i) => i.quantity > 0).length)
|
||||||
|
|
||||||
|
const isSpecialPrice = computed(() => selectedBooksCount.value === 2)
|
||||||
|
|
||||||
|
const regularTotalPrice = computed(() => {
|
||||||
|
// Сумма без скидки
|
||||||
|
return store.cartQuantities.reduce((sum, item) => {
|
||||||
|
const book = cartList.items.find((b: any) => b.id === item.id)
|
||||||
|
return sum + (book ? Number(book.price) * item.quantity : 0)
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
19
pages/email.vue
Normal file
19
pages/email.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative z-50 text-black min-h-screen py-12">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<UiHeading tag="h1" size="500" class="font-bold text-primary mb-4"
|
||||||
|
>Контактные данные</UiHeading
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EmailForm />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import UiHeading from '~/components/Typography/UiHeading.vue'
|
||||||
|
|
||||||
|
// Страница использует компонент EmailForm
|
||||||
|
</script>
|
@ -1,12 +1,14 @@
|
|||||||
import UiParagraph from '@/components/Typography/UiParagraph.vue';
|
import UiParagraph from '@/components/Typography/UiParagraph.vue';
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-for="({ topContent, button, botContent }, index) in content.data"
|
v-for="({ topContent, button, botContent, path }, index) in content.data"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="flex flex-col items-center max-w-96"
|
class="flex flex-col items-center max-w-96"
|
||||||
>
|
>
|
||||||
<UiParagraph size="300" class="mb-12 h-32">{{ topContent }} </UiParagraph>
|
<UiParagraph size="300" class="mb-12 h-32">{{ topContent }} </UiParagraph>
|
||||||
<UiButton variants="primary" class="mb-3 w-full">{{ button }} </UiButton>
|
<UiButton tag="RouterLink" :to="path" variants="primary" class="mb-3 w-full"
|
||||||
|
>{{ button }}
|
||||||
|
</UiButton>
|
||||||
<UiParagraph as="span" size="200"> {{ botContent }}</UiParagraph>
|
<UiParagraph as="span" size="200"> {{ botContent }}</UiParagraph>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -23,12 +25,14 @@ const content = reactive({
|
|||||||
'💡 Узнай, как думает мужчина, что его действительно цепляет, и что делает женщину незабываемой.',
|
'💡 Узнай, как думает мужчина, что его действительно цепляет, и что делает женщину незабываемой.',
|
||||||
button: 'КУПИТЬ КНИГУ I',
|
button: 'КУПИТЬ КНИГУ I',
|
||||||
botContent: 'PDF + EPUB сразу после оплаты',
|
botContent: 'PDF + EPUB сразу после оплаты',
|
||||||
|
path: '/books/1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
topContent:
|
topContent:
|
||||||
'💡 Продолжение для тех, кто готов перейти от флирта к глубокому контакту. Как строить притяжение, не теряя себя.',
|
'💡 Продолжение для тех, кто готов перейти от флирта к глубокому контакту. Как строить притяжение, не теряя себя.',
|
||||||
button: 'КУПИТЬ КНИГУ II',
|
button: 'КУПИТЬ КНИГУ II',
|
||||||
botContent: 'PDF + EPUB сразу после оплаты',
|
botContent: 'PDF + EPUB сразу после оплаты',
|
||||||
|
path: '/books/2',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
Before Width: | Height: | Size: 208 KiB After Width: | Height: | Size: 208 KiB |
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 218 KiB |
58
store/useSelectedBook.ts
Normal file
58
store/useSelectedBook.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
interface CartItem {
|
||||||
|
id: number
|
||||||
|
quantity: number
|
||||||
|
summary?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSelectedBook = defineStore('selectedBook', {
|
||||||
|
state: () => ({
|
||||||
|
cartQuantities: [] as CartItem[],
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
increment(id: number) {
|
||||||
|
const item = this.cartQuantities.find((i) => i.id === id)
|
||||||
|
if (item && item.quantity < 1) {
|
||||||
|
item.quantity++
|
||||||
|
}
|
||||||
|
},
|
||||||
|
decrement(id: number) {
|
||||||
|
const item = this.cartQuantities.find((i) => i.id === id)
|
||||||
|
if (item) {
|
||||||
|
item.quantity--
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reset(id: number) {
|
||||||
|
const idx = this.cartQuantities.findIndex((i) => i.id === id)
|
||||||
|
if (idx !== -1) {
|
||||||
|
this.cartQuantities.splice(idx, 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addToCart(id: number) {
|
||||||
|
if (!this.cartQuantities.find((i) => i.id === id)) {
|
||||||
|
this.cartQuantities.push({ id, quantity: 1 })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getQuantity(id: number) {
|
||||||
|
return this.cartQuantities.find((i) => i.id === id)?.quantity || 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
getTotalPrice: (state) => (books: { id: number; price: number }[]) => {
|
||||||
|
const selected = state.cartQuantities.filter((i) => i.quantity > 0)
|
||||||
|
if (selected.length === 2) {
|
||||||
|
// Проверяем, что это две разные книги
|
||||||
|
const ids = selected.map((i) => i.id)
|
||||||
|
if (ids[0] !== ids[1]) {
|
||||||
|
return 936
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Обычная сумма
|
||||||
|
return selected.reduce((sum, item) => {
|
||||||
|
const book = books.find((b) => b.id === item.id)
|
||||||
|
return sum + (book ? book.price * item.quantity : 0)
|
||||||
|
}, 0)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
Reference in New Issue
Block a user