Files
webapp/app/pages/index.vue

573 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
const { locale } = useI18n()
const localePath = useLocalePath()
const isEn = computed(() => locale.value === 'en')
const heroTitle = computed(() => isEn.value
? 'Optovia makes procurement and logistics transparent'
: 'Optovia делает закупку и логистику прозрачными')
const heroSubtitle = computed(() => isEn.value
? 'One flow for product search, hubs, offers, and route decisions.'
: 'Единый поток: поиск товара, хабы, офферы и решение по маршруту.')
const howSteps = computed(() => isEn.value
? [
{
number: '01',
title: 'Choose product and destination',
text: 'Start from the top search capsule and set what you buy and where it should go.',
},
{
number: '02',
title: 'See offers and hubs on map',
text: 'Compare suppliers, hubs, prices, and route options on one interactive canvas.',
},
{
number: '03',
title: 'Commit with full context',
text: 'Move to the best option with clear terms, timeline, and next actions for your team.',
},
]
: [
{
number: '01',
title: 'Выберите товар и направление',
text: 'Начните в верхней поисковой капсуле: задайте что закупаете и куда везете.',
},
{
number: '02',
title: 'Смотрите офферы и хабы на карте',
text: 'Сравнивайте поставщиков, хабы, цены и маршруты в одном интерактивном полотне.',
},
{
number: '03',
title: 'Принимайте решение с полным контекстом',
text: 'Переходите к лучшему варианту с понятными условиями, сроками и шагами для команды.',
},
])
const services = computed(() => isEn.value
? [
{
title: 'Supplier and offer discovery',
text: 'Find relevant suppliers and compare live commercial terms without tab chaos.',
},
{
title: 'Hub-first route strategy',
text: 'Evaluate delivery through key hubs and optimize route economics early.',
},
{
title: 'Map-based operating control',
text: 'Keep product, destination, and route context in one place for faster execution.',
},
{
title: 'Team workflow continuity',
text: 'Share context between buyer, operations, and manager roles without data loss.',
},
]
: [
{
title: 'Поиск поставщиков и офферов',
text: 'Находите релевантных поставщиков и сравнивайте коммерцию без хаоса вкладок.',
},
{
title: 'Маршрутная стратегия через хабы',
text: 'Оценивайте доставку через ключевые хабы и заранее оптимизируйте экономику.',
},
{
title: 'Операционный контроль на карте',
text: 'Держите товар, направление и маршрут в одном месте для быстрого исполнения.',
},
{
title: 'Непрерывный командный workflow',
text: 'Передавайте контекст между закупкой, операционкой и менеджментом без потерь.',
},
])
const advantages = computed(() => isEn.value
? [
'Single visual language from homepage to map and cabinet.',
'Faster decisions due to shared context between roles.',
'Less operational friction in procurement and route planning.',
]
: [
'Единый визуальный контур от лендинга до карты и кабинета.',
'Быстрее решения за счет общего контекста между ролями.',
'Меньше операционных потерь в закупке и планировании маршрута.',
])
const trustedBy = computed(() => isEn.value
? ['Agro Holdings', 'Food Retail', 'Import Teams', 'Distribution Groups', 'Regional Buyers', 'Logistics Partners']
: ['Агро холдинги', 'Пищевой ритейл', 'Импорт-команды', 'Дистрибьюторы', 'Региональные закупки', 'Логистические партнеры'])
const testimonials = computed(() => isEn.value
? [
{
quote: 'We reduced route decision time from days to hours because all options are visible on one map.',
author: 'Elena Morozova',
role: 'Head of Procurement Operations',
},
{
quote: 'The capsule search and hub view made supplier comparison much cleaner for our team.',
author: 'Dmitry Volkov',
role: 'Import Manager',
},
{
quote: 'Optovia removed communication noise between buyers and logistics managers.',
author: 'Alex Gromov',
role: 'CEO, Trading Company',
},
]
: [
{
quote: 'Скорость выбора маршрута сократилась с дней до часов, потому что все варианты видны на одной карте.',
author: 'Екатерина Морозова',
role: 'Руководитель закупочной операционки',
},
{
quote: 'Капсульный поиск и режим хабов сделали сравнение поставщиков заметно чище для команды.',
author: 'Дмитрий Волков',
role: 'Менеджер по импорту',
},
{
quote: 'Optovia убрала шум в коммуникации между закупкой и логистикой.',
author: 'Александр Громов',
role: 'CEO, торговая компания',
},
])
const leadTestimonial = computed(() => testimonials.value[0] ?? null)
const sideTestimonials = computed(() => testimonials.value.slice(1))
const ctaTitle = computed(() => isEn.value ? 'Scale your sourcing flow with Optovia' : 'Масштабируйте закупочный поток вместе с Optovia')
const ctaText = computed(() => isEn.value
? 'Move from fragmented tools to one coherent workflow.'
: 'Перейдите от разрозненных инструментов к единому рабочему контуру.')
definePageMeta({
layout: 'topnav',
})
</script>
<template>
<main class="landing-page">
<section class="hero-section">
<div class="mx-auto w-full max-w-[1280px] px-3 md:px-4">
<div class="mx-auto max-w-[980px] text-center text-white">
<h1 class="text-4xl font-black leading-tight md:text-6xl">{{ heroTitle }}</h1>
<p class="mx-auto mt-5 max-w-[760px] text-base text-white/80 md:text-lg">{{ heroSubtitle }}</p>
<div class="mt-8 flex flex-wrap items-center justify-center gap-3">
<NuxtLink :to="localePath('/catalog')" class="btn h-11 min-h-0 rounded-full border-0 bg-white px-6 text-[#12213a] hover:bg-white/90">
{{ isEn ? 'Open Catalog' : 'Открыть каталог' }}
</NuxtLink>
<NuxtLink :to="localePath('/catalog')" class="btn h-11 min-h-0 rounded-full border border-white/40 bg-transparent px-6 text-white hover:bg-white/10">
{{ isEn ? 'Explore map flow' : 'Посмотреть карту' }}
</NuxtLink>
</div>
</div>
</div>
</section>
<section class="section section--light">
<div class="section-inner">
<header class="section-header">
<h2>{{ isEn ? 'How it works' : 'Как это работает' }}</h2>
</header>
<ol class="steps-flow">
<li v-for="step in howSteps" :key="step.number" class="step-item">
<p class="step-number">{{ step.number }}</p>
<h3>{{ step.title }}</h3>
<p>{{ step.text }}</p>
</li>
</ol>
</div>
</section>
<section class="section section--dark">
<div class="section-inner">
<header class="section-header section-header--inverse">
<h2>{{ isEn ? 'Core capabilities' : 'Ключевые возможности' }}</h2>
</header>
<div class="service-stack">
<article v-for="(service, index) in services" :key="service.title" class="service-lane">
<p class="service-index">{{ String(index + 1).padStart(2, '0') }}</p>
<div>
<h3>{{ service.title }}</h3>
<p>{{ service.text }}</p>
</div>
</article>
</div>
</div>
</section>
<section class="section section--accent">
<div class="section-inner why-grid">
<div>
<h2>{{ isEn ? 'Why teams choose this model' : 'Почему команды выбирают эту модель' }}</h2>
<p class="why-lead">
{{ isEn ? 'The same interaction model works from landing entry to day-to-day map operations.' : 'Одна и та же модель взаимодействия работает от первого экрана до ежедневной работы на карте.' }}
</p>
</div>
<ul class="why-list">
<li v-for="item in advantages" :key="item">{{ item }}</li>
</ul>
</div>
</section>
<section class="section section--plain">
<div class="section-inner">
<header class="section-header">
<h2>{{ isEn ? 'Trusted by teams' : 'Нам доверяют команды' }}</h2>
</header>
<div class="logo-wall" role="list">
<div v-for="brand in trustedBy" :key="brand" role="listitem" class="logo-brand">{{ brand }}</div>
</div>
</div>
</section>
<section class="section section--reviews">
<div class="section-inner">
<header class="section-header">
<h2>{{ isEn ? 'What teams say' : 'Что говорят команды' }}</h2>
</header>
<div class="review-layout">
<article v-if="leadTestimonial" class="review-main">
<p class="review-main__quote">«{{ leadTestimonial.quote }}»</p>
<div class="review-person">
<div class="review-avatar review-avatar--lg">{{ leadTestimonial.author.slice(0, 1) }}</div>
<div>
<p class="review-name">{{ leadTestimonial.author }}</p>
<p class="review-role">{{ leadTestimonial.role }}</p>
</div>
</div>
</article>
<div class="review-side">
<article v-for="item in sideTestimonials" :key="item.author" class="review-mini">
<p>«{{ item.quote }}»</p>
<div class="review-person">
<div class="review-avatar">{{ item.author.slice(0, 1) }}</div>
<div>
<p class="review-name">{{ item.author }}</p>
<p class="review-role">{{ item.role }}</p>
</div>
</div>
</article>
</div>
</div>
</div>
</section>
<section class="section section--cta">
<div class="section-inner">
<div class="cta-shell">
<h2>{{ ctaTitle }}</h2>
<p>{{ ctaText }}</p>
<NuxtLink :to="localePath('/catalog')" class="btn h-11 min-h-0 rounded-full border-0 bg-white px-6 text-[#12334f] hover:bg-white/90">
{{ isEn ? 'Start now' : 'Начать сейчас' }}
</NuxtLink>
</div>
</div>
</section>
</main>
</template>
<style scoped>
.landing-page {
display: flex;
min-height: 100%;
flex-direction: column;
padding-bottom: 0;
background: #eef2f6;
}
.hero-section {
position: relative;
min-height: 72vh;
width: 100%;
background: linear-gradient(132deg, #0b3a46 0%, #132b49 48%, #1a2a63 100%);
padding: 10rem 0.75rem 2.5rem;
}
.section {
padding: 3.25rem 0;
}
.section-inner {
margin: 0 auto;
width: 100%;
max-width: 1280px;
padding: 0 0.75rem;
}
.section-header {
margin-bottom: 1.35rem;
}
.section-header h2 {
margin: 0.3rem 0 0;
font-size: clamp(1.8rem, 4vw, 3rem);
font-weight: 900;
color: #12213a;
}
.section-header--inverse h2 {
color: #fff;
}
.section--light {
background: linear-gradient(180deg, #f1f5fb 0%, #edf3fb 100%);
}
.steps-flow {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: 1rem;
}
.step-item {
position: relative;
padding: 1.2rem 0 1.2rem 4.3rem;
border-top: 1px solid #cfdae8;
}
.step-number {
position: absolute;
left: 0;
top: 0.65rem;
margin: 0;
font-size: clamp(1.8rem, 4vw, 2.7rem);
font-weight: 900;
color: #d12e35;
}
.step-item h3 {
margin: 0;
font-size: 1.35rem;
font-weight: 900;
color: #12213a;
}
.step-item p {
margin: 0.45rem 0 0;
color: #3f5673;
line-height: 1.5;
}
.section--dark {
background: linear-gradient(155deg, #0b1a2f 0%, #102842 100%);
}
.service-stack {
display: grid;
gap: 0.75rem;
}
.service-lane {
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 18px;
padding: 1.15rem;
display: grid;
gap: 0.8rem;
grid-template-columns: auto 1fr;
align-items: start;
color: #fff;
background: linear-gradient(110deg, #10243f 0%, #1c4665 100%);
}
.service-index {
margin: 0;
font-size: 1.15rem;
font-weight: 900;
color: rgba(255, 255, 255, 0.7);
}
.service-lane h3 {
margin: 0;
font-size: 1.28rem;
font-weight: 900;
}
.service-lane p {
margin: 0.45rem 0 0;
color: rgba(255, 255, 255, 0.88);
}
.section--accent {
background: linear-gradient(180deg, #f8f0ea 0%, #f4ece8 100%);
}
.why-grid {
display: grid;
gap: 1.35rem;
}
.why-grid h2 {
margin: 0.3rem 0 0;
font-size: clamp(1.8rem, 4vw, 3rem);
font-weight: 900;
color: #12213a;
}
.why-lead {
margin: 1rem 0 0;
color: #3f5673;
line-height: 1.55;
}
.why-list {
margin: 0;
padding-left: 1.2rem;
display: grid;
gap: 0.75rem;
color: #243e5c;
}
.section--plain {
background: #f1f4f8;
}
.logo-wall {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.8rem;
}
.logo-brand {
border-radius: 12px;
border: 1px solid #d3deea;
background: #fff;
padding: 0.9rem;
text-align: center;
font-weight: 700;
color: #2d4561;
}
.section--reviews {
background: #0f1f34;
}
.section--reviews .section-header h2 {
color: #fff;
}
.review-layout {
display: grid;
gap: 1rem;
}
.review-main,
.review-mini {
border-radius: 18px;
border: 1px solid rgba(255, 255, 255, 0.15);
background: rgba(255, 255, 255, 0.08);
padding: 1.2rem;
color: rgba(255, 255, 255, 0.9);
}
.review-main__quote {
margin: 0;
font-size: 1.18rem;
line-height: 1.55;
font-weight: 700;
}
.review-side {
display: grid;
gap: 0.8rem;
}
.review-person {
margin-top: 1rem;
display: flex;
align-items: center;
gap: 0.8rem;
}
.review-avatar {
height: 2.25rem;
width: 2.25rem;
border-radius: 999px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
background: rgba(255, 255, 255, 0.18);
}
.review-avatar--lg {
height: 2.75rem;
width: 2.75rem;
}
.review-name {
margin: 0;
font-weight: 700;
color: #fff;
}
.review-role {
margin: 0.2rem 0 0;
font-size: 0.86rem;
color: rgba(255, 255, 255, 0.65);
}
.section--cta {
background: #eef2f6;
}
.cta-shell {
border-radius: 24px;
padding: 2rem;
background: linear-gradient(120deg, #0f3b54 0%, #1f5b7f 100%);
color: #fff;
text-align: center;
}
.cta-shell h2 {
margin: 0;
font-size: clamp(1.8rem, 4vw, 2.8rem);
font-weight: 900;
}
.cta-shell p {
margin: 0.8rem auto 1.4rem;
max-width: 680px;
color: rgba(255, 255, 255, 0.82);
}
@media (min-width: 768px) {
.hero-section {
padding-top: 12rem;
}
.review-layout {
grid-template-columns: 1.1fr 0.9fr;
}
.logo-wall {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.why-grid {
grid-template-columns: 1fr 1fr;
align-items: start;
}
}
@media (min-width: 1024px) {
.logo-wall {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
}
</style>