fix(auth): redirect callback to cabinet and align landing blocks

This commit is contained in:
Ruslan Bakiev
2026-04-21 12:46:47 +07:00
parent 7033df0fbc
commit 351125b51d
11 changed files with 258 additions and 152 deletions

View File

@@ -15,5 +15,5 @@ definePageMeta({
const { t } = useI18n()
const localePath = useLocalePath()
await navigateTo(localePath('/'))
await navigateTo(localePath('/clientarea/orders'))
</script>

View File

@@ -1,6 +1,5 @@
<script setup lang="ts">
const { locale } = useI18n()
const localePath = useLocalePath()
const isEn = computed(() => locale.value === 'en')
@@ -8,10 +7,6 @@ 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
? [
{
@@ -53,36 +48,52 @@ const services = computed(() => isEn.value
{
title: 'Supplier and offer discovery',
text: 'Find relevant suppliers and compare live commercial terms without tab chaos.',
toneFrom: '#0f243d',
toneTo: '#153962',
},
{
title: 'Hub-first route strategy',
text: 'Evaluate delivery through key hubs and optimize route economics early.',
toneFrom: '#182b45',
toneTo: '#22466f',
},
{
title: 'Map-based operating control',
text: 'Keep product, destination, and route context in one place for faster execution.',
toneFrom: '#15243a',
toneTo: '#214a60',
},
{
title: 'Team workflow continuity',
text: 'Share context between buyer, operations, and manager roles without data loss.',
toneFrom: '#122033',
toneTo: '#1d3a5c',
},
]
: [
{
title: 'Поиск поставщиков и офферов',
text: 'Находите релевантных поставщиков и сравнивайте коммерцию без хаоса вкладок.',
toneFrom: '#0f243d',
toneTo: '#153962',
},
{
title: 'Маршрутная стратегия через хабы',
text: 'Оценивайте доставку через ключевые хабы и заранее оптимизируйте экономику.',
toneFrom: '#182b45',
toneTo: '#22466f',
},
{
title: 'Операционный контроль на карте',
text: 'Держите товар, направление и маршрут в одном месте для быстрого исполнения.',
toneFrom: '#15243a',
toneTo: '#214a60',
},
{
title: 'Непрерывный командный workflow',
text: 'Передавайте контекст между закупкой, операционкой и менеджментом без потерь.',
toneFrom: '#122033',
toneTo: '#1d3a5c',
},
])
@@ -98,9 +109,16 @@ const advantages = computed(() => isEn.value
'Меньше операционных потерь в закупке и планировании маршрута.',
])
const trustedBy = computed(() => isEn.value
? ['Agro Holdings', 'Food Retail', 'Import Teams', 'Distribution Groups', 'Regional Buyers', 'Logistics Partners']
: ['Агро холдинги', 'Пищевой ритейл', 'Импорт-команды', 'Дистрибьюторы', 'Региональные закупки', 'Логистические партнеры'])
const trustedBy = [
{ name: 'Gazprom', logo: '/trust-logos/gazprom.svg' },
{ name: 'Rossiya 1', logo: '/trust-logos/russia1.svg' },
{ name: 'Absolut Bank', logo: '/trust-logos/absolutbank.svg' },
{ name: 'Kalashnikov', logo: '/trust-logos/kalashnikov.svg' },
{ name: 'Sber Logistics', logo: '/trust-logos/sberlog.svg' },
{ name: 'Dellin', logo: '/trust-logos/dellin.svg' },
{ name: 'PEK', logo: '/trust-logos/pek.svg' },
{ name: 'FESCO', logo: '/trust-logos/fesco.svg' },
] as const
const testimonials = computed(() => isEn.value
? [
@@ -108,16 +126,19 @@ 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',
avatar: 'https://randomuser.me/api/portraits/women/44.jpg',
},
{
quote: 'The capsule search and hub view made supplier comparison much cleaner for our team.',
author: 'Dmitry Volkov',
role: 'Import Manager',
avatar: 'https://randomuser.me/api/portraits/men/52.jpg',
},
{
quote: 'Optovia removed communication noise between buyers and logistics managers.',
author: 'Alex Gromov',
role: 'CEO, Trading Company',
avatar: 'https://randomuser.me/api/portraits/men/41.jpg',
},
]
: [
@@ -125,27 +146,22 @@ const testimonials = computed(() => isEn.value
quote: 'Скорость выбора маршрута сократилась с дней до часов, потому что все варианты видны на одной карте.',
author: 'Екатерина Морозова',
role: 'Руководитель закупочной операционки',
avatar: 'https://randomuser.me/api/portraits/women/44.jpg',
},
{
quote: 'Капсульный поиск и режим хабов сделали сравнение поставщиков заметно чище для команды.',
author: 'Дмитрий Волков',
role: 'Менеджер по импорту',
avatar: 'https://randomuser.me/api/portraits/men/52.jpg',
},
{
quote: 'Optovia убрала шум в коммуникации между закупкой и логистикой.',
author: 'Александр Громов',
role: 'CEO, торговая компания',
avatar: 'https://randomuser.me/api/portraits/men/41.jpg',
},
])
const leadTestimonial = computed(() => testimonials.value[0] ?? null)
const sideTestimonials = computed(() => testimonials.value.slice(1))
const ctaTitle = computed(() => isEn.value ? 'Scale your procurement flow with Optovia' : 'Масштабируйте поток закупок вместе с Optovia')
const ctaText = computed(() => isEn.value
? 'Move from fragmented tools to one coherent workflow.'
: 'Перейдите от разрозненных инструментов к единому рабочему контуру.')
definePageMeta({
layout: 'topnav',
})
@@ -153,19 +169,12 @@ definePageMeta({
<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>
<section class="relative min-h-[72vh] w-full bg-gradient-to-br from-[#0b3a46] via-[#132b49] to-[#1a2a63] px-3 pb-10 pt-40 text-white md:px-4 md:pt-52">
<div class="mx-auto w-full max-w-[1280px]">
<div class="mx-auto max-w-[940px] text-center" data-landing-search-anchor>
<h1 class="text-4xl font-black leading-tight md:text-6xl">
{{ heroTitle }}
</h1>
</div>
</div>
</section>
@@ -193,7 +202,12 @@ definePageMeta({
</header>
<div class="service-stack">
<article v-for="(service, index) in services" :key="service.title" class="service-lane">
<article
v-for="(service, index) in services"
:key="service.title"
class="service-lane"
:style="{ backgroundImage: `linear-gradient(110deg, ${service.toneFrom} 0%, ${service.toneTo} 100%)` }"
>
<p class="service-index">{{ String(index + 1).padStart(2, '0') }}</p>
<div>
<h3>{{ service.title }}</h3>
@@ -225,8 +239,10 @@ definePageMeta({
<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 class="logo-wall" role="list" :aria-label="isEn ? 'Client logos' : 'Логотипы клиентов'">
<figure v-for="brand in trustedBy" :key="brand.name" role="listitem" class="logo-brand">
<img :src="brand.logo" :alt="`Logo ${brand.name}`" loading="lazy" />
</figure>
</div>
</div>
</section>
@@ -238,44 +254,32 @@ definePageMeta({
</header>
<div class="review-layout">
<article v-if="leadTestimonial" class="review-main">
<p class="review-main__quote">«{{ leadTestimonial.quote }}»</p>
<article class="review-main">
<div class="review-person">
<div class="review-avatar review-avatar--lg">{{ leadTestimonial.author.slice(0, 1) }}</div>
<img :src="testimonials[0].avatar" :alt="testimonials[0].author" class="review-avatar review-avatar--lg" loading="lazy" />
<div>
<p class="review-name">{{ leadTestimonial.author }}</p>
<p class="review-role">{{ leadTestimonial.role }}</p>
<p class="review-name">{{ testimonials[0].author }}</p>
<p class="review-role">{{ testimonials[0].role }}</p>
</div>
</div>
<p class="review-main__quote">«{{ testimonials[0].quote }}»</p>
</article>
<div class="review-side">
<article v-for="item in sideTestimonials" :key="item.author" class="review-mini">
<p>«{{ item.quote }}»</p>
<article v-for="item in testimonials.slice(1)" :key="item.author" class="review-mini">
<div class="review-person">
<div class="review-avatar">{{ item.author.slice(0, 1) }}</div>
<img :src="item.avatar" :alt="item.author" class="review-avatar" loading="lazy" />
<div>
<p class="review-name">{{ item.author }}</p>
<p class="review-role">{{ item.role }}</p>
</div>
</div>
<p>«{{ item.quote }}»</p>
</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>
@@ -288,14 +292,6 @@ definePageMeta({
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;
}
@@ -364,7 +360,9 @@ definePageMeta({
}
.section--dark {
background: linear-gradient(155deg, #0b1a2f 0%, #102842 100%);
background:
radial-gradient(circle at 90% 15%, rgba(217, 61, 67, 0.3), rgba(217, 61, 67, 0) 34%),
linear-gradient(155deg, #0b1a2f 0%, #102842 100%);
}
.service-stack {
@@ -381,7 +379,6 @@ definePageMeta({
grid-template-columns: auto 1fr;
align-items: start;
color: #fff;
background: linear-gradient(110deg, #10243f 0%, #1c4665 100%);
}
.service-index {
@@ -403,7 +400,9 @@ definePageMeta({
}
.section--accent {
background: linear-gradient(180deg, #f8f0ea 0%, #f4ece8 100%);
background:
radial-gradient(circle at 6% 0%, rgba(251, 220, 207, 0.54), rgba(251, 220, 207, 0) 37%),
linear-gradient(180deg, #f8f0ea 0%, #f4ece8 100%);
}
.why-grid {
@@ -426,14 +425,39 @@ definePageMeta({
.why-list {
margin: 0;
padding-left: 1.2rem;
padding: 0;
list-style: none;
display: grid;
gap: 0.75rem;
color: #243e5c;
gap: 1rem;
}
.why-list li {
padding: 0.2rem 0 0.2rem 2.3rem;
color: #1d2f49;
font-weight: 700;
position: relative;
line-height: 1.45;
}
.why-list li::before {
content: '✓';
position: absolute;
left: 0;
top: 0.05rem;
width: 1.55rem;
height: 1.55rem;
border-radius: 999px;
display: flex;
align-items: center;
justify-content: center;
background: #1f8a5a;
color: #fff;
font-size: 0.9rem;
font-weight: 900;
}
.section--plain {
background: #f1f4f8;
background: #f7f9fc;
}
.logo-wall {
@@ -443,42 +467,83 @@ definePageMeta({
}
.logo-brand {
border-radius: 12px;
border: 1px solid #d3deea;
background: #fff;
padding: 0.9rem;
text-align: center;
font-weight: 700;
color: #2d4561;
margin: 0;
min-height: 84px;
border-radius: 20px;
border: 1px solid #d4dde8;
background: linear-gradient(180deg, #ffffff 0%, #f2f6fb 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 0.75rem;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
}
.logo-brand img {
width: min(88%, 240px);
height: auto;
object-fit: contain;
}
.section--reviews {
background: #0f1f34;
}
.section--reviews .section-header h2 {
color: #fff;
background: linear-gradient(180deg, #edf3fb 0%, #e9f0f8 100%);
flex: 1 0 auto;
padding-bottom: 0;
margin-bottom: 0;
}
.review-layout {
display: grid;
gap: 1rem;
gap: 0.8rem;
}
.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 {
border-radius: 24px;
padding: 1.3rem;
background: linear-gradient(145deg, #14253d 0%, #1c3b5e 100%);
color: #fff;
display: grid;
align-content: start;
gap: 0.9rem;
}
.review-person {
display: flex;
align-items: center;
gap: 0.75rem;
}
.review-avatar {
width: 2.75rem;
height: 2.75rem;
border-radius: 999px;
object-fit: cover;
border: 2px solid rgba(255, 255, 255, 0.44);
}
.review-avatar--lg {
width: 3.2rem;
height: 3.2rem;
}
.review-name {
margin: 0;
font-size: 0.95rem;
font-weight: 800;
color: #ffffff;
}
.review-role {
margin: 0.1rem 0 0;
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.82);
}
.review-main__quote {
margin: 0;
font-size: 1.18rem;
line-height: 1.55;
font-weight: 700;
color: #fff;
font-size: clamp(1.2rem, 2.6vw, 1.6rem);
line-height: 1.45;
}
.review-side {
@@ -486,87 +551,76 @@ definePageMeta({
gap: 0.8rem;
}
.review-person {
margin-top: 1rem;
display: flex;
align-items: center;
gap: 0.8rem;
.review-mini {
border-left: 4px solid #d12e35;
padding: 0.75rem 0.9rem;
background: rgba(255, 255, 255, 0.65);
}
.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-mini > p {
margin: 0.55rem 0 0;
color: #27405f;
}
.review-avatar--lg {
height: 2.75rem;
width: 2.75rem;
.review-mini .review-name {
color: #1e3555;
}
.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);
.review-mini .review-role {
color: #4f6581;
}
@media (min-width: 768px) {
.hero-section {
padding-top: 12rem;
.section {
padding: 4.5rem 0;
}
.review-layout {
grid-template-columns: 1.1fr 0.9fr;
.section-inner {
padding: 0 1rem;
}
.logo-wall {
.steps-flow {
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1.5rem;
}
.step-item {
border-top: 0;
border-left: 1px solid #cfdae8;
padding: 1rem 0 1rem 1.7rem;
}
.step-number {
position: static;
margin-bottom: 0.75rem;
}
.why-grid {
grid-template-columns: 1fr 1fr;
grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
align-items: start;
}
.why-list {
grid-template-columns: repeat(3, minmax(0, 1fr));
align-items: stretch;
gap: 1.35rem;
}
.logo-wall {
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 1rem 1.2rem;
}
.review-layout {
grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
gap: 1.2rem;
}
}
@media (min-width: 1024px) {
.logo-wall {
grid-template-columns: repeat(6, minmax(0, 1fr));
@media (max-width: 1023px) {
.landing-page {
padding-bottom: 0;
}
}
</style>

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="180" height="64" viewBox="0 0 180 64" role="img" aria-labelledby="title">
<title id="title">Absolut Bank</title>
<rect width="180" height="64" rx="18" fill="#fff8ea"/>
<circle cx="34" cy="32" r="14" fill="#d84a2f"/>
<text x="58" y="37" fill="#1f2937" font-family="Onest, Arial, sans-serif" font-size="18" font-weight="800">Absolut Bank</text>
</svg>

After

Width:  |  Height:  |  Size: 405 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="180" height="64" viewBox="0 0 180 64" role="img" aria-labelledby="title">
<title id="title">Dellin</title>
<rect width="180" height="64" rx="18" fill="#eef8f0"/>
<path d="M28 20h22v24H28z" fill="#2b8a3e"/>
<path d="M34 26h10v12H34z" fill="#ffffff"/>
<text x="62" y="38" fill="#1f2937" font-family="Onest, Arial, sans-serif" font-size="22" font-weight="800">Dellin</text>
</svg>

After

Width:  |  Height:  |  Size: 435 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="180" height="64" viewBox="0 0 180 64" role="img" aria-labelledby="title">
<title id="title">FESCO</title>
<rect width="180" height="64" rx="18" fill="#eef5ff"/>
<path d="M24 39c13-17 29-17 42 0" fill="none" stroke="#2563eb" stroke-width="7" stroke-linecap="round"/>
<text x="76" y="38" fill="#1f2937" font-family="Onest, Arial, sans-serif" font-size="23" font-weight="900">FESCO</text>
</svg>

After

Width:  |  Height:  |  Size: 448 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="180" height="64" viewBox="0 0 180 64" role="img" aria-labelledby="title">
<title id="title">Gazprom</title>
<rect width="180" height="64" rx="18" fill="#eef7ff"/>
<path d="M36 18c8 10 2 15 2 22 0 5 4 8 9 8 8 0 13-6 13-14 0-9-7-16-15-22 2 10-8 14-9 6Z" fill="#2b7de9"/>
<text x="72" y="38" fill="#1f2937" font-family="Onest, Arial, sans-serif" font-size="22" font-weight="900">Gazprom</text>
</svg>

After

Width:  |  Height:  |  Size: 453 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="180" height="64" viewBox="0 0 180 64" role="img" aria-labelledby="title">
<title id="title">Kalashnikov</title>
<rect width="180" height="64" rx="18" fill="#f5f0ea"/>
<path d="M30 20h24v8H42v16H30z" fill="#242424"/>
<text x="64" y="37" fill="#1f2937" font-family="Onest, Arial, sans-serif" font-size="17" font-weight="900">Kalashnikov</text>
</svg>

After

Width:  |  Height:  |  Size: 404 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="180" height="64" viewBox="0 0 180 64" role="img" aria-labelledby="title">
<title id="title">PEK</title>
<rect width="180" height="64" rx="18" fill="#fff2e6"/>
<rect x="26" y="20" width="34" height="24" rx="7" fill="#ef7d22"/>
<text x="74" y="39" fill="#1f2937" font-family="Onest, Arial, sans-serif" font-size="27" font-weight="900">PEK</text>
</svg>

After

Width:  |  Height:  |  Size: 406 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="180" height="64" viewBox="0 0 180 64" role="img" aria-labelledby="title">
<title id="title">Russia 1</title>
<rect width="180" height="64" rx="18" fill="#eef3ff"/>
<rect x="24" y="20" width="42" height="24" rx="7" fill="#2563eb"/>
<rect x="50" y="20" width="16" height="24" rx="5" fill="#dc2626"/>
<text x="78" y="38" fill="#1f2937" font-family="Onest, Arial, sans-serif" font-size="22" font-weight="900">Russia 1</text>
</svg>

After

Width:  |  Height:  |  Size: 485 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="180" height="64" viewBox="0 0 180 64" role="img" aria-labelledby="title">
<title id="title">SberLog</title>
<rect width="180" height="64" rx="18" fill="#edf9f2"/>
<circle cx="39" cy="32" r="15" fill="#21a038"/>
<path d="m31 31 6 6 13-15" fill="none" stroke="#ffffff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<text x="66" y="38" fill="#1f2937" font-family="Onest, Arial, sans-serif" font-size="22" font-weight="900">SberLog</text>
</svg>

After

Width:  |  Height:  |  Size: 519 B

View File

@@ -154,7 +154,8 @@ export default defineEventHandler(async (event) => {
if (normalizedPath === '/callback') {
await logto.handleSignInCallback(url.href)
await sendRedirect(event, localePrefix || '/', 302)
const clientareaPath = `${localePrefix}/clientarea`
await sendRedirect(event, clientareaPath || '/clientarea', 302)
return
}