feat(frontend): add branded not-found page

This commit is contained in:
2026-09-21 19:12:08 +03:30
parent be97d94ef6
commit 9328556c28
5 changed files with 115 additions and 35 deletions

View File

@@ -1,37 +1,3 @@
<script setup lang="ts">
const config = useRuntimeConfig()
</script>
<template>
<div class="min-h-dvh">
<header class="border-border bg-surface h-14 border-b shadow-[0_1px_2px_rgba(0,0,0,.03)]">
<div class="mx-auto flex h-full w-full max-w-[1120px] items-center px-4 sm:px-6 lg:px-8">
<BrandLogo />
</div>
</header>
<main class="mx-auto w-full max-w-[760px] px-4 py-10 sm:px-6 sm:py-14">
<section aria-labelledby="page-title">
<p class="text-brand mb-2 text-sm font-bold">سرویس تبدیل کارت به شبا</p>
<h1 id="page-title" class="text-foreground text-2xl leading-[1.5] font-extrabold sm:text-3xl">
شماره شبا را سریع و مطمئن پیدا کنید
</h1>
<p class="text-muted mt-3 max-w-[620px] text-sm leading-8 sm:text-[15px]">
استعلام مستقیماً از سرویس زرین‌هاب انجام می‌شود. شماره کامل کارت در گزارش‌ها یا پاسخ عمومی نگهداری نمی‌شود.
</p>
</section>
<div class="mt-7 sm:mt-8">
<CardConverter :api-base-url="config.public.apiBaseUrl" />
</div>
<p class="text-muted mt-5 flex items-start gap-2 text-xs leading-6">
<svg viewBox="0 0 24 24" class="mt-0.5 size-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z" />
<path d="m9 12 2 2 4-4" />
</svg>
برای حفاظت از اطلاعات بانکی، فقط نسخه پوشیده‌شده شماره کارت در سوابق فنی ثبت می‌شود.
</p>
</main>
</div>
<NuxtPage />
</template>

View File

@@ -0,0 +1,21 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import ErrorPage from './error.vue'
describe('ErrorPage', () => {
it('renders a branded not-found page with a way home', () => {
const wrapper = mount(ErrorPage, {
props: {
error: {
statusCode: 404,
statusMessage: 'Page not found',
},
},
})
expect(wrapper.get('h1').text()).toBe('صفحه پیدا نشد')
expect(wrapper.get('a').attributes('href')).toBe('/')
expect(wrapper.get('[role="img"]').attributes('aria-label')).toBe('زرین‌پال')
})
})

55
frontend/app/error.vue Normal file
View File

@@ -0,0 +1,55 @@
<script setup lang="ts">
import type { NuxtError } from '#app'
import { computed } from 'vue'
import BrandLogo from './components/BrandLogo.vue'
const props = defineProps<{
error: NuxtError
}>()
const isNotFound = computed(() => props.error.statusCode === 404)
</script>
<template>
<div class="min-h-dvh">
<header class="border-border bg-surface h-14 border-b shadow-[0_1px_2px_rgba(0,0,0,.03)]">
<div class="mx-auto flex h-full w-full max-w-[1120px] items-center px-4 sm:px-6 lg:px-8">
<BrandLogo />
</div>
</header>
<main class="mx-auto flex w-full max-w-[760px] items-center px-4 py-12 sm:min-h-[calc(100dvh-3.5rem)] sm:px-6 sm:py-16">
<section
class="card border-border bg-surface w-full border p-6 sm:p-8"
aria-labelledby="error-title"
>
<div class="bg-accent text-brand mb-5 grid size-12 place-items-center rounded-lg" aria-hidden="true">
<svg viewBox="0 0 24 24" class="size-6" fill="none" stroke="currentColor" stroke-width="1.8">
<path d="M12 8v5m0 3.5v.01" stroke-linecap="round" />
<circle cx="12" cy="12" r="9" />
</svg>
</div>
<p class="text-brand mb-2 text-sm font-bold">خطای {{ error.statusCode }}</p>
<h1 id="error-title" class="text-foreground text-2xl leading-[1.5] font-extrabold sm:text-3xl">
{{ isNotFound ? 'صفحه پیدا نشد' : 'خطایی رخ داده است' }}
</h1>
<p class="text-muted mt-3 max-w-[560px] text-sm leading-8 sm:text-[15px]">
{{
isNotFound
? 'نشانی واردشده معتبر نیست یا صفحه موردنظر جابه‌جا شده است.'
: 'در نمایش این صفحه مشکلی پیش آمد. لطفاً دوباره تلاش کنید.'
}}
</p>
<a
href="/"
class="bg-brand hover:bg-brand-hover text-on-brand mt-6 inline-flex min-h-11 items-center justify-center rounded-lg px-5 text-sm font-bold transition-colors"
>
بازگشت به صفحه اصلی
</a>
</section>
</main>
</div>
</template>

View File

@@ -0,0 +1,37 @@
<script setup lang="ts">
const config = useRuntimeConfig()
</script>
<template>
<div class="min-h-dvh">
<header class="border-border bg-surface h-14 border-b shadow-[0_1px_2px_rgba(0,0,0,.03)]">
<div class="mx-auto flex h-full w-full max-w-[1120px] items-center px-4 sm:px-6 lg:px-8">
<BrandLogo />
</div>
</header>
<main class="mx-auto w-full max-w-[760px] px-4 py-10 sm:px-6 sm:py-14">
<section aria-labelledby="page-title">
<p class="text-brand mb-2 text-sm font-bold">سرویس تبدیل کارت به شبا</p>
<h1 id="page-title" class="text-foreground text-2xl leading-[1.5] font-extrabold sm:text-3xl">
شماره شبا را سریع و مطمئن پیدا کنید
</h1>
<p class="text-muted mt-3 max-w-[620px] text-sm leading-8 sm:text-[15px]">
استعلام مستقیماً از سرویس زرین‌هاب انجام می‌شود. شماره کامل کارت در گزارش‌ها یا پاسخ عمومی نگهداری نمی‌شود.
</p>
</section>
<div class="mt-7 sm:mt-8">
<CardConverter :api-base-url="config.public.apiBaseUrl" />
</div>
<p class="text-muted mt-5 flex items-start gap-2 text-xs leading-6">
<svg viewBox="0 0 24 24" class="mt-0.5 size-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z" />
<path d="m9 12 2 2 4-4" />
</svg>
برای حفاظت از اطلاعات بانکی، فقط نسخه پوشیده‌شده شماره کارت در سوابق فنی ثبت می‌شود.
</p>
</main>
</div>
</template>