mirror of
https://github.com/mmahdium/portfolio.git
synced 2026-09-30 11:31:42 +03:30
Update README and localization files to enhance project description, add error messages, and improve Persian translations.
This commit is contained in:
+323
@@ -0,0 +1,323 @@
|
||||
<template>
|
||||
<UApp>
|
||||
<div class="error-page">
|
||||
<UContainer>
|
||||
<div class="error-content">
|
||||
<!-- Animated Icon -->
|
||||
<div class="error-icon-wrapper">
|
||||
<div class="error-icon-bg"></div>
|
||||
<UIcon :name="errorIcon" class="error-icon" />
|
||||
</div>
|
||||
|
||||
<!-- Status Code -->
|
||||
<h1 class="error-code">
|
||||
{{ statusCode }}
|
||||
</h1>
|
||||
|
||||
<!-- Title -->
|
||||
<h2 class="error-title">
|
||||
{{ errorTitle }}
|
||||
</h2>
|
||||
|
||||
<!-- Message -->
|
||||
<p class="error-message">
|
||||
{{ message }}
|
||||
</p>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="error-actions">
|
||||
<UButton to="/" size="lg" color="primary" icon="i-heroicons-home" class="error-button">
|
||||
{{ $t('common.backToHome') }}
|
||||
</UButton>
|
||||
|
||||
<UButton @click="handleGoBack" size="lg" color="gray" variant="ghost" icon="i-heroicons-arrow-left"
|
||||
class="error-button">
|
||||
{{ $t('common.goBack') }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<!-- Additional Help -->
|
||||
<UCard v-if="statusCode === 404" class="error-help">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon name="i-heroicons-light-bulb" class="text-primary" />
|
||||
<span class="font-semibold">{{ $t('error.helpTitle') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ul class="error-suggestions">
|
||||
<li>{{ $t('error.suggestion1') }}</li>
|
||||
<li>{{ $t('error.suggestion2') }}</li>
|
||||
<li>{{ $t('error.suggestion3') }}</li>
|
||||
</ul>
|
||||
</UCard>
|
||||
</div>
|
||||
</UContainer>
|
||||
</div>
|
||||
</UApp>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Error Page
|
||||
*
|
||||
* This page is displayed when an error occurs during rendering.
|
||||
* It handles both 404 (Not Found) and 500 (Server Error) cases.
|
||||
*
|
||||
* Learn more: https://nuxt.com/docs/getting-started/error-handling
|
||||
*/
|
||||
|
||||
interface ErrorProps {
|
||||
error: {
|
||||
statusCode: number
|
||||
statusMessage?: string
|
||||
message?: string
|
||||
}
|
||||
}
|
||||
|
||||
const props = defineProps<ErrorProps>()
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
const statusCode = computed(() => props.error?.statusCode || 500)
|
||||
|
||||
const errorIcon = computed(() => {
|
||||
switch (statusCode.value) {
|
||||
case 404:
|
||||
return 'i-heroicons-magnifying-glass'
|
||||
case 403:
|
||||
return 'i-heroicons-lock-closed'
|
||||
case 500:
|
||||
return 'i-heroicons-exclamation-triangle'
|
||||
default:
|
||||
return 'i-heroicons-x-circle'
|
||||
}
|
||||
})
|
||||
|
||||
const errorTitle = computed(() => {
|
||||
switch (statusCode.value) {
|
||||
case 404:
|
||||
return t('error.notFoundTitle')
|
||||
case 403:
|
||||
return t('error.forbiddenTitle')
|
||||
case 500:
|
||||
return t('error.serverErrorTitle')
|
||||
default:
|
||||
return t('error.defaultTitle')
|
||||
}
|
||||
})
|
||||
|
||||
const message = computed(() => {
|
||||
if (props.error?.message) {
|
||||
return props.error.message
|
||||
}
|
||||
|
||||
switch (statusCode.value) {
|
||||
case 404:
|
||||
return t('error.notFoundMessage')
|
||||
case 403:
|
||||
return t('error.forbiddenMessage')
|
||||
case 500:
|
||||
return t('error.serverErrorMessage')
|
||||
default:
|
||||
return t('error.defaultMessage')
|
||||
}
|
||||
})
|
||||
|
||||
const handleGoBack = () => {
|
||||
if (window.history.length > 1) {
|
||||
router.back()
|
||||
} else {
|
||||
router.push('/')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.error-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem 1rem;
|
||||
background: linear-gradient(135deg, rgb(var(--color-gray-50)) 0%, rgb(var(--color-gray-100)) 100%);
|
||||
}
|
||||
|
||||
:global(.dark) .error-page {
|
||||
background: linear-gradient(135deg, rgb(var(--color-gray-950)) 0%, rgb(var(--color-gray-900)) 100%);
|
||||
}
|
||||
|
||||
.error-content {
|
||||
text-align: center;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
margin: auto !important;
|
||||
padding: 0 1rem;
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.error-icon-wrapper {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.error-icon-bg {
|
||||
position: absolute;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
background: rgb(var(--color-primary-500) / 0.1);
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
position: relative;
|
||||
font-size: 4rem;
|
||||
color: rgb(var(--color-primary-500));
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.error-code {
|
||||
font-size: 6rem;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
margin-bottom: 1rem;
|
||||
background: linear-gradient(135deg, rgb(var(--color-primary-500)), rgb(var(--color-primary-600)));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 2rem !important;
|
||||
font-weight: 700 !important;
|
||||
margin-bottom: 1rem !important;
|
||||
color: rgb(var(--color-gray-900));
|
||||
}
|
||||
|
||||
:global(.dark) .error-title {
|
||||
color: rgb(var(--color-gray-100));
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.75;
|
||||
margin-bottom: 2.5rem !important;
|
||||
color: rgb(var(--color-gray-600));
|
||||
}
|
||||
|
||||
:global(.dark) .error-message {
|
||||
color: rgb(var(--color-gray-400));
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem !important;
|
||||
justify-content: center;
|
||||
margin-bottom: 3rem !important;
|
||||
}
|
||||
|
||||
.error-button {
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.error-help {
|
||||
margin-top: 0;
|
||||
text-align: start;
|
||||
animation: fadeIn 0.8s ease-out 0.3s both;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.error-suggestions {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.error-suggestions li {
|
||||
position: relative;
|
||||
padding-left: 2.5rem !important;
|
||||
padding-right: 1rem !important;
|
||||
color: rgb(var(--color-gray-700));
|
||||
line-height: 1.7;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
:global(.dark) .error-suggestions li {
|
||||
color: rgb(var(--color-gray-300));
|
||||
}
|
||||
|
||||
.error-suggestions li::before {
|
||||
content: "→";
|
||||
position: absolute;
|
||||
left: 0.75rem;
|
||||
top: 0;
|
||||
color: rgb(var(--color-primary-500));
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.error-code {
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.error-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div class="layout-default">
|
||||
<!-- Default layout wrapper -->
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Default Layout
|
||||
*
|
||||
* This is a minimal example layout demonstrating Nuxt's layout system.
|
||||
* To use this layout in a page, add: definePageMeta({ layout: 'default' })
|
||||
*
|
||||
* Learn more: https://nuxt.com/docs/guide/directory-structure/layouts
|
||||
*/
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout-default {
|
||||
/* Add default layout styles here */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="layout-marketing">
|
||||
<!-- Marketing layout with centered content -->
|
||||
<div class="marketing-container">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Marketing Layout
|
||||
*
|
||||
* Example layout for marketing pages with centered content.
|
||||
* To use this layout in a page, add: definePageMeta({ layout: 'marketing' })
|
||||
*
|
||||
* Learn more: https://nuxt.com/docs/guide/directory-structure/layouts
|
||||
*/
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout-marketing {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.marketing-container {
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
padding: 2rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Demo Route Middleware
|
||||
*
|
||||
* This is a non-global route middleware example that demonstrates
|
||||
* Nuxt's middleware system without affecting actual navigation.
|
||||
*
|
||||
* To use this middleware on a specific page, add:
|
||||
* definePageMeta({ middleware: 'demo' })
|
||||
*
|
||||
* For global middleware, create a file with .global.ts extension.
|
||||
*
|
||||
* Learn more: https://nuxt.com/docs/guide/directory-structure/middleware
|
||||
*/
|
||||
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
// Example: Log navigation (no-op for demo purposes)
|
||||
if (import.meta.dev) {
|
||||
console.log('[Demo Middleware] Navigating from:', from.path, 'to:', to.path)
|
||||
}
|
||||
|
||||
// Example: Conditional redirect (commented out to avoid side effects)
|
||||
// if (to.path === '/restricted') {
|
||||
// return navigateTo('/')
|
||||
// }
|
||||
|
||||
// Allow navigation to continue
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
# ⚠️ Deprecated Directory
|
||||
|
||||
**This directory is deprecated and should not be used for new assets.**
|
||||
|
||||
## Why?
|
||||
|
||||
With Nuxt's `srcDir: 'app'` configuration, static assets must be placed in the **root `public/` directory**, not `app/public/`.
|
||||
|
||||
## Migration
|
||||
|
||||
All assets from this directory have been consolidated into the root `public/` folder. Please use:
|
||||
|
||||
```
|
||||
/public/
|
||||
├── favicon/
|
||||
├── fonts/
|
||||
└── img/
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [Nuxt 4.x Public Directory Documentation](https://nuxt.com/docs/4.x/guide/directory-structure/public)
|
||||
- Assets are served from `/` (e.g., `/img/logo.png` resolves to `public/img/logo.png`)
|
||||
Reference in New Issue
Block a user