Merge pull request 'dev' (#26) from dev into main
Deploy / build_and_push_images (push) Successful in 1m50s Details
Deploy / deploy_to_server_dev (push) Successful in 35s Details

Reviewed-on: #26
This commit is contained in:
ksenia_mikhailova 2024-07-15 09:00:47 +03:00
commit dc1e491d1f
19 changed files with 387 additions and 123 deletions

View File

@ -26,6 +26,7 @@ jobs:
deploy_to_server_dev:
needs: [build_and_push_images]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure SSH key
@ -42,7 +43,7 @@ jobs:
NUXT_PUBLIC_API_BASE: ${{'https://mns.'}}${{ gitea.ref_name == 'dev' && 'dev.' || '' }}kustarshina.ru/kp
NUXT_PUBLIC_IMG_BASE: ${{'https://mns.'}}${{ gitea.ref_name == 'dev' && 'dev.' || '' }}kustarshina.ru
NUXT_PUBLIC_BASE_URL: ${{'https://kupizabor.'}}${{ gitea.ref_name == 'dev' && 'dev.' || '' }}kustarshina.ru
NUXT_PUBLIC_YANDEX_METRIKA_ID: ${{ secrets.YANDEX_METRIKA_ID }}
NUXT_PUBLIC_YANDEX_METRIKA_ID: ${{ gitea.ref_name == 'dev' && 0 || secrets.YANDEX_METRIKA_ID }}
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
@ -52,6 +53,7 @@ jobs:
script: |
cd $HOSTING_PATH
git checkout ${{gitea.ref_name}}
git pull
echo "NUXT_PUBLIC_API_BASE=$NUXT_PUBLIC_API_BASE
NUXT_PUBLIC_IMG_BASE=$NUXT_PUBLIC_IMG_BASE
NUXT_PUBLIC_BASE_URL=$NUXT_PUBLIC_BASE_URL

9
assets/LOGO.svg Normal file
View File

@ -0,0 +1,9 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M80.7917 0L65.2088 21.1238H112.386L128 0H80.7917Z" fill="#FF5A06"/>
<path d="M112.926 106.876H64.8394L79.4215 128H127.986L112.926 106.876Z" fill="#FF5A06"/>
<path d="M55.1291 21.1238V0H17.1687L0 21.1238H55.1291Z" fill="#FF5A06"/>
<path d="M108.204 26.7819H61.0348L55.1291 34.7875V26.7819H17.1687L0 48.0314H92.4977L108.204 26.7819Z" fill="#FF5A06"/>
<path d="M17.1687 53.6896H88.3154L81.5421 62.8533L89.8892 74.5619H0L17.1687 53.6896Z" fill="#FF5A06"/>
<path d="M17.1687 80.22H93.9229L108.893 101.218H60.9334L55.1291 92.8099V101.218H0L17.1687 80.22Z" fill="#FF5A06"/>
<path d="M55.1291 128V106.876H17.1687L0 128H55.1291Z" fill="#FF5A06"/>
</svg>

After

Width:  |  Height:  |  Size: 754 B

View File

@ -0,0 +1,3 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 64C0 28.672 28.672 0 64 0C99.328 0 128 28.672 128 64C128 99.328 99.328 128 64 128C28.672 128 0 99.328 0 64ZM91.545 90.2986C94.0432 76.9254 98.964 47.9124 100.1 35.9747C100.139 35.4178 100.116 34.777 100.097 34.2289C100.079 33.7361 100.064 33.3183 100.1 33.1036C100.024 32.4992 99.7968 31.6681 99.0397 31.0636C98.1313 30.3081 96.6929 30.157 96.0872 30.157C93.2105 30.157 88.8953 31.6681 67.8494 40.4324C60.5061 43.4546 45.8194 49.8012 23.7136 59.3967C20.1555 60.8322 18.2629 62.1922 18.1115 63.5522C17.84 65.9231 20.7938 66.8364 24.845 68.089C25.3123 68.2335 25.7943 68.3826 26.2876 68.5388C30.1485 69.8233 35.2964 71.2588 38.0218 71.3344C40.4443 71.4099 43.1697 70.3522 46.1979 68.3122C66.8653 54.4101 77.4639 47.3835 78.1452 47.2324C78.2046 47.2239 78.2649 47.2145 78.3258 47.205C78.8089 47.1296 79.3317 47.048 79.735 47.3835C79.9043 47.5623 80.0272 47.7797 80.0932 48.0167C80.1591 48.2537 80.1661 48.5032 80.1136 48.7435C79.8129 50.0036 67.5273 61.3664 61.9141 66.5581C60.4594 67.9035 59.4528 68.8345 59.2191 69.0677C58.6372 69.6586 58.0457 70.2193 57.4762 70.7592C53.8217 74.2233 51.0694 76.8321 57.5536 81.0809C60.7527 83.1844 63.297 84.9144 65.8654 86.6609C68.4632 88.4273 71.0858 90.2106 74.4357 92.4142C75.2864 92.9751 76.1005 93.5543 76.8939 94.1188C80.0546 96.3675 82.8872 98.3828 86.397 98.0808C88.3654 97.8541 90.4851 95.9653 91.545 90.2986Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

3
assets/icons/vk.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 64C0 28.672 28.672 0 64 0C99.328 0 128 28.672 128 64C128 99.328 99.328 128 64 128C28.672 128 0 99.328 0 64ZM94.2421 90.8735H103.893L103.88 90.8694C106.61 90.8694 108.014 89.4619 107.062 86.7591C106.215 84.0355 103.037 80.0995 98.9033 75.408C97.7647 74.0829 96.3361 72.68 95.0685 71.4352C93.834 70.2229 92.7521 69.1605 92.2397 68.4661C90.8314 66.5853 91.2219 65.8504 92.2397 64.1648C92.2397 64.1648 104.075 47.4702 105.289 41.8487C105.937 39.7727 105.289 38.2822 102.389 38.2822H92.7175C90.2706 38.2822 89.1447 39.5984 88.4967 41.0058C88.4967 41.0058 83.5197 53.0088 76.5737 60.7935C74.3262 63.0438 73.2876 63.7787 72.0745 63.7787C71.5137 63.7787 70.5831 63.0438 70.5831 60.9679V41.7449C70.5831 39.3243 69.827 38.1992 67.7706 38.1992H52.5616C51.0702 38.1992 50.1396 39.3243 50.1396 40.4495C50.1396 41.3407 50.6445 41.974 51.2892 42.7825C52.3323 44.0907 53.7414 45.8579 53.9699 49.9198V64.1689C53.9699 67.2621 53.4091 67.8225 52.196 67.8225C48.9099 67.8225 40.946 55.7365 36.1644 41.94C35.213 39.3243 34.2825 38.1992 31.8356 38.1992H22.1642C19.4598 38.1992 18.8989 39.5153 18.8989 40.9228C18.8989 43.5385 22.185 56.1891 34.1952 73.0788C42.1591 84.6043 53.5171 90.7905 63.7534 90.7905C69.9433 90.7905 70.6787 89.3872 70.6787 87.0497V78.335C70.6787 75.6072 71.2437 75.0467 73.2336 75.0467C74.6419 75.0467 77.1719 75.7816 82.8841 81.316C84.776 83.2318 86.2168 84.8363 87.3965 86.1501C90.3176 89.4031 91.638 90.8735 94.2421 90.8735Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

3
assets/icons/youtube.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 64C0 28.672 28.672 0 64 0C99.328 0 128 28.672 128 64C128 99.328 99.328 128 64 128C28.672 128 0 99.328 0 64ZM46.2495 91.7311L96.8031 68.0998C101.228 66.0253 101.228 62.643 96.8031 60.5685L46.2495 36.9373C41.8246 34.8854 38.1992 37.1966 38.1992 42.0784V86.5899C38.1992 91.483 41.8246 93.783 46.2495 91.7311Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 483 B

View File

@ -25,30 +25,77 @@ body {
.header {
@apply p-2 bg-slate-200;
a:not([target="_blank"]) {
@apply hidden xl:inline-block
.container {
@apply items-center;
}
}
.logo {
@apply text-ioprim font-bold w-20 text-2xl leading-4 py-4 col-span-4 xl:col-span-2;
@apply w-full py-4 col-span-4 xl:col-span-2;
a {
@apply flex items-center gap-2;
}
&_text {
@apply leading-4 text-3xl font-semibold;
}
svg {
@apply text-ioprim inline-block m-0 w-10 h-10;
}
}
.menu {
@apply col-span-8 flex justify-between;
@apply hidden xl:flex col-span-10 justify-end;
a {
@apply underline decoration-0 underline-offset-4 decoration-dotted hover:decoration-transparent transition-all;
@apply text-zinc-800 hover:text-ioprim-900 transition-colors;
&:before {
content: '';
@apply inline-block w-[2px] h-5 mx-4 align-middle bg-gradient-to-t from-transparent via-zinc-800 via-50% to-transparent;
}
&:first-child::before {
content: none;
}
}
a[href^=http] {
@apply relative no-underline;
@apply relative;
&:after {
content: '';
@apply ml-2;
}
}
&.toggle_visible {
@apply max-xl:flex max-xl:flex-col max-xl:text-center;
a {
@apply max-xl:mb-2;
}
a:before {
@media screen and (max-width: 1280px) {
content: none;
}
}
}
&-toggle {
@apply flex xl:hidden col-span-8 justify-end;
>span {
@apply cursor-pointer text-zinc-600 hover:text-ioprim-900 transition-colors;
svg {
@apply text-3xl;
}
}
}
}
a[href^="#"] {
@ -56,31 +103,67 @@ a[href^="#"] {
}
.footer {
@apply px-2 py-4 bg-slate-200;
@apply px-2 py-4 bg-slate-200 text-zinc-800 pt-12;
.container {
@apply gap-4
}
.k-logo {
@apply col-span-12 xl:col-span-2 row-span-3 text-9xl text-orange-400;
@apply col-span-12 xl:col-span-2 row-span-3 text-9xl text-ioprim;
svg {
@apply my-0 mx-auto;
}
&:after {
content: '';
@apply block lg:hidden mr-auto ml-auto mt-5 h-[2px] max-w-44 mx-4 align-middle bg-gradient-to-r from-transparent via-zinc-800 via-50% to-transparent;
}
}
&-text {
@apply col-span-12 xl:col-span-5 text-sm prose;
@apply col-span-12 xl:col-span-5 text-sm mb-2;
&-small {
@apply col-span-12 xl:col-span-10 text-xs min-w-full prose;
@apply col-span-12 xl:col-span-10 text-xs min-w-full;
}
&-social {
@apply col-span-12 xl:col-span-10 flex gap-4
@apply col-span-12 xl:col-span-10 flex gap-4 flex-wrap justify-center;
}
}
&-icon {
@apply order-2 flex justify-center items-center gap-2;
&:hover {
.footer-icon-text {
@apply no-underline;
}
}
svg {
@apply mb-0;
}
&-big {
@apply text-3xl order-1 w-full text-center;
.footer-icon-text {
@apply no-underline;
}
}
&-text {
@apply underline;
}
}
&_two {
@apply xl:bg-slate-200 py-16 lg:py-8 text-sm text-zinc-500;
}
}
.siteblock {
@ -150,9 +233,11 @@ a[href^="#"] {
&-status {
@apply text-center;
&-icon {
@apply text-8xl;
}
&-text {
@apply text-3xl;
}
@ -172,7 +257,11 @@ label {
}
input {
@apply bg-neutral-200 border border-gray-300 text-gray-900 rounded focus:ring-blue-500 focus:border-blue-500 text-lg p-2.5 disabled:cursor-not-allowed;
@apply bg-neutral-200 border border-gray-300 text-gray-900 rounded focus:ring-blue-500 focus:border-blue-500 text-lg p-2.5 disabled:cursor-not-allowed disabled:text-black;
}
input[type=checkbox] {
@apply w-4 h-4;
}
textarea {
@ -258,4 +347,20 @@ button {
@apply cursor-not-allowed opacity-50 pointer-events-none;
}
}
}
.calc_table {
@apply flex flex-col gap-4 max-w-4xl;
>.grid {
@apply gap-4 border-solid border-b items-center last:border-b-0;
>[class*=col] {
@apply p-2;
}
>[class*=row-span]+[class*=col-span] {
@apply pl-4;
}
}
}

View File

@ -23,14 +23,7 @@ const cameraStat = reactive({
})
const pointLight = ref()
const groundMaterial = ref({
color: "#555",
roughness: 0.7,
metalness: 0,
side: FrontSide,
precision: 'lowp',
})
const pointLight2 = ref()
const loadAll = async () => {
const { scene: light } = await useGLTF('/models_light/zabor_so_svetom.glb')
pointLight.value = light.children[2]

View File

@ -78,11 +78,11 @@ const changeParametres = () => {
if (auto_length) {
let w = parametric.length.min
const max_sections = Math.floor((total_length_mm - fence_length) / (parametric.length.min + fence_length))
const min_sections = Math.ceil((total_length_mm - fence_length) / (parametric.length.max + fence_length))
const min_sections = Math.floor((total_length_mm - fence_length) / (parametric.length.max + fence_length))
for (let index = min_sections; index <= max_sections; index++) {
full_sections = index
w = (total_length_mm - fence_length * (index - 1)) / index
w = (total_length_mm - fence_length - fence_length * index) / index
if (
w >= parametric.length.min
&& w <= parametric.length.max
@ -99,7 +99,7 @@ const changeParametres = () => {
}
}
if (((full_sections * length) + (full_sections * fence_length) + fence_length) <= total_length_mm) {
if (((full_sections * length) + (full_sections * fence_length) + fence_length) < total_length_mm) {
form_state.extra_section = Math.floor((total_length_mm - fence_length) % length)
} else {
form_state.extra_section = 0
@ -160,19 +160,6 @@ const goal = (target: string, params: object) => {
<input id="height" type="range" class="w-full" v-bind="parametric.height"
v-model="form_state.height" :ref="form_refs.height" />
</div>
<div class="form-item">
<label for="total_length">Общая длина забора, м</label>
<input type="number" id="total_length" v-bind="parametric.total_length"
v-model="form_state.total_length" :ref="form_refs.total_length" />
</div>
<div class="form-item form-item_checkbox">
<input id="auto_length" type="checkbox" v-model="form_state.auto_length" />
<label for="auto_length">Автоматический подбор секции</label>
</div>
<div class="form-item form-item_checkbox">
<input id="remove_pillar" type="checkbox" v-model="form_state.remove_pillar" />
<label for="remove_pillar">Без столбов</label>
</div>
</div>
</div>
<div class="col-span-12 lg:col-span-6">
@ -183,66 +170,72 @@ const goal = (target: string, params: object) => {
disabled />
<ColorPicker :cb="setLamelleColor" />
</div>
<div class="form-item">
<template v-for="item in predefLamelleColors">
<ColorPicker :color="item" :cb="setLamelleColor" :open="false"
:active="lamelle_color == item" />
</template>
</div>
<div class="form-item">
<label for="pillar_color">Цвет столба</label>
<input id="pillar_color" type="text" :value="getColorNameFromRal(pillar_color)" class="w-60"
disabled />
<ColorPicker :cb="setPillarColor" />
</div>
</div>
</div>
<div class="col-span-12">
<div class="form-row">
<div class="form-item">
<template v-for="item in predefPillarColors">
<ColorPicker :color="item" :cb="setPillarColor" :open="false"
:active="pillar_color == item" />
</template>
<label for="total_length">Общая длина забора, м</label>
<input type="number" id="total_length" v-bind="parametric.total_length"
v-model="form_state.total_length" :ref="form_refs.total_length" />
</div>
<div class="form-item w-full lg:w-2/4">
<p v-if="form_state.extra_section" class="text-ioprim">
Внимание! Дополнительная секция приводит к увеличению стоимости.
Рекомендуем вам изменить длину забора или длину секции!
</p>
</div>
</div>
<div class="form-row min-h-12">
<div class="form-item form-item_checkbox">
<input id="auto_length" type="checkbox" v-model="form_state.auto_length" />
<label for="auto_length">Автоматический подбор секции</label>
</div>
<div class="form-item form-item_checkbox">
<input id="remove_pillar" type="checkbox" v-model="form_state.remove_pillar" />
<label for="remove_pillar">Без столбов</label>
</div>
</div>
</div>
<div class="col-span-12 lg:col-span-8 prose min-w-full">
<div class="col-span-12 grid calc_table">
<template v-if="(form_state.total_length * 1000) >= parametric.length.min">
<p>
Забор общей длиной {{ form_state.total_length }}{{ '\xa0' }}м,
{{ section_count }}
<Plural :n="section_count" :forms="plurals.section" /> по
{{ `${parseFloat(form_state.length.toString()).toFixed(2)}\xa0мм` }}{{
form_state.extra_section ? ` и 1 дополнительная секция
длиной ${form_state.extra_section.toFixed(2)}\xa0мм` : '' }}.
</p>
<p v-if="parametric.length.min <= form_state.total_length * 1000">
Всего <template v-if="!form_state.remove_pillar">
<div class="grid grid-cols-6">
<div class="col-span-4">Секции</div>
<div class="col-span-1">{{ section_count }}</div>
<div class="col-span-1 row-span-2">{{
`${parseFloat(form_state.length.toString()).toFixed(2)}\xa0мм` }}
</div>
<div class="col-span-4">Ламели, RAL {{ lamelle_color }}, {{
getColorNameFromRal(lamelle_color)?.toLowerCase() }}</div>
<div class="col-span-1"> {{ section_count * lamelles_count }}</div>
</div>
<div class="grid grid-cols-6" v-if="form_state.extra_section">
<div class="col-span-4">Дополнительная секция</div>
<div class="col-span-1">1</div>
<div class="col-span-1 row-span-2">{{
`${parseFloat(form_state.extra_section.toString()).toFixed(2)}\xa0мм` }}</div>
<div class="col-span-4">Ламели, RAL {{ lamelle_color }}, {{
getColorNameFromRal(lamelle_color)?.toLowerCase() }}</div>
<div class="col-span-1"> {{ 1 * lamelles_count }}</div>
</div>
<div class="grid grid-cols-6" v-if="!form_state.remove_pillar">
<div class="col-span-4">Столбы, RAL {{ pillar_color }}, {{
getColorNameFromRal(pillar_color)?.toLowerCase() }}</div>
<div class="col-span-1">
{{ section_count + ~~(!!form_state.extra_section) + 1 }}
<Plural :forms="plurals.fence" :n="section_count + ~~(!!form_state.extra_section) + 1" />,
</template>
{{ section_count * lamelles_count }}
<Plural :n="section_count * lamelles_count" :forms="plurals.lamelle" />
{{ `длиной ${parseFloat(form_state.length.toString()).toFixed(2)}\xa0мм` }}<template
v-if="form_state.extra_section">
{{ ` и ${~~(!!form_state.extra_section.toFixed(2)) * lamelles_count}` }}
<Plural :n="~~(!!form_state.extra_section) * lamelles_count" :forms="plurals.lamelle" />
{{ `длиной ${form_state.extra_section}\xa0мм` }}
</template>.
</p>
<p>
Окрашивается по технологии порошковой окраски: <br />
ламели: {{ getColorNameFromRal(lamelle_color)?.toLowerCase() }};
столбы: {{ getColorNameFromRal(pillar_color)?.toLowerCase() }}.
</p>
</div>
<div class="col-span-1">
{{ `${parseFloat(form_state.fence_length.toString()).toFixed(2)}\xa0мм` }}
</div>
</div>
</template>
</div>
<div class="prose col-span-12 lg:col-span-4">
<p v-if="form_state.extra_section" class="text-ioprim">
Внимание! Дополнительная секция приводит к увеличению стоимости.
Рекомендуем вам изменить длину забора или длину секции!
</p>
</div>
<div class="form-row justify-center">
<button @click.prevent="toggleModal">Рассчитать прямо сейчас</button>
</div>
</form>
</div>
</template>

View File

@ -1,25 +1,63 @@
<script setup lang="ts">
import k_logo from '@/assets/icons/logo.svg'
import k_logo from '@/assets/LOGO.svg'
import { apiFetch } from '~/utils/apiFetch';
import tg from '@/assets/icons/telegram.svg'
import vk from '@/assets/icons/vk.svg'
import yt from '@/assets/icons/youtube.svg'
const icons = {
'simple-icons:vk': vk,
'simple-icons:telegram': tg,
'simple-icons:youtube': yt,
}
const { data: footerData } = await apiFetch<ApiFooterType[]>(`footer/?ordering=small_text`)
const { data: social_networkData } = await apiFetch<ApiSocial_networkType[]>(`social_network/`)
</script>
<template>
<div class="footer" id="contacts">
<div class="container">
<div class="k-logo">
<k_logo />
<div>
<div class="footer" id="contacts">
<div class="container">
<div class="col-span-12 lg:col-span-6 xl:col-span-3 mb-12 lg:mb-0">
<div class="k-logo">
<k_logo />
</div>
</div>
<div class="col-span-12 lg:col-span-6 xl:col-span-6 mb-12 lg:mb-0">
<template v-for="item in footerData">
<div class="footer-text" v-if="!item.small_text">
<template v-for="p in item.text.split('\n')">
<p v-if="p.trim().length">{{ p }}</p>
</template>
</div>
</template>
</div>
<div class="col-span-12 xl:col-span-3">
<div class="footer-text footer-text-social" v-if="social_networkData">
<template v-for="item in social_networkData">
<a :class="['footer-icon', { 'footer-icon-big': !item.icon }]" :href="item.link"
target="_blank">
<template v-if="item.icon">
<component :is="icons[(item.icon.trim() as keyof typeof icons)]"
v-if="icons.hasOwnProperty(item.icon.trim())" />
<Icon :name="item.icon" v-else />
</template>
<span class="footer-icon-text">{{ item.name }}</span>
</a>
</template>
</div>
</div>
</div>
<template v-for="item in footerData">
<div class="footer-text" :class="[{ 'footer-text-small': item.small_text }]">{{ item.text }}</div>
</template>
<div class="footer-text footer-text-social" v-if="social_networkData">
<template v-for="item in social_networkData">
<a :href="item.link" target="_blank">
<Icon :name="item.icon" /> {{ item.name }}
</a>
</template>
</div>
<div class="footer_two">
<div class="container">
<div class="col-span-10 col-start-2">
<template v-for="item in footerData">
<div class="footer-text" v-if="item.small_text">
{{ item.text }}
</div>
</template>
</div>
</div>
</div>
</div>

View File

@ -1,21 +1,36 @@
<script setup lang="ts">
import { apiFetch } from '~/utils/apiFetch';
import k_logo from '@/assets/LOGO.svg'
const { data: pagesData } = await apiFetch<ApiPagesType[]>(`pages/?ordering=order`)
const { data: menuData } = await apiFetch<ApiMenuType>(`menu/1/`)
const pagesData = (menuData.value ? menuData.value.pages : []).sort((a, b) => a.order - b.order)
const route = useRoute()
const menu_visible = ref(false)
const toggle_menu = () => {
if (window.innerWidth < 1280) {
menu_visible.value = !menu_visible.value
}
}
</script>
<template>
<div class="header">
<div class="container">
<div class="logo">
<NuxtLink to="/">
Купи забор
<k_logo />
<span class="logo_text">Kupizabor</span>
</NuxtLink>
</div>
<div class="menu">
<div class="menu-toggle">
<span @click="toggle_menu">
<Icon name="mdi:menu" />
</span>
</div>
<div class="menu" :class="[{ 'toggle_visible': menu_visible }]">
<template v-for="item in pagesData">
<NuxtLink :to="item.external_link || ((route.name == 'index' ? '' : '/') + `#${item.slug}`)"
:target="item.external_link ? '_blank' : '_self'">
:target="item.external_link ? '_blank' : '_self'" @click="toggle_menu">
{{ item.menu_title }}
</NuxtLink>
</template>

View File

@ -30,12 +30,15 @@ type modalDataType = {
phone?: string
name?: string
email?: string
policy: boolean
}
const modal_data = reactive<modalDataType>({
email: undefined,
phone: undefined,
name: undefined
name: undefined,
policy: true
})
const modal_form = reactive({
disabled: true,
errors: [],
@ -62,13 +65,17 @@ const validate = () => {
modal_form.disabled = true
if (
(modal_data.phone && phone_regexp.test(modal_data.phone))
|| (modal_data.email && email_regex.test(modal_data.email))
(
(modal_data.phone && phone_regexp.test(modal_data.phone))
|| (modal_data.email && email_regex.test(modal_data.email))
) && modal_data.policy == true
) {
modal_form.disabled = false
return
}
}
watch(modal_data, validate)
const submit = async (e: any) => {
goal('submit_form', modal_data)
modal_state.show_status = 'loading'
@ -178,6 +185,14 @@ const modalStatus = {
'success': ["mdi:check-circle-outline", 'Данные отправлены'],
'error': ["mdi:close-circle-outline", 'Ошибка отправки'],
}
const policy = () => {
navigateTo('/policy', {
open: {
target: "_blank",
},
});
}
</script>
<template>
<div v-if="isModalOpen" class="modal-backdrop" @click.self="toggleModal">
@ -194,11 +209,17 @@ const modalStatus = {
<template v-else-if="modal_state.show_form">
<h2>Оставьте контакты для связи </h2>
<form @submit.prevent="submit" ref="form">
<input type="text" placeholder="Ваше имя" v-model="modal_data.name" @keyup="validate" />
<input type="text" placeholder="Ваше имя" v-model="modal_data.name" />
<input type="phone" placeholder="Ваш номер телефона" v-model="modal_data.phone"
@keypress="validateInput" @keyup="validate" />
<input type="e-mail" placeholder="Ваш e-mail" v-model="modal_data.email" @keypress="validateInput"
@keyup="validate" />
@keypress="validateInput" />
<input type="e-mail" placeholder="Ваш e-mail" v-model="modal_data.email"
@keypress="validateInput" />
<div class="flex gap-4 justify-between items-center">
<input type="checkbox" id="policy" v-model="modal_data.policy" />
<label for="policy">Нажимая кнопку "Отправить" Вы также даете согласие на <NuxtLink to="policy" @click="policy">обработку персональных
данных</NuxtLink>.
</label>
</div>
{{ total_txt && total_txt.total[0] }}
<div class="flex gap-4">
<button class="not-prose" :disabled="modal_form.disabled" type="submit">Отправить</button>

View File

@ -13,7 +13,7 @@ export const use_pillar_color = () => useState<ralTypes>('pillar_color', () => p
export const use_lamelle_color = () => useState<ralTypes>('lamelle_color', () => predefLamelleColors[Math.floor(Math.random() * predefLamelleColors.length)] as ralTypes)
export const use_section_count = () => useState('section_count', () => n)
export const use_extra_section = () => useState('extra_section', () => 0)
export const use_total_length = () => useState('total_length', () => ((min * n) - 100) * 0.001)
export const use_total_length = () => useState('total_length', () => (((min + 104) * n) + 104) * 0.001)
export const use_min_length = () => useState('min_length', () => 700)
export const use_max_size = () => useState<number>('max_size', () => 13)
export const use_explosion_state = () => useState<boolean>('explosion_state', () => false)

View File

@ -10,4 +10,4 @@ services:
- "${DOCKER_PORT}:3000"
volumes:
- ./.env:/src/.env
image: ci.svs-tech.pro/mns-mini-zabor:latest
image: ci.svs-tech.pro/mns-mini-zabor:$BRANCH

View File

@ -1,13 +1,11 @@
<script setup lang="ts">
const config = useRuntimeConfig()
const apiBase = config.public.apiBase
import '@/assets/main.scss'
const config = useRuntimeConfig()
import { apiFetch } from './utils/apiFetch';
import type { NuxtError } from '#app'
import og_img from '/og_img.png'
const { data: seoData } = await apiFetch<ApiKpType>(`kp/2`)
const { data: seoData } = await apiFetch<ApiKpType>(`kp/2/`)
useSeoMeta({
title: seoData.value?.title,
ogTitle: seoData.value?.title,
@ -20,9 +18,11 @@ useSeoMeta({
const props = defineProps({
error: Object as () => NuxtError
})
const route = useRoute()
if(route.path !== '/404') {
// navigateTo('/404')
}
</script>
<template>
<div>
<Header />
@ -32,6 +32,11 @@ const props = defineProps({
<h1>Вы ищете страницу, которой не существует. Вернитесь на главную страницу сайта</h1>
<p>Извините, но мы не можем найти запрашиваемую страницу. К сожалению, мы не можем помочь вам с
покупкой забора здесь.</p>
<p class="hidden">
<code>
{{ error?.message }}
</code>
</p>
<button @click="navigateTo('/')" class="not-prose">Вернуться на главную</button>
</div>
</div>

View File

@ -42,6 +42,11 @@ export default defineNuxtConfig({
},
},
},
vue: {
compilerOptions: {
isCustomElement: (tag) => ['nobr'].includes(tag),
}
},
ssr: true,
modules: [
'@nuxtjs/tailwindcss',

52
pages/[slug].vue Normal file
View File

@ -0,0 +1,52 @@
<script setup lang="ts">
const config = useRuntimeConfig()
const imgBase = config.public.imgBase
import { apiFetch } from '~/utils/apiFetch';
import { marked } from 'marked';
import og_img from '/og_img.png'
const { data: seoData } = await apiFetch<ApiKpType>(`kp/1/`)
useSeoMeta({
title: seoData.value?.title,
ogTitle: seoData.value?.title,
description: seoData.value?.content,
ogDescription: seoData.value?.content,
ogImage: config.public.baseUrl + og_img,
// twitterCard: 'summary_large_image',
})
const route = useRoute()
const { data } = await apiFetch<ApiPagesType>(`pages/${route.params.slug}/`)
if (!data.value) {
throw createError({
statusCode: 404,
})
}
const policyText = computed(() => {
if (!data?.value) return ''
let c = data?.value.content || ''
return marked.parse(c)
})
</script>
<template>
<div>
<div class="siteblock bg-white">
<div class="container">
<h1 class="siteblock-title">{{ data?.title || '404' }}</h1>
<div class="col-span-full prose max-w-full" v-html="policyText" />
</div>
</div>
<div class="siteblock siteblock_imgbg bg-slate-500" v-if="data?.image"
:style="[{ backgroundImage: `url(${[imgBase, data?.image].join('/')})` }]">
<NuxtImg :src="[imgBase, data?.image].join('/')" class="invisible" alt="data.title || 'фоновая картинка'" title="" format="webp"
loading="lazy" />
</div>
<div class="siteblock siteblock_calc bg-white" v-else>
<Suspense>
<CalcModels />
</Suspense>
</div>
</div>
</template>

View File

@ -7,7 +7,7 @@ import { marked } from 'marked';
import og_img from '/og_img.png'
const { data: seoData } = await apiFetch<ApiKpType>(`kp/1`)
const { data: seoData } = await apiFetch<ApiKpType>(`kp/1/`)
useSeoMeta({
title: seoData.value?.title,
ogTitle: seoData.value?.title,
@ -17,15 +17,16 @@ useSeoMeta({
// twitterCard: 'summary_large_image',
})
const { data: pagesData } = await apiFetch<ApiPagesType[]>(`pages/?ordering=order`)
const { data: menuData } = await apiFetch<ApiMenuType>(`menu/1/?ordering=order`)
const pagesData = menuData.value ? menuData.value.pages : []
const { data: reviewsData } = await apiFetch<ApiReviewsType[]>(`review/`)
const { data: calculatorData } = await apiFetch(`calculator/5/`)
const about = (pagesData.value as ApiPagesType[]).find(el => el.slug == 'about')
const reviews = (pagesData.value as ApiPagesType[]).find(el => el.slug == 'clients')
const delivery = (pagesData.value as ApiPagesType[]).find(el => el.slug == 'how_to')
const advantages = (pagesData.value as ApiPagesType[]).find(el => el.slug == 'advantages')
const about = pagesData.find(el => el.slug == 'about')
const reviews = pagesData.find(el => el.slug == 'clients')
const delivery = pagesData.find(el => el.slug == 'how_to')
const advantages = pagesData.find(el => el.slug == 'advantages')
const roubleSign = new Intl.NumberFormat('ru-RU', {
style: 'currency',

7
types/index.d.ts vendored
View File

@ -14,8 +14,15 @@ type ApiKpType = {
is_indexed: boolean
}
type ApiMenuType = {
id: number
type: string
pages: ApiPagesType[]
}
type ApiPagesType = {
id: number
order: number
title: string
menu_title: string
slug: string

View File

@ -5,6 +5,15 @@ export async function apiFetch<T>(path: string) {
headers.set('Referer', config.public.baseUrl)
return useFetch<T>(`${apiBase}/${path}`, {
baseURL: config.public.baseUrl,
headers
headers,
onResponseError({ response }) {
console.log(response.status)
console.log(response.url)
window.location.pathname = '/404'
throw createError({
statusCode: 404,
fatal: true
})
},
})
}