fix(webapp): localize footer/menu and harden auth routes
This commit is contained in:
@@ -7,9 +7,9 @@ const props = withDefaults(defineProps<{
|
|||||||
const { toLocalized } = useLocalizedNavigation()
|
const { toLocalized } = useLocalizedNavigation()
|
||||||
|
|
||||||
const footerLinks = [
|
const footerLinks = [
|
||||||
{ label: 'Главная', to: '/' },
|
{ label: 'Home', to: '/' },
|
||||||
{ label: 'Заказы', to: '/clientarea/orders' },
|
{ label: 'Orders', to: '/clientarea/orders' },
|
||||||
{ label: 'Кабинет', to: '/clientarea/profile' },
|
{ label: 'Profile', to: '/clientarea/profile' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const socialLinks = [
|
const socialLinks = [
|
||||||
@@ -37,9 +37,9 @@ const currentYear = new Date().getFullYear()
|
|||||||
: 'border-white/20 bg-[rgba(11,24,42,0.26)] [backdrop-filter:blur(6px)]'"
|
: 'border-white/20 bg-[rgba(11,24,42,0.26)] [backdrop-filter:blur(6px)]'"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-[clamp(1.6rem,3.4vw,2.6rem)] font-black leading-[1.08]">Готовы к следующей поставке?</h2>
|
<h2 class="m-0 text-[clamp(1.6rem,3.4vw,2.6rem)] font-black leading-[1.08]">Ready for your next shipment?</h2>
|
||||||
<p class="mt-3 max-w-[720px] leading-6 text-white/85">
|
<p class="mt-3 max-w-[720px] leading-6 text-white/85">
|
||||||
Получайте предложения в реальном времени, сравнивайте условия и запускайте закупку прямо в Optovia.
|
Get live offers, compare terms, and launch procurement directly in Optovia.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ const currentYear = new Date().getFullYear()
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 px-1 text-[0.78rem] text-white/70">
|
<div class="mt-4 px-1 text-[0.78rem] text-white/70">
|
||||||
<p>© {{ currentYear }} Optovia. Все права защищены.</p>
|
<p>© {{ currentYear }} Optovia. All rights reserved.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -18,16 +18,22 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const { t } = useI18n()
|
||||||
const auth = useAuth()
|
const auth = useAuth()
|
||||||
const { basePath, isBasePathActive, navigateToLocalized, toLocalized } = useLocalizedNavigation()
|
const { basePath, isBasePathActive, navigateToLocalized, toLocalized } = useLocalizedNavigation()
|
||||||
const { draft: calcDraft, hydrateFromQuery, buildQuery: buildCalcQuery } = useCalcSearchDraft()
|
const { draft: calcDraft, hydrateFromQuery, buildQuery: buildCalcQuery } = useCalcSearchDraft()
|
||||||
const isLandingPage = computed(() => basePath.value === '/')
|
const isLandingPage = computed(() => basePath.value === '/')
|
||||||
const isAuthPage = computed(() => basePath.value === '/auth' || route.path === '/sign-in' || basePath.value === '/callback')
|
const isAuthPage = computed(() =>
|
||||||
|
basePath.value === '/auth'
|
||||||
|
|| basePath.value === '/sign-in'
|
||||||
|
|| basePath.value === '/sign-out'
|
||||||
|
|| basePath.value === '/callback'
|
||||||
|
)
|
||||||
const isCalcPage = computed(() => basePath.value.startsWith('/catalog'))
|
const isCalcPage = computed(() => basePath.value.startsWith('/catalog'))
|
||||||
const isManagerStagePage = computed(() => basePath.value.startsWith('/manager'))
|
const isManagerStagePage = computed(() => basePath.value.startsWith('/manager'))
|
||||||
const isDarkHeaderScene = computed(() => isLandingPage.value || isManagerStagePage.value)
|
const isDarkHeaderScene = computed(() => isLandingPage.value || isManagerStagePage.value)
|
||||||
const isAuthenticated = computed(() => props.isAuthenticated || auth.isAuthenticated.value)
|
const isAuthenticated = computed(() => props.isAuthenticated || auth.isAuthenticated.value)
|
||||||
const profileLabel = computed(() => props.profileLabel || (auth.user.value?.id ?? 'Профиль'))
|
const profileLabel = computed(() => props.profileLabel || (auth.user.value?.id ?? t('ui.profile')))
|
||||||
const showLogistics = computed(() => props.showLogistics || Boolean((auth.user.value as any)?.isAdmin))
|
const showLogistics = computed(() => props.showLogistics || Boolean((auth.user.value as any)?.isAdmin))
|
||||||
const landingSearchScrollY = ref(0)
|
const landingSearchScrollY = ref(0)
|
||||||
const landingSearchTopStart = ref(450)
|
const landingSearchTopStart = ref(450)
|
||||||
@@ -71,9 +77,16 @@ const showAdminDock = computed(() => Boolean(showLogistics.value) && !isAuthPage
|
|||||||
// Fullscreen menu
|
// Fullscreen menu
|
||||||
const isMenuOpen = ref(false)
|
const isMenuOpen = ref(false)
|
||||||
const menuLinks = computed(() => {
|
const menuLinks = computed(() => {
|
||||||
return [
|
const links: Array<{ label: string; to: string; icon: string }> = [
|
||||||
{ label: 'Мои заказы', to: '/clientarea/orders', icon: 'orders' },
|
{ label: 'Optovia', to: '/', icon: 'map' },
|
||||||
|
{ label: t('ui.calculate'), to: '/catalog', icon: 'map' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (isAuthenticated.value) {
|
||||||
|
links.push({ label: t('ui.profile'), to: '/clientarea/profile', icon: 'referral' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return links
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => route.fullPath, () => {
|
watch(() => route.fullPath, () => {
|
||||||
|
|||||||
@@ -48,12 +48,20 @@ export const useAuth = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const signIn = async (_redirectUri?: string) => {
|
const signIn = async (_redirectUri?: string) => {
|
||||||
|
if (import.meta.client) {
|
||||||
|
window.location.assign('/sign-in')
|
||||||
|
return
|
||||||
|
}
|
||||||
await navigateTo('/sign-in', { external: true })
|
await navigateTo('/sign-in', { external: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
const login = signIn
|
const login = signIn
|
||||||
|
|
||||||
const signOut = async (_logoutRedirectUri?: string) => {
|
const signOut = async (_logoutRedirectUri?: string) => {
|
||||||
|
if (import.meta.client) {
|
||||||
|
window.location.assign('/sign-out')
|
||||||
|
return
|
||||||
|
}
|
||||||
await navigateTo('/sign-out', { external: true })
|
await navigateTo('/sign-out', { external: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
export default defineNuxtRouteMiddleware(async (to) => {
|
export default defineNuxtRouteMiddleware(async (to) => {
|
||||||
|
const basePath = stripLocalePrefix(to.path, ['ru', 'en'])
|
||||||
|
|
||||||
// Skip auth routes handled by @logto/nuxt
|
// Skip auth routes handled by @logto/nuxt
|
||||||
if (to.path === '/sign-in' || to.path === '/sign-out' || to.path === '/callback') {
|
if (basePath === '/sign-in' || basePath === '/sign-out' || basePath === '/callback') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip public auth paths
|
// Skip public auth paths
|
||||||
if (to.path.startsWith('/auth/')) {
|
if (basePath.startsWith('/auth/')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ const testimonials = computed(() => isEn.value
|
|||||||
const leadTestimonial = computed(() => testimonials.value[0] ?? null)
|
const leadTestimonial = computed(() => testimonials.value[0] ?? null)
|
||||||
const sideTestimonials = computed(() => testimonials.value.slice(1))
|
const sideTestimonials = computed(() => testimonials.value.slice(1))
|
||||||
|
|
||||||
const ctaTitle = computed(() => isEn.value ? 'Scale your sourcing flow with Optovia' : 'Масштабируйте закупочный поток вместе с Optovia')
|
const ctaTitle = computed(() => isEn.value ? 'Scale your procurement flow with Optovia' : 'Масштабируйте поток закупок вместе с Optovia')
|
||||||
const ctaText = computed(() => isEn.value
|
const ctaText = computed(() => isEn.value
|
||||||
? 'Move from fragmented tools to one coherent workflow.'
|
? 'Move from fragmented tools to one coherent workflow.'
|
||||||
: 'Перейдите от разрозненных инструментов к единому рабочему контуру.')
|
: 'Перейдите от разрозненных инструментов к единому рабочему контуру.')
|
||||||
|
|||||||
@@ -23,6 +23,28 @@ const RESOURCES = [
|
|||||||
'https://billing.optovia.ru'
|
'https://billing.optovia.ru'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const LOCALE_CODES = ['ru', 'en'] as const
|
||||||
|
|
||||||
|
function resolveLocalizedPath(pathname: string) {
|
||||||
|
const trimmed = pathname === '/' ? '/' : pathname.replace(/\/+$/, '') || '/'
|
||||||
|
const segments = trimmed.split('/').filter(Boolean)
|
||||||
|
const firstSegment = segments[0]
|
||||||
|
|
||||||
|
if (firstSegment && LOCALE_CODES.includes(firstSegment as (typeof LOCALE_CODES)[number])) {
|
||||||
|
const normalized = `/${segments.slice(1).join('/')}` || '/'
|
||||||
|
const normalizedPath = normalized === '//' ? '/' : normalized
|
||||||
|
return {
|
||||||
|
localePrefix: `/${firstSegment}`,
|
||||||
|
normalizedPath: normalizedPath || '/',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
localePrefix: '',
|
||||||
|
normalizedPath: trimmed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const createSessionWrapper = (event: H3Event) => {
|
const createSessionWrapper = (event: H3Event) => {
|
||||||
const storage = useStorage('logto')
|
const storage = useStorage('logto')
|
||||||
let currentSessionId = ''
|
let currentSessionId = ''
|
||||||
@@ -60,16 +82,19 @@ const createSessionWrapper = (event: H3Event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const endpoint = process.env.NUXT_LOGTO_ENDPOINT || 'https://auth.optovia.ru'
|
const endpoint = process.env.NUXT_LOGTO_ENDPOINT || process.env.LOGTO_ENDPOINT || 'https://auth.optovia.ru'
|
||||||
const appId = process.env.NUXT_LOGTO_APP_ID || ''
|
const appId = process.env.NUXT_LOGTO_APP_ID || process.env.LOGTO_APP_ID || process.env.LOGTO_CLIENT_ID || ''
|
||||||
const appSecret = process.env.NUXT_LOGTO_APP_SECRET || ''
|
const appSecret = process.env.NUXT_LOGTO_APP_SECRET || process.env.LOGTO_APP_SECRET || process.env.LOGTO_CLIENT_SECRET || ''
|
||||||
|
const url = getRequestURL(event)
|
||||||
|
const { localePrefix, normalizedPath } = resolveLocalizedPath(url.pathname)
|
||||||
|
|
||||||
if (!appId || !appSecret) {
|
if (!appId || !appSecret) {
|
||||||
|
if (normalizedPath === '/sign-in' || normalizedPath === '/sign-out' || normalizedPath === '/callback') {
|
||||||
|
await sendRedirect(event, localePrefix || '/', 302)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = getRequestURL(event)
|
|
||||||
|
|
||||||
if (getCookie(event, LEGACY_COOKIE_NAME)) {
|
if (getCookie(event, LEGACY_COOKIE_NAME)) {
|
||||||
setCookie(event, LEGACY_COOKIE_NAME, '', { path: '/', maxAge: 0 })
|
setCookie(event, LEGACY_COOKIE_NAME, '', { path: '/', maxAge: 0 })
|
||||||
}
|
}
|
||||||
@@ -113,21 +138,23 @@ export default defineEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (url.pathname === '/sign-in') {
|
if (normalizedPath === '/sign-in') {
|
||||||
|
const callbackPath = `${localePrefix}/callback`
|
||||||
await logto.signIn({
|
await logto.signIn({
|
||||||
redirectUri: new URL('/callback', url).href
|
redirectUri: new URL(callbackPath || '/callback', url).href
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.pathname === '/sign-out') {
|
if (normalizedPath === '/sign-out') {
|
||||||
await logto.signOut(new URL('/', url).href)
|
const homePath = localePrefix || '/'
|
||||||
|
await logto.signOut(new URL(homePath, url).href)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.pathname === '/callback') {
|
if (normalizedPath === '/callback') {
|
||||||
await logto.handleSignInCallback(url.href)
|
await logto.handleSignInCallback(url.href)
|
||||||
await sendRedirect(event, '/', 302)
|
await sendRedirect(event, localePrefix || '/', 302)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user