This commit is contained in:
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<template v-if="book">
|
||||
<div class="relative z-50 min-h-screen text-white mb-[208px]">
|
||||
<template v-if="!route.params.titlesSlug">
|
||||
<template v-if="!route.params.chapter && !route.params.titlesSlug">
|
||||
<!--верхний блок-->
|
||||
<section
|
||||
class="flex min-h-[600px] flex-row relative z-40 before:content-[''] before:absolute before:top-[-140px] before:bg-top before:left-0 before:w-[1280px] before:h-[1000px] before:bg-[url(/assets/img/webp/vino-galante.webp)] before:bg-no-repeat before:bg-contain mt-40"
|
||||
@ -84,7 +84,10 @@
|
||||
<div class="mt-28 pt-2">
|
||||
<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">
|
||||
<NuxtLink to="#" class="flex flex-col gap-8 items-center cursor-pointer">
|
||||
<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" />
|
||||
|
||||
<UiParagraph size="250" as="span"> Читай отрывок </UiParagraph>
|
||||
|
175
pages/books/[slug]/chapters/[chapter].vue
Normal file
175
pages/books/[slug]/chapters/[chapter].vue
Normal file
@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<div v-if="content" class="relative z-50 min-h-screen text-white mb-[208px]">
|
||||
<section class="flex flex-col relative z-40 mt-40 ml-18 mr-18">
|
||||
<div v-html="renderedContent" class="prose prose-invert max-w-none"></div>
|
||||
</section>
|
||||
</div>
|
||||
<div v-else class="text-white text-center py-20">Глава не найдена.</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Получаем параметры из URL
|
||||
const route = useRoute()
|
||||
const bookSlug = route.params.slug as string
|
||||
const chapterSlug = route.params.chapter as string
|
||||
|
||||
// Загружаем контент из markdown файла
|
||||
const { data: content } = await useAsyncData(`book-${bookSlug}-${chapterSlug}`, () => {
|
||||
return $fetch(`/api/content/books/${chapterSlug}`)
|
||||
})
|
||||
|
||||
// Конвертируем markdown в HTML (простая реализация)
|
||||
const renderedContent = computed(() => {
|
||||
if (!content.value?.content) return ''
|
||||
|
||||
let html = content.value.content
|
||||
|
||||
// Заменяем заголовки
|
||||
html = html.replace(
|
||||
/^### (.*$)/gim,
|
||||
'<h3 style="color: #f54b7e; font-weight: bold; margin-top: 1.5rem; margin-bottom: 0.5rem;">$1</h3>',
|
||||
)
|
||||
html = html.replace(
|
||||
/^## (.*$)/gim,
|
||||
'<h2 style="color: #f54b7e; font-weight: bold; margin-top: 2rem; margin-bottom: 1rem;">$1</h2>',
|
||||
)
|
||||
html = html.replace(
|
||||
/^# (.*$)/gim,
|
||||
'<h1 style="color: #f54b7e; font-weight: bold; font-size: 1.5em; margin-bottom: 1rem;">$1</h1>',
|
||||
)
|
||||
|
||||
// Заменяем параграфы
|
||||
html = html.replace(
|
||||
/^  (.*$)/gim,
|
||||
'<p style="margin-bottom: 1rem; line-height: 1.6;">$1</p>',
|
||||
)
|
||||
|
||||
// Заменяем жирный текст (двойные звездочки)
|
||||
html = html.replace(/\*\*(.*?)\*\*/g, '<strong style=" font-weight: bold;">$1</strong>')
|
||||
|
||||
// Заменяем курсив (звездочки)
|
||||
html = html.replace(/\*(.*?)\*/g, '<em style="font-style: italic;">$1</em>')
|
||||
|
||||
// Заменяем курсив (подчеркивания)
|
||||
html = html.replace(/\_(.*?)\_/g, '<em style="font-style: italic;">$1</em>')
|
||||
|
||||
// Заменяем изображения - исправляем пути
|
||||
html = html.replace(
|
||||
/!\[([^\]]*)\]\(([^)]+)\)/g,
|
||||
'<img src="/img/books/$2" alt="$1" style="max-width: 100%; height: auto; margin: 1rem 0; display: block;" />',
|
||||
)
|
||||
|
||||
// Заменяем списки
|
||||
html = html.replace(/^- (.*$)/gim, '<li style="margin-bottom: 0.5rem;">$1</li>')
|
||||
html = html.replace(/(<li.*<\/li>)/s, '<ul style="margin-bottom: 1rem;">$1</ul>')
|
||||
|
||||
// Заменяем <br/> теги на HTML <br>
|
||||
html = html.replace(/<br\/>/g, '<br>')
|
||||
|
||||
// Заменяем переносы строк
|
||||
html = html.replace(/\n/g, '<br>')
|
||||
|
||||
return html
|
||||
})
|
||||
|
||||
// Устанавливаем мета-теги
|
||||
useHead({
|
||||
title: `Глава ${chapterSlug} | Книга ${bookSlug} | Vino Galante`,
|
||||
meta: [
|
||||
{
|
||||
name: 'description',
|
||||
content: `Читайте главу ${chapterSlug} из книги ${bookSlug} автора Vino Galante`,
|
||||
},
|
||||
],
|
||||
link: [
|
||||
{
|
||||
rel: 'canonical',
|
||||
href: `https://ebook.miduway.space/books/${bookSlug}/chapters/${chapterSlug}`,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Обработка ошибок, если файл не найден
|
||||
if (!content.value) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Глава не найдена',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Дополнительные стили для контента */
|
||||
.prose {
|
||||
color: white;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.prose h1 {
|
||||
color: #f54b7e;
|
||||
font-weight: bold;
|
||||
font-size: 1.5em;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.prose h2 {
|
||||
color: #f54b7e;
|
||||
font-weight: bold;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.prose h3 {
|
||||
color: #f54b7e;
|
||||
font-weight: bold;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.prose p {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.prose ul {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.prose li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.prose strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.prose em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.prose blockquote {
|
||||
border-left: 4px solid #f54b7e;
|
||||
padding-left: 1rem;
|
||||
margin: 1rem 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.prose img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 1rem 0;
|
||||
display: block;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Стили для подписей к изображениям */
|
||||
.prose img + br + em {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
color: #f54b7e;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
@ -1,20 +1,12 @@
|
||||
<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" 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"
|
||||
>
|
||||
<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">
|
||||
{{ section.title }}
|
||||
@ -43,27 +35,14 @@
|
||||
</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-gray-200 [&]:font-normal">
|
||||
{{ subsection.title }}
|
||||
</UiHeading>
|
||||
|
||||
<!-- Items list -->
|
||||
<ul
|
||||
v-if="subsection.items"
|
||||
class="ml-6 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"
|
||||
<ul v-if="subsection.items" class="ml-6 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"
|
||||
> {{ item }}</UiParagraph
|
||||
>
|
||||
</li>
|
||||
@ -71,15 +50,8 @@
|
||||
|
||||
<!-- 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"
|
||||
>
|
||||
<div v-for="(nestedSub, nestedIndex) in subsection.subsections" :key="nestedIndex">
|
||||
<UiHeading tag="H4" size="300" class="[&]:text-gray-200 [&]:font-normal">
|
||||
{{ nestedSub.title }}
|
||||
</UiHeading>
|
||||
</div>
|
||||
@ -94,61 +66,60 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import UiHeading from "@/components/Typography/UiHeading.vue";
|
||||
import UiParagraph from "@/components/Typography/UiParagraph.vue";
|
||||
// import { useHead } from '@vueuse/head'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import UiHeading from '@/components/Typography/UiHeading.vue'
|
||||
import UiParagraph from '@/components/Typography/UiParagraph.vue'
|
||||
|
||||
interface SubsectionTitle {
|
||||
text: string;
|
||||
img?: string;
|
||||
text: string
|
||||
img?: string
|
||||
}
|
||||
|
||||
interface Subsection {
|
||||
title: string | SubsectionTitle;
|
||||
items?: string[];
|
||||
title: string | SubsectionTitle
|
||||
items?: string[]
|
||||
subsections?: Array<{
|
||||
title: string;
|
||||
}>;
|
||||
title: string
|
||||
}>
|
||||
}
|
||||
|
||||
interface Section {
|
||||
title: string;
|
||||
subsections?: Subsection[];
|
||||
title: string
|
||||
subsections?: Subsection[]
|
||||
}
|
||||
|
||||
interface TitlesData {
|
||||
title: string;
|
||||
titleMeta: string;
|
||||
sections: Section[];
|
||||
title: string
|
||||
titleMeta: string
|
||||
sections: Section[]
|
||||
}
|
||||
|
||||
const route = useRoute();
|
||||
const route = useRoute()
|
||||
|
||||
const currentTitlesData = ref<TitlesData | null>(null);
|
||||
const currentTitlesData = ref<TitlesData | null>(null)
|
||||
|
||||
const titles = computed(() => currentTitlesData.value);
|
||||
const titles = computed(() => currentTitlesData.value)
|
||||
|
||||
const loadTitlesData = async (slug: string) => {
|
||||
try {
|
||||
const module = await import(`./_data/${slug}.json`);
|
||||
currentTitlesData.value = module.default as TitlesData;
|
||||
const module = await import(`./_data/${slug}.json`)
|
||||
currentTitlesData.value = module.default as TitlesData
|
||||
} catch (error) {
|
||||
console.error(`Ошибка при загрузке содержания с slug '${slug}':`, error);
|
||||
currentTitlesData.value = null;
|
||||
console.error(`Ошибка при загрузке содержания с slug '${slug}':`, error)
|
||||
currentTitlesData.value = null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.params.titlesSlug,
|
||||
async (newSlug) => {
|
||||
if (newSlug) {
|
||||
await loadTitlesData(newSlug as string);
|
||||
await loadTitlesData(newSlug as string)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(titles, (newTitles) => {
|
||||
if (newTitles) {
|
||||
@ -156,17 +127,17 @@ watch(titles, (newTitles) => {
|
||||
title: `${newTitles.titleMeta} | Vino Galante`,
|
||||
meta: [
|
||||
{
|
||||
name: "description",
|
||||
content: "Содержание книги Vino Galante",
|
||||
name: 'description',
|
||||
content: 'Содержание книги Vino Galante',
|
||||
},
|
||||
],
|
||||
link: [
|
||||
{
|
||||
rel: "canonical",
|
||||
rel: 'canonical',
|
||||
href: `https://ebook.miduway.space/books/${route.params.slug}/${route.params.titlesSlug}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
17
pages/cart/_data/cart.json
Normal file
17
pages/cart/_data/cart.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"title": "Корзина",
|
||||
"items": [
|
||||
{
|
||||
"name": "Как влюбить в себя любого \n Книга I. \n Откровения бывшего Казановы",
|
||||
"src": "/assets/img/png/book1.png",
|
||||
"buy": "добавить Книгу I"
|
||||
},
|
||||
{
|
||||
"name": "Как влюбить в себя любого \n Книга II. \n Тонкая игра",
|
||||
"src": "/assets/img/png/book2.png",
|
||||
"buy": "добавить Книгу II"
|
||||
}
|
||||
],
|
||||
"message": "💡 Купи обе книги и получи скидку 10% - 936 за комплект",
|
||||
"price": "520 ₽"
|
||||
}
|
8
pages/cart/index.vue
Normal file
8
pages/cart/index.vue
Normal file
@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<section class="relative z-50">
|
||||
<UiHeading tag="h1" size="300"> Корзина </UiHeading>
|
||||
</section>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import UiHeading from '~/components/Typography/UiHeading.vue'
|
||||
</script>
|
46
pages/question/_data/question-answer.json
Normal file
46
pages/question/_data/question-answer.json
Normal file
@ -0,0 +1,46 @@
|
||||
[
|
||||
{
|
||||
"question": "1. Как купить книгу?",
|
||||
"answer": "Чтобы купить книгу, просто перейдите на страницу соответствующей книги и нажмите кнопку «Купить». Вы будете перенаправлены в корзину, где можно подтвердить заказ и выбрать способ оплаты."
|
||||
},
|
||||
{
|
||||
"question": "2. Какие способы оплаты доступны?",
|
||||
"answer": "На сайте доступны самые удобные и безопасные способы оплаты: банковские карты (Visa, Mastercard, Мир), Система Быстрых Платежей (СБП) и сервис SberPay. Вы можете выбрать любой из них при оформлении заказа."
|
||||
},
|
||||
{
|
||||
"question": "3. Можно ли скачать книгу сразу после покупки?",
|
||||
"answer": "Да, после подтверждения покупки, ссылка на скачивание книги будет доступна сразу. Вы получите файл на указанный при заказе email, либо сможете скачать книгу непосредственно с сайта."
|
||||
},
|
||||
{
|
||||
"question": "4. Что делать, если я не могу скачать книгу?",
|
||||
"answer": "Если у вас возникли проблемы с скачиванием книги, пожалуйста, свяжитесь с нами через контактный email vinogalante@yandex.ru, и мы постараемся решить ваш вопрос как можно скорее."
|
||||
},
|
||||
{
|
||||
"question": "5. Есть ли пробный отрывок книги?",
|
||||
"answer": "Да, на каждой странице книги есть возможность ознакомиться с аннотацией и скачать отрывок или читать его прямо на сайте. Это поможет вам оценить стиль и содержание книги перед покупкой."
|
||||
},
|
||||
{
|
||||
"question": "6. Как читать книгу на мобильном устройстве?",
|
||||
"answer": "Книги можно читать на мобильных устройствах в любом формате (например, PDF). Просто скачайте файл и откройте его с помощью подходящего приложения, например, Adobe Reader для PDF."
|
||||
},
|
||||
{
|
||||
"question": "7. Можно ли вернуть книгу, если она не понравилась?",
|
||||
"answer": "К сожалению, электронные книги не подлежат возврату, поскольку они могут быть скачаны сразу после покупки. Мы рекомендуем ознакомиться с отрывком книги перед покупкой, чтобы убедиться, что она соответствует вашим ожиданиям."
|
||||
},
|
||||
{
|
||||
"question": "8. Как связаться с автором?",
|
||||
"answer": "Вы можете связаться со мной по электронной почте vinogalante@yandex.ru. Я всегда рад услышать отзывы о книгах и ответить на вопросы."
|
||||
},
|
||||
{
|
||||
"question": "9. Как узнать, когда выйдет следующая книга?",
|
||||
"answer": "На данный момент автор не планирует продолжать писать книги по данной теме. Сейчас автор сосредоточен на других проектах."
|
||||
},
|
||||
{
|
||||
"question": "10. Есть ли скидки на книги?",
|
||||
"answer": "Да, при оплате двух книг сразу предоставляется скидка 10%. Просто добавьте обе книги в корзину, и скидка будет автоматически применена при оформлении заказа."
|
||||
},
|
||||
{
|
||||
"question": "11. Могу ли я получить книгу в другом формате?",
|
||||
"answer": "На данный момент книги доступны в формате PDF, ePub. Если вам нужен другой формат, пожалуйста, свяжитесь с нами, и мы постараемся помочь."
|
||||
}
|
||||
]
|
43
pages/question/index.vue
Normal file
43
pages/question/index.vue
Normal file
@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<section class="relative z-50 ml-4">
|
||||
<div v-for="({ question, answer }, index) in questions" :key="index" class="mb-16">
|
||||
<button
|
||||
type="button"
|
||||
@click="toggleFAQ(index)"
|
||||
class="flex gap-12 mb-5 items-baseline font-bold"
|
||||
>
|
||||
<UiParagraph as="span" size="600">
|
||||
{{ question }}
|
||||
</UiParagraph>
|
||||
<img
|
||||
src="/assets/icon/arrow.svg"
|
||||
alt="question"
|
||||
class="duration-500"
|
||||
:class="activeIndex === index ? 'rotate-90' : 'rotate-0'"
|
||||
/>
|
||||
</button>
|
||||
<Transition name="slide">
|
||||
<div v-show="activeIndex === index">
|
||||
<UiParagraph size="300">
|
||||
{{ answer }}
|
||||
</UiParagraph>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import UiParagraph from '~/components/Typography/UiParagraph.vue'
|
||||
import questions from './_data/question-answer.json'
|
||||
|
||||
const activeIndex = ref<null | number>(null)
|
||||
|
||||
const toggleFAQ = (index: number) => {
|
||||
activeIndex.value = activeIndex.value === index ? null : index
|
||||
}
|
||||
|
||||
useHead({
|
||||
title: 'Вопрос - ответ | Vino Galante',
|
||||
})
|
||||
</script>
|
Reference in New Issue
Block a user