11 Commits

Author SHA1 Message Date
ff6f40fdb4 fix
All checks were successful
Deploy Nuxt App / deploy (push) Successful in 4m45s
2025-07-27 14:09:49 +04:00
7760305c08 fix
Some checks failed
Deploy Nuxt App / deploy (push) Failing after 16m30s
2025-07-27 13:03:18 +04:00
c88be2ff6f fix
All checks were successful
Deploy Nuxt App / deploy (push) Successful in 3m53s
2025-07-27 09:42:46 +04:00
b4a936eac9 fix
All checks were successful
Deploy Nuxt App / deploy (push) Successful in 4m6s
2025-07-27 09:23:51 +04:00
4907ed064b fix
Some checks failed
Deploy Nuxt App / deploy (push) Failing after 1m48s
2025-07-27 09:13:31 +04:00
093e71b449 update
Some checks failed
Deploy Nuxt App / deploy (push) Failing after 44s
2025-07-27 09:11:25 +04:00
42a6225e99 new file: download/Otryvok_1.pdf
Some checks failed
Deploy Nuxt App / deploy (push) Failing after 50s
new file:   download/Otryvok_2.pdf
	modified:   nuxt.config.ts
	modified:   package.json
	modified:   pages/books/[slug].vue
	modified:   pages/books/_data/1.json
	modified:   pages/books/_data/2.json
2025-07-27 08:55:28 +04:00
1fd718a0a0 fix size fontstyle in question page
All checks were successful
Deploy Nuxt App / deploy (push) Successful in 3m17s
2025-06-25 13:39:28 +04:00
925947ddd5 fix
All checks were successful
Deploy Nuxt App / deploy (push) Successful in 3m32s
2025-06-25 11:55:42 +04:00
1b211c3148 fix
All checks were successful
Deploy Nuxt App / deploy (push) Successful in 4m25s
2025-06-22 09:25:44 +04:00
925b6197f2 add. Closes #8
All checks were successful
Deploy Nuxt App / deploy (push) Successful in 3m59s
2025-06-22 08:53:02 +04:00
30 changed files with 597 additions and 153 deletions

18
.env Normal file
View 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.com
SMTP_PASS=avhpihoudpyvibtx
DEFAULT_TO_EMAIL=miduway@yandex.ru
SMTP_HOST=smtp.yandex.ru
SMTP_PORT=465

20
.env.example Normal file
View File

@ -0,0 +1,20 @@
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
SMTP_HOST=smtp.yandex.ru
SMTP_PORT=465

View File

@ -8,6 +8,7 @@ Make sure to install dependencies:
```bash
# npm
npm install
# pnpm

71
components/EmailForm.vue Normal file
View File

@ -0,0 +1,71 @@
<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 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('Успех')) {
return 'bg-green-100 text-green-700 border border-green-200'
}
return 'bg-blue-100 text-blue-700 border-blue-200'
})
async function onSubmit() {
if (!to.value) {
status.value = 'Пожалуйста, введите ваш e-mail'
return
}
isLoading.value = true
status.value = ''
try {
await $fetch('/api/send-order', {
method: 'POST',
body: { to: to.value },
})
status.value = 'Успех! Письмо с подтверждением отправлено на вашу почту.'
to.value = ''
} catch (error) {
console.error('Ошибка отправки письма:', error)
status.value = `Ошибка отправки: ${error?.data?.message || error.message || 'Неизвестная ошибка'}`
} finally {
isLoading.value = false
}
}
</script>

View File

@ -2,6 +2,8 @@
<component
:is="tag"
: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="[baseStyle, size]"
data-ui="ui-button"
@ -11,36 +13,40 @@
</template>
<script setup lang="ts">
import { toRefs, computed } from "vue";
import { colorVariants } from "./UiButton.params.js";
import { toRefs, computed } from 'vue'
import { colorVariants } from './UiButton.params.js'
const props = defineProps({
tag: {
type: String,
default: "button",
default: 'button',
},
variants: {
type: String as () => "primary" | "secondary",
default: "primary",
type: String as () => 'primary' | 'secondary',
default: 'primary',
validator: (value) => {
return ["primary", "secondary"].includes(value as string);
return ['primary', 'secondary'].includes(value as string)
},
},
size: {
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,
secondary: colorVariants.secondary,
};
}
const baseStyle = computed(() => {
const variant = variants.value as "primary" | "secondary";
return colorClasses[variant]?.join(" ") || "";
});
const variant = variants.value as 'primary' | 'secondary'
return colorClasses[variant]?.join(' ') || ''
})
</script>

View File

@ -14,9 +14,11 @@ _Гарвард_
Механизмы мужской и женской влюбленностей несколько разнятся. Т.е. мужчины и женщины, парни и девушки влюбляются по-разному. У девушек крышу сразу не сносит, а у парней крышу сносит именно сразу. В этом и заключается принципиальное различие в механизмах. Безусловно, эта разница — классика, поэтому на этот счет есть вариации. Однако, по своей сути, так оно и есть. На первых порах отношений девушка способна трезво оценивать парня. Она еще не влюбилась, она оценивает, присматривается, приценивается к парню. Он ей нравится, но она может трезво мыслить. Видишь эти два графика ниже по тексту[2]? Теперь ты понимаешь различие в механизмах.
Когда отношения с девушкой только начинаются, когда еще не было секса, парень испытывает некое подобие эйфории и одновременной зависимости от девушки. Я опять же говорю о классическом случае. Парень пытается быть максимально хорошим для нее. Она ему нравится, он запал на нее. Все его мысли — о ней. Девушка же влюбляется с задержкой по времени, когда парень пройдет ее фильтры. Когда у нее “щелкнет” в голове, тогда она и влюбляется.
<div style="display:flex; flex-direction: column; justify-content: center; align-items: center;">
![Рисунок 5 — Различия между мужской и женской схемами влюбления](image2.png)
Рисунок 5 — Различия между мужской и женской схемами влюбления<br/>
_Иллюстрация создана автором книги_
</div>
&ensp;&ensp;Смотри, как выгодно для тебя устроила природа. Ты еще ничего не сделала в плане развития отношений, но уже имеешь бонус. Красота!! Спасибо матери-природе. Такой механизм — следствие заботы о выживании рода, вида. Вид должен выжить. Поэтому тебе дается время на фильтрацию самцов, чтобы ты могла выбрать лучшего и получить его семя. Самый живучий самец, самец, обошедший своих сородичей, самый жизнеспобный, способен дать тебе лучшее семя среди своих представителей. Так ты сможешь родить наиболее жизнеспособного ребенка, самого живучего. Забеременев же от слабого самца, ты родишь слабое потомство. Именно из-за такого механизма выживания вида парень сразу испытывает влечение к девушке, но после первого секса сразу же остывает к ней. Его задача выполнена, он оплодотворил самку. Его следующая задача — найти новую самку и оплодотворить ее. Поэтому он также быстро “западает” на следующую, трахает ее, остывает к ней и ищет новую. Парень выполняет задачу продолжения рода: совокупляется с максимальным количеством девушек.
Но для нас с тобой сейчас самое важное — работать в зоне t — фильтрации. Как раз в период, который тебе подарила мать-природа, ты должна работать над отношениями. Тебе дали бонус в виде времени, не просри его. Те фишки, которые я тебе даю, имеют максимальную эффективность как раз в период фильтрации самцов. Как только вы переспите, период фильтрации закончится. И эффективность любых манипулятивных фишек, даже от Вино Галанте, упадет в разы. Это же так просто!! Ты пропустила парня через свои фильтры. Мать-природа позаботилась о том, чтобы сразу после этого ему захотелось другую самку. Как только ему захотелось другую, тебе становится архисложно влиять на парня. Природа, природа, против нее не поспоришь.

View File

@ -1,2 +0,0 @@
VITE_MODE=DEVELOP
VITE_API_BASE_PATH=http://localhost:5173/

View File

@ -13,7 +13,7 @@ const headerNavigation = [
},
{
name: 'Купить',
path: '/buy',
path: '/cart',
},
{
name: 'Отзывы',

View File

@ -25,11 +25,13 @@ export default defineNuxtConfig({
css: ['@/assets/css/tailwind.css'],
...config,
htmlValidator: {
usePrettier: true,
logLevel: 'error',
failOnError: false,
},
app: {
head: {
title: 'Vino Galante',
@ -39,4 +41,13 @@ export default defineNuxtConfig({
...head,
},
},
runtimeConfig: {
smtpHost: process.env.SMTP_HOST,
smtpPort: process.env.SMTP_PORT,
smtpUser: process.env.SMTP_USER,
smtpPass: process.env.SMTP_PASS,
},
modules: [...config.modules],
})

View File

@ -13,6 +13,7 @@
"format": "node_modules/.bin/prettier --write ./"
},
"dependencies": {
"@a2seven/yoo-checkout": "^1.1.4",
"@nuxt/content": "^3.6.0",
"@nuxt/fonts": "^0.11.4",
"@nuxt/icon": "^1.13.0",
@ -22,10 +23,15 @@
"@nuxtjs/sitemap": "^7.4.0",
"@pinia/nuxt": "^0.5.5",
"@tailwindcss/postcss": "^4.1.10",
"better-sqlite3": "^11.10.0",
"better-sqlite3": "^12",
"crypto-js": "^4.2.0",
"date-fns": "^4.1.0",
"husky": "^9.1.7",
"nodemailer": "^7.0.3",
"nuxt": "^3.17.5",
"nuxt-schema-org": "^5.0.5",
"pg": "^8.16.2",
"postgres": "^3.4.7",
"swiper": "^11.2.8",
"vue": "^3.5.16"
},
@ -34,6 +40,8 @@
"@nuxt/test-utils": "^3.11.3",
"@nuxtjs/tailwindcss": "^6.11.4",
"@types/node": "^22.0.0",
"@types/nodemailer": "^6.4.17",
"@types/pg": "^8.15.4",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.5.0",
"autoprefixer": "^10.4.18",

View File

@ -33,7 +33,7 @@
</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-row">
@ -41,7 +41,12 @@
<img src="/img/svg/books/ruble.svg" alt="ruble" />
</div>
<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 }}
</UiButton>
<UiParagraph size="200">
@ -81,20 +86,29 @@
</div>
<!--навигация по книге-->
<div class="mt-28 pt-2">
<div class="mt-28">
<ul class="flex flex-row mr-32 items-baseline justify-between lg:whitespace-nowrap">
<li class="flex flex-row h-24 w-[105px] items-center">
<li class="flex flex-row items-center">
<NuxtLink
:to="`/books/${route.params.slug}/chapters/${route.params.slug}/`"
class="flex flex-col gap-8 items-center cursor-pointer"
>
<img src="/img/svg/books/read.svg" alt="Читай отрывок" width="62" height="53" />
<img
src="/img/svg/books/bi_book.svg"
alt="Читай отрывок"
width="62"
height="53"
/>
<UiParagraph size="250" as="span"> Читай отрывок </UiParagraph>
</NuxtLink>
</li>
<li class="flex flex-row items-center h-24 w-[105px]">
<NuxtLink to="#" class="flex flex-col items-center gap-8 cursor-pointer">
<li class="flex flex-row items-center">
<a
download
:href="book.download"
class="flex flex-col items-center gap-8 cursor-pointer"
>
<img
src="/img/svg/books/download.svg"
alt="Скачай отрывок"
@ -103,9 +117,9 @@
/>
<UiParagraph size="250" as="span"> Скачай отрывок </UiParagraph>
</NuxtLink>
</a>
</li>
<li class="flex flex-row items-center h-24 w-[105px]">
<li class="flex flex-row items-center">
<NuxtLink
:to="`/books/${route.params.slug}/${book.hrefTitles}`"
class="flex flex-col items-center gap-8 cursor-pointer"
@ -153,7 +167,7 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useRoute } from '#app'
import { useRoute, useRouter } from '#app'
import UiHeading from '@/components/Typography/UiHeading.vue'
import UiParagraph from '@/components/Typography/UiParagraph.vue'
import UiButton from '@/components/UiButton/UiButton.vue'
@ -178,10 +192,13 @@ interface BookDetail {
}>
href: string
hrefTitles: string
download: string
}
const route = useRoute()
const router = useRouter()
const currentBookData = ref<BookDetail | null>(null)
const book = computed(() => currentBookData.value)
@ -196,6 +213,10 @@ const loadBookData = async (slug: string) => {
}
}
const handleSelect = () => {
router.push('/cart/')
}
watch(
() => route.params.slug,
async (newSlug) => {

View File

@ -1,19 +1,23 @@
<template>
<div v-if="titles" class="relative z-50 min-h-screen text-white mb-[208px]">
<section class="flex flex-col relative z-40 mt-40 ml-18">
<UiHeading tag="H1" class="whitespace-pre-line [&]:font-normal mb-10 -ml-5" size="500">
<UiHeading
tag="H1"
class="whitespace-pre-line [&]:font-normal mb-10 -ml-5 !text-[24px]"
size="500"
>
{{ titles.title }}
</UiHeading>
<div class="flex flex-col gap-6">
<div v-for="(section, index) in titles.sections" :key="index" class="flex flex-col gap-4">
<!-- Main section title -->
<UiHeading tag="h2" size="300" class="text-three [&]:font-normal">
<UiHeading tag="h2" size="300" class="text-[#E20C66] !text-[24px] [&]:font-normal">
{{ section.title }}
</UiHeading>
<!-- Subsections -->
<div v-if="section.subsections" class="ml-6 flex flex-col gap-4">
<div v-if="section.subsections" class="ml-10 flex flex-col gap-4">
<div
v-for="(subsection, subIndex) in section.subsections"
:key="subIndex"
@ -30,19 +34,28 @@
:alt="subsection.title.text"
class="w-6 h-6"
/>
<UiHeading tag="H3" size="300" class="[&]:font-normal">
<UiHeading
tag="H3"
size="300"
class="[&]:text-[#FFFFFF] !text-[22px] [&]:font-normal"
>
{{ subsection.title.text }}
</UiHeading>
</div>
<!-- Regular subsection -->
<UiHeading v-else tag="H3" size="300" class="[&]:text-gray-200 [&]:font-normal">
<UiHeading
v-else
tag="H3"
size="300"
class="[&]:text-[#FFFFFF] ml-1 !text-[22px] [&]:font-normal"
>
{{ subsection.title }}
</UiHeading>
<!-- Items list -->
<ul v-if="subsection.items" class="ml-6 flex flex-col gap-2 list-decimal">
<ul v-if="subsection.items" class="ml-10 flex flex-col gap-2 list-decimal">
<li v-for="(item, itemIndex) in subsection.items" :key="itemIndex">
<UiParagraph size="300" class="[&]:text-gray-200 [&]:font-normal"
<UiParagraph size="300" class="[&]:text-[#CDCDCD] [&]:font-normal"
>&nbsp;{{ item }}</UiParagraph
>
</li>
@ -51,8 +64,11 @@
<!-- Nested subsections -->
<div v-if="subsection.subsections" class="flex flex-col gap-2">
<div v-for="(nestedSub, nestedIndex) in subsection.subsections" :key="nestedIndex">
<UiHeading tag="H4" size="300" class="[&]:text-gray-200 [&]:font-normal">
{{ nestedSub.title }}
<UiHeading
tag="H4"
class="[&]:text-[#CDCDCD] [&]:font-normal ml-4 [&]:text-[20px]"
>
{{ nestedSub.text }}
</UiHeading>
</div>
</div>
@ -80,7 +96,8 @@ interface Subsection {
title: string | SubsectionTitle
items?: string[]
subsections?: Array<{
title: string
title?: string
text?: string
}>
}

View File

@ -4,10 +4,20 @@
"titleMeta": "Оглавление - Книга I",
"sections": [
{
"title": "Благодарности"
"subsections": [
{
"title": "Благодарности"
}
]
},
{
"subsections": [
{
"title": "Введение"
}
]
},
{
"title": "Введение",
"subsections": [
{
"title": "Работа с любым печатным материалом"
@ -74,26 +84,29 @@
"title": {
"text": "Поля-тараканы",
"img": "/img/svg/books/1/titles-1/simple-icons_cockroachlabs.svg"
}
},
{
"title": "Техника безопасности при работе в полях"
},
{
"title": "Восстановление сил"
},
{
"title": "Главный принцип развития"
},
{
"title": "Техники выхода в аптайм"
},
{
"title": "Действия"
},
{
"title": "Результат"
},
"subsections": [
{
"text": "Техника безопасности при работе в полях"
},
{
"text": "Восстановление сил"
},
{
"text": "Главный принцип развития"
},
{
"text": "Техники выхода в аптайм"
},
{
"text": "Действия"
},
{
"text": "Результат"
}
]
},
{
"title": {
"text": "Калибровка",
@ -115,19 +128,19 @@
},
"subsections": [
{
"title": "Определение и классификация"
"text": "Определение и классификация"
},
{
"title": "Псевдопассивная. Этап первый"
"text": "Псевдопассивная. Этап первый"
},
{
"title": "Псевдопассивная. Этап второй"
"text": "Псевдопассивная. Этап второй"
},
{
"title": "Активный тип стратегии"
"text": "Активный тип стратегии"
},
{
"title": "Третий этап. Степени кокетства"
"text": "Третий этап. Степени кокетства"
}
]
}
@ -147,35 +160,36 @@
"subsections": [
{
"title": "Введение"
},
{
"text": "Физическое здоровье"
},
{
"text": "Ментальный посыл. Стильность"
},
{
"text": "Ментальный посыл. Внутреннее ощущение мира"
}
]
},
{
"title": "Физическое здоровье"
},
{
"title": "Ментальный посыл. Стильность"
},
{
"title": "Ментальный посыл. Внутреннее ощущение мира"
},
{
"title": {
"text": "Ошибки резидента",
"img": "/img/svg/books/1/titles-1/si_error-line.svg"
},
"items": [
"Косак 1",
"Косак 2",
"Полукосак 3",
"Косак 4",
"МетаКосак 5 опасный",
"МетаКосак 6 опасный",
"Косак 7",
"МетаКосак 8 крайне опасно",
"МетаКосак 9 опасно",
"Косак 10",
"Косак 11"
"Косяк 1",
"Косяк 2",
"Полукосяк 3",
"Косяк 4",
"МегаКосяк 5 опасный",
"МегаКосяк 6 опасный",
"Косяк 7",
"МегаКосяк 8 крайне опасно",
"МегаКосяк 9 опасно",
"Косяк 10",
"Косяк 11"
]
},
{

View File

@ -22,16 +22,16 @@
},
"subsections": [
{
"title": "Экскурс в историю"
"text": "Экскурс в историю"
},
{
"title": "Правила формирования запасного аэродрома"
"text": "Правила формирования запасного аэродрома"
},
{
"title": "Подробнее о пятом пункте или как не врать"
"text": "Подробнее о пятом пункте или как не врать"
},
{
"title": "Следствия наличия запасного аэродрома"
"text": "Следствия наличия запасного аэродрома"
}
]
},
@ -42,13 +42,13 @@
},
"subsections": [
{
"title": "Одежда"
"text": "Одежда"
},
{
"title": "Правильные места для свиданий"
"text": "Правильные места для свиданий"
},
{
"title": "Самое важное в свиданиях"
"text": "Самое важное в свиданиях"
}
]
}
@ -66,7 +66,6 @@
"img": "/img/svg/books/2/titles-2/Vector (4).svg"
},
"items": [
"Механизм мужской и женской влюбленностей",
"Главный секрет влюбления",
"Ментальная подстройка",
"Подстройка по ценностям",
@ -102,13 +101,13 @@
},
"subsections": [
{
"title": "Введение"
"text": "Введение"
},
{
"title": "Оральные ласки"
"text": "Оральные ласки"
},
{
"title": "Минет"
"text": "Минет"
}
]
},
@ -119,13 +118,13 @@
},
"subsections": [
{
"title": "Российские школы соблазнения"
"text": "Российские школы соблазнения"
},
{
"title": "Как найти соблазнителя"
"text": "Как найти соблазнителя"
},
{
"title": "История рождения одного термина"
"text": "История рождения одного термина"
}
]
},
@ -136,22 +135,22 @@
},
"subsections": [
{
"title": "Как парни воспринимают секс"
"text": "Как парни воспринимают секс"
},
{
"title": "Девичья невинность"
"text": "Девичья невинность"
},
{
"title": "Прожим девственницы. Метод Галанте."
"text": "Прожим девственницы. Метод Галанте."
},
{
"title": "Прожим девственницы. Классический метод."
"text": "Прожим девственницы. Классический метод."
},
{
"title": "Прожим девственницы. Метод пикапера."
"text": "Прожим девственницы. Метод пикапера."
},
{
"title": "Вместо эпилога"
"text": "Вместо эпилога"
}
]
},

View File

@ -35,5 +35,6 @@
}
],
"href": "https://www.litres.ru/58125553/",
"hrefTitles": "title-1"
"hrefTitles": "title-1",
"download": "/files/Otryvok_1.pdf"
}

View File

@ -35,5 +35,6 @@
}
],
"href": "https://www.litres.ru/vino-galante/kak-vlubit-v-sebya-lubogo-tonkaya-igra/",
"hrefTitles": "title-2"
"hrefTitles": "title-2",
"download": "/files/Otryvok_2.pdf"
}

View File

@ -1,17 +1,21 @@
{
"title": "Корзина",
"items": [
{
"id": 1,
"name": "Как влюбить в себя любого \n Книга I. \n Откровения бывшего Казановы",
"src": "/assets/img/png/book1.png",
"buy": "добавить Книгу I"
"src": "/img/books/book1.png",
"buy": "добавить Книгу I",
"price": 520,
"message": "💡 Купи обе книги и получи \n скидку 10% - 936 за комплект"
},
{
"id": 2,
"name": "Как влюбить в себя любого \n Книга II. \n Тонкая игра",
"src": "/assets/img/png/book2.png",
"buy": "добавить Книгу II"
"src": "/img/books/book2.png",
"buy": "добавить Книгу II",
"price": 520,
"message": "💡 Купи обе книги и получи \n скидку 10% - 936 за комплект"
}
],
"message": "💡 Купи обе книги и получи скидку 10% - 936 за комплект",
"price": "520 ₽"
"message": "💡 Купи обе книги и получи скидку 10% - 936 за комплект"
}

View File

@ -1,8 +1,113 @@
<template>
<section class="relative z-50">
<UiHeading tag="h1" size="300"> Корзина </UiHeading>
<section class="relative z-50 ml-4">
<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-start mb-24"
>
<div class="w-40 h-40 relative -top-5">
<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>
</template>
<script setup lang="ts">
import cartList from './_data/cart.json'
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>

19
pages/email.vue Normal file
View 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>

View File

@ -18,9 +18,7 @@
class="swiper-slide feedback-slide !w-[356px] !h-[325px]"
>
<div class="feedback-card !w-full !h-full">
<UiParagraph size="250" class="card-text">{{
feedback.text
}}</UiParagraph>
<UiParagraph size="250" class="card-text">{{ feedback.text }}</UiParagraph>
</div>
</div>
</div>
@ -38,34 +36,34 @@
</template>
<script setup lang="ts">
import { onMounted } from "vue";
import Swiper from "swiper";
import { Pagination, Autoplay } from "swiper/modules";
import UiHeading from "@/components/Typography/UiHeading.vue";
import UiParagraph from "@/components/Typography/UiParagraph.vue";
import feedbackData from "./_data/feedback.data";
import { onMounted } from 'vue'
import Swiper from 'swiper'
import { Pagination, Autoplay } from 'swiper/modules'
import UiHeading from '@/components/Typography/UiHeading.vue'
import UiParagraph from '@/components/Typography/UiParagraph.vue'
import feedbackData from './_data/feedback.data'
import "swiper/css";
import "swiper/css/pagination";
import UiButton from "@/components/UiButton/UiButton.vue";
import 'swiper/css'
import 'swiper/css/pagination'
import UiButton from '@/components/UiButton/UiButton.vue'
onMounted(() => {
new Swiper(".feedback-swiper", {
new Swiper('.feedback-swiper', {
modules: [Pagination, Autoplay],
direction: "horizontal",
direction: 'horizontal',
loop: true,
autoplay: {
delay: 3000,
disableOnInteraction: false,
},
slidesPerView: "auto",
slidesPerView: 'auto',
spaceBetween: 72,
pagination: {
el: ".swiper-pagination",
el: '.swiper-pagination',
clickable: false,
},
});
});
})
})
</script>
<style lang="css" scoped>
@ -88,7 +86,7 @@ onMounted(() => {
}
.feedback-card {
background-color: #1a1a1a;
background-color: #171b27;
border-radius: 10px;
padding: 20px;
color: #fff;

View File

@ -3,7 +3,7 @@
<UiHeading tag="h1" size="300" class="text-three">
Книги для тебя, если ты не знаешь...
</UiHeading>
<ul class="flex mt-20 flex-row items-center justify-between">
<ul class="flex mt-20 flex-row items-center justify-between gap-10">
<li
class="flex flex-col-reverse justify-end w-38 gap-4 h-64 items-center transition-transform transform hover:scale-110"
v-for="({ img, text }, index) in questions.data"
@ -19,36 +19,36 @@
</template>
<script setup lang="ts">
import UiHeading from "@/components/Typography/UiHeading.vue";
import UiParagraph from "@/components/Typography/UiParagraph.vue";
import { reactive } from "vue";
import UiHeading from '@/components/Typography/UiHeading.vue'
import UiParagraph from '@/components/Typography/UiParagraph.vue'
import { reactive } from 'vue'
const questions = reactive({
data: [
{
img: "/img/svg/ellipse1.svg",
img: '/img/svg/ellipse1.svg',
text: 'Что делать, если у тебя ноги не "от ушей" и ты далеко не Мисс Мира?',
},
{
img: "/img/svg/ellipse2.svg",
text: "Как начать легко общаться с противоположным полом и о чем надо помолчать?",
img: '/img/svg/ellipse2.svg',
text: 'Как начать легко общаться с противоположным полом и о чем надо помолчать?',
},
{
img: "/img/svg/ellipse3.svg",
text: "Как относиться к сексу и на сколько важна девичья невинность?",
img: '/img/svg/ellipse3.svg',
text: 'Как относиться к сексу и на сколько важна девичья невинность?',
},
{
img: "/img/svg/ellipse4.svg",
text: "Сколько нужно заниматься, чтобы обрести НОВУЮ себя?",
img: '/img/svg/ellipse4.svg',
text: 'Сколько нужно заниматься, чтобы обрести НОВУЮ себя?',
},
{
img: "/img/svg/ellipse5.svg",
text: "Как выработать стратегию долгосрочных отношений?",
img: '/img/svg/ellipse5.svg',
text: 'Как выработать стратегию долгосрочных отношений?',
},
{
img: "/img/svg/ellipse6.svg",
text: "Как добиться того, чтоб парень делал так, как ты хочешь, но думал, что он сам так решил?",
img: '/img/svg/ellipse6.svg',
text: 'Как добиться того, чтоб парень делал так, как ты хочешь, но думал, что он сам так решил?',
},
],
});
})
</script>

View File

@ -1,12 +1,14 @@
import UiParagraph from '@/components/Typography/UiParagraph.vue';
<template>
<div
v-for="({ topContent, button, botContent }, index) in content.data"
v-for="({ topContent, button, botContent, path }, index) in content.data"
:key="index"
class="flex flex-col items-center max-w-96"
>
<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>
</div>
</template>
@ -23,12 +25,14 @@ const content = reactive({
'💡 Узнай, как думает мужчина, что его действительно цепляет, и что делает женщину незабываемой.',
button: 'КУПИТЬ КНИГУ I',
botContent: 'PDF + EPUB сразу после оплаты',
path: '/books/1',
},
{
topContent:
'💡 Продолжение для тех, кто готов перейти от флирта к глубокому контакту. Как строить притяжение, не теряя себя.',
button: 'КУПИТЬ КНИГУ II',
botContent: 'PDF + EPUB сразу после оплаты',
path: '/books/2',
},
],
})

View File

@ -6,7 +6,7 @@
@click="toggleFAQ(index)"
class="flex gap-12 mb-5 items-baseline font-bold"
>
<UiParagraph as="span" size="600">
<UiParagraph as="span" size="500">
{{ question }}
</UiParagraph>
<img

BIN
public/files/Otryvok_1.pdf Normal file

Binary file not shown.

BIN
public/files/Otryvok_2.pdf Normal file

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 208 KiB

After

Width:  |  Height:  |  Size: 208 KiB

View File

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 218 KiB

View File

@ -0,0 +1,15 @@
<svg width="63" height="54" viewBox="0 0 63 54" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_984_377)">
<path d="M4.60144 9.38058C8.04021 8.15329 12.9711 6.8298 17.7659 6.41849C22.9338 5.97401 27.3168 6.62746 29.858 8.91288V41.2405C26.2249 39.4825 21.6205 39.2404 17.3735 39.6052C12.7884 40.0033 8.16455 41.1344 4.60144 42.2953V9.38058ZM33.7436 8.91288C36.2848 6.62746 40.6678 5.97401 45.8356 6.41849C50.6305 6.8298 55.5613 8.15329 59.0001 9.38058V42.2953C55.4331 41.1344 50.8131 40 46.2281 39.6086C41.9772 39.2404 37.3766 39.4792 33.7436 41.2405V8.91288ZM31.8008 5.9143C27.9734 3.10479 22.4248 2.68684 17.3735 3.11806C11.4906 3.62556 5.55342 5.34709 1.85431 6.78336C1.51489 6.91514 1.22707 7.12751 1.0252 7.39513C0.823323 7.66274 0.715924 7.9743 0.71582 8.2926V44.7798C0.71591 45.0573 0.797563 45.3303 0.953298 45.5739C1.10903 45.8175 1.33387 46.0238 1.60722 46.174C1.88057 46.3242 2.19369 46.4134 2.51791 46.4335C2.84212 46.4536 3.16705 46.4039 3.46295 46.289C6.89007 44.9622 12.4115 43.3667 17.762 42.9057C23.2369 42.4347 27.8258 43.1942 30.2854 45.8147C30.4674 46.0084 30.6981 46.1647 30.9604 46.2721C31.2227 46.3796 31.5099 46.4353 31.8008 46.4353C32.0917 46.4353 32.3789 46.3796 32.6412 46.2721C32.9034 46.1647 33.1341 46.0084 33.3162 45.8147C35.7758 43.1942 40.3647 42.4347 45.8356 42.9057C51.19 43.3667 56.7154 44.9622 60.1386 46.289C60.4345 46.4039 60.7594 46.4536 61.0836 46.4335C61.4079 46.4134 61.721 46.3242 61.9943 46.174C62.2677 46.0238 62.4925 45.8175 62.6483 45.5739C62.804 45.3303 62.8856 45.0573 62.8857 44.7798V8.2926C62.8856 7.9743 62.7782 7.66274 62.5764 7.39513C62.3745 7.12751 62.0867 6.91514 61.7472 6.78336C58.0481 5.34709 52.1109 3.62556 46.2281 3.11806C41.1768 2.68353 35.6281 3.10479 31.8008 5.9143Z" fill="white"/>
<path d="M4.60144 9.38058C8.04021 8.15329 12.9711 6.8298 17.7659 6.41849C22.9338 5.97401 27.3168 6.62746 29.858 8.91288V41.2405C26.2249 39.4825 21.6205 39.2404 17.3735 39.6052C12.7884 40.0033 8.16455 41.1344 4.60144 42.2953V9.38058ZM33.7436 8.91288C36.2848 6.62746 40.6678 5.97401 45.8356 6.41849C50.6305 6.8298 55.5613 8.15329 59.0001 9.38058V42.2953C55.4331 41.1344 50.8131 40 46.2281 39.6086C41.9772 39.2404 37.3766 39.4792 33.7436 41.2405V8.91288ZM31.8008 5.9143C27.9734 3.10479 22.4248 2.68684 17.3735 3.11806C11.4906 3.62556 5.55342 5.34709 1.85431 6.78336C1.51489 6.91514 1.22707 7.12751 1.0252 7.39513C0.823323 7.66274 0.715924 7.9743 0.71582 8.2926V44.7798C0.71591 45.0573 0.797563 45.3303 0.953298 45.5739C1.10903 45.8175 1.33387 46.0238 1.60722 46.174C1.88057 46.3242 2.19369 46.4134 2.51791 46.4335C2.84212 46.4536 3.16705 46.4039 3.46295 46.289C6.89007 44.9622 12.4115 43.3667 17.762 42.9057C23.2369 42.4347 27.8258 43.1942 30.2854 45.8147C30.4674 46.0084 30.6981 46.1647 30.9604 46.2721C31.2227 46.3796 31.5099 46.4353 31.8008 46.4353C32.0917 46.4353 32.3789 46.3796 32.6412 46.2721C32.9034 46.1647 33.1341 46.0084 33.3162 45.8147C35.7758 43.1942 40.3647 42.4347 45.8356 42.9057C51.19 43.3667 56.7154 44.9622 60.1386 46.289C60.4345 46.4039 60.7594 46.4536 61.0836 46.4335C61.4079 46.4134 61.721 46.3242 61.9943 46.174C62.2677 46.0238 62.4925 45.8175 62.6483 45.5739C62.804 45.3303 62.8856 45.0573 62.8857 44.7798V8.2926C62.8856 7.9743 62.7782 7.66274 62.5764 7.39513C62.3745 7.12751 62.0867 6.91514 61.7472 6.78336C58.0481 5.34709 52.1109 3.62556 46.2281 3.11806C41.1768 2.68353 35.6281 3.10479 31.8008 5.9143Z" fill="url(#paint0_radial_984_377)" fill-opacity="0.2"/>
</g>
<defs>
<radialGradient id="paint0_radial_984_377" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(31.8008 24.7048) rotate(41.336) scale(32.9053 273.844)">
<stop stop-color="#EE70A4"/>
<stop offset="1" stop-color="#FF33AD"/>
</radialGradient>
<clipPath id="clip0_984_377">
<rect width="62.1699" height="53.0723" fill="white" transform="translate(0.71582)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,53 @@
import nodemailer from 'nodemailer'
export default defineEventHandler(async (event) => {
const { to } = await readBody(event)
if (!to) {
throw createError({
statusCode: 400,
statusMessage: 'Bad Request: "to" field is required',
})
}
const config = useRuntimeConfig(event)
// Убедимся, что все переменные окружения на месте
if (!config.smtpHost || !config.smtpPort || !config.smtpUser || !config.smtpPass) {
console.error('SMTP configuration is missing in runtime config.')
throw createError({
statusCode: 500,
statusMessage: 'Internal Server Error: SMTP configuration is incomplete.',
})
}
const transporter = nodemailer.createTransport({
host: config.smtpHost,
port: parseInt(config.smtpPort, 10),
secure: parseInt(config.smtpPort, 10) === 465, // true for 465, false for other ports
auth: {
user: config.smtpUser,
pass: config.smtpPass,
},
})
const mailOptions = {
from: `"Vino Galante" <${config.smtpUser}>`,
to: to, // Отправляем на email, полученный от клиента
bcc: config.smtpUser, // Отправляем скрытую копию самому себе для сохранения в "Отправленных"
subject: 'Ваш заказ принят!',
text: 'Спасибо за ваш заказ! Мы скоро свяжемся с вами для уточнения деталей.',
html: '<b>Спасибо за ваш заказ!</b><p>Мы скоро свяжемся с вами для уточнения деталей.</p>',
}
try {
await transporter.sendMail(mailOptions)
return { success: true, message: 'Email sent' }
} catch (error) {
console.error('Error sending email:', error)
throw createError({
statusCode: 500,
statusMessage: `Failed to send email: ${(error as Error).message || 'Unknown error'}`,
})
}
})

58
store/useSelectedBook.ts Normal file
View 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)
},
},
})