Update README and localization files to enhance project description, add error messages, and improve Persian translations.

This commit is contained in:
Ali Arghyani
2025-11-05 12:36:02 +03:30
parent e1bc3d8adf
commit 37f5fb1aef
8 changed files with 618 additions and 87 deletions

215
README.md
View File

@@ -2,96 +2,165 @@
# Nuxt UI Portfolio · Ali Arghyani
A bilingual portfolio powered by Nuxt 4, Nuxt UI, Tailwind4, and TypeScript. The site highlights projects, experience, and recommendations with a focus on performance, accessibility, and developer experience. Dark mode is the default theme, with automatic RTL support for Persian content.
A modern, bilingual portfolio built with Nuxt 4, Nuxt UI, Tailwind CSS 4, and TypeScript. Features English and Persian (RTL) support with dark mode as default.
## Features
- Nuxt 4 application using the `app/` source directory structure
- Nuxt UI component library with custom theme tokens and chip variants
- Tailwind CSS 4 utility pipeline with custom variants and shared utilities
- Nuxt Image integration for hero and project thumbnails with responsive formats
- `@nuxtjs/i18n` for English (`en`) and Persian (`fa`) locales, including RTL switching
- `@nuxtjs/color-mode` for light/dark themes (dark is the default)
- VueUse utilities for scroll observation, reduced motion checks, and lazy mounting
- Local Roobert font preloads delivered from `public/fonts`
- Ready for deployment to Vercel (Nitro preset) or any static/edge host
🔗 **Live Demo**: [aliarghyani.vercel.app](https://aliarghyani.vercel.app)
## Tech Stack
- **Framework**: Nuxt 4 (Vue 3 + Vite)
- **UI Library**: Nuxt UI 4
- **Styling**: Tailwind CSS 4, custom CSS utilities
- **Language**: TypeScript
- **Internationalization**: `@nuxtjs/i18n`
- **Theme & Color Mode**: `@nuxtjs/color-mode`
- **Images**: `@nuxt/image`
- **Composables**: VueUse
## ✨ Features
- **Nuxt 4** - Latest framework with `app/` directory structure
- **Nuxt UI 4** - Modern component library with custom theming
- **Tailwind CSS 4** - Utility-first styling with custom variants
- **TypeScript** - Full type safety
- **i18n** - English & Persian with automatic RTL switching
- **Dark Mode** - Default theme with light mode option
- **Responsive Images** - Optimized with `@nuxt/image`
- **Performance** - Lazy loading, font preloading, reduced motion support
## <20> Quick StaTrt
## Quick Start
```bash
# Install dependencies
pnpm install
# Start development server
pnpm dev
```
- Node.js 18.20.0 or newer (up to 22.x) is required.
- Nuxt dev server runs on http://localhost:3000 by default.
### Project Scripts
- `pnpm dev` Start the Nuxt development server with HMR.
- `pnpm build` Create a production build.
- `pnpm preview` Preview the production build locally.
- `pnpm generate` Generate a fully static build (optional).
- `pnpm typecheck` Run Vue TSC for type analysis.
- `pnpm format` Check formatting with Prettier.
- `pnpm format:write` Apply Prettier formatting fixes.
Visit: http://localhost:3000
## Project Structure (excerpt)
```
app/
app.vue # Root shell, head configuration, font preloads
app.config.ts # Nuxt UI tokens, component defaults
assets/css/main.css # Tailwind entrypoint, base styles, chip utilities
components/ # UI components (TopNav, Hero, Projects, etc.)
composables/ # Reusable logic (useSectionObserver, useSocialText)
data/ # Portfolio data (localized TypeScript modules)
pages/ # Route components (index, blog placeholder)
utils/ # chipTones and other helpers
### Requirements
- Node.js 18.20.0 or newer (up to 22.x)
- pnpm (recommended) or npm
public/
fonts/ # Roobert font files served directly
img/ # Static imagery consumed by Nuxt Image
### Available Scripts
i18n/
locales/ # en.json and fa.json dictionaries
```bash
pnpm dev # Start dev server
pnpm build # Build for production
pnpm preview # Preview production build
pnpm generate # Generate static site
pnpm typecheck # Run TypeScript checks
pnpm format # Check code formatting
pnpm format:write # Fix code formatting
```
## Internationalization
- `@nuxtjs/i18n` is configured with `strategy: 'prefix_except_default'`.
- Locale metadata (language, dir, name) is defined in `nuxt.config.ts`.
- Dynamic `lang` and `dir` attributes are applied in `app/app.vue`, so toggling locales updates both SSR and client output.
- Portfolio content lives in typed modules (`app/data/portfolio.ts` for EN, `app/data/portfolio.fa.ts` for FA).
## 📁 Project Structure
## Styling & Theming
- Tailwind CSS is initialized through `app/assets/css/main.css` with a reusable chip utility, hover states, and dark mode variant.
- Nuxt UI theme tokens (`app/app.config.ts`) unify colors, typography, chip variants, and button defaults.
- Local Roobert fonts are preloaded and combined with Fraunces, Inter, and Outfit using `@nuxt/fonts`.
- Dark mode is the default preference; color mode state is stored under `nuxt-color-mode`.
```
app/ # Source directory (srcDir)
├── app.vue # Root component
├── app.config.ts # Nuxt UI theme tokens
├── error.vue # Error page (404, 500)
├── assets/css/ # Tailwind & global styles
├── components/ # Auto-imported components
│ ├── common/ # Shared UI (TopNav, Footer)
│ └── portfolio/ # Portfolio components
├── composables/ # Auto-imported composables
├── data/ # Static content (EN/FA)
├── layouts/ # Layout components
├── middleware/ # Route middleware
├── pages/ # File-based routing
├── plugins/ # Nuxt plugins
├── types/ # TypeScript definitions
└── utils/ # Helper functions
## Performance Notes
- Hero and project images use `<NuxtImg>` with responsive sizes and modern formats.
- Recommendations carousel mounts lazily once visible and respects `prefers-reduced-motion`.
- Fonts are preloaded in `app/app.vue` to stabilize LCP, and assets are served from `public/fonts`.
- Additional caching, route rules, or SWR headers can be added via `nuxt.config.ts` when deploying.
server/ # Server-side code
└── api/ # API endpoints
└── health.get.ts # Health check (/api/health)
## Deployment
### Vercel
- Set the build command to `pnpm build`.
- Output directory remains Nuxt's default (`.output`).
- Optional Plausible analytics is controlled by `runtimeConfig.public.loadPlausible`.
public/ # Static assets
├── favicon/ # Favicon files
├── fonts/ # Local fonts (Roobert, Vazirmatn)
└── img/ # Images
i18n/ # Internationalization
└── locales/ # Translation files (en.json, fa.json)
```
## 🌐 Internationalization
- **Strategy**: `prefix_except_default` (English is default, Persian uses `/fa` prefix)
- **Locales**: English (`en`) and Persian (`fa`) with RTL support
- **Content**: Separate data files for each language in `app/data/`
- **Switching**: Automatic `lang` and `dir` attributes update on locale change
## 🎨 Styling & Theming
- **Tailwind CSS 4** with custom utilities and variants
- **Nuxt UI tokens** in `app.config.ts` for consistent theming
- **Dark mode** as default with `@nuxtjs/color-mode`
- **Local fonts**: Roobert (EN) and Vazirmatn (FA) preloaded for performance
- **Custom chip utility** for tags and badges
## 🚀 Deployment
### Vercel (Recommended)
```bash
# Install Vercel CLI
npm i -g vercel
# Deploy
vercel
```
Or connect your GitHub repository to Vercel for automatic deployments.
### Static Hosting
- Run `pnpm generate`.
- Serve the contents of `.output/public`.
```bash
# Generate static files
pnpm generate
## Contributing
Issues and pull requests are welcome. Please run `pnpm typecheck` and `pnpm format` before submitting changes to keep the project consistent.
# Deploy .output/public to any static host
```
## License
This project is open source under the MIT License. See [`LICENSE`](./LICENSE) for details.
### Environment Variables
- `NUXT_PUBLIC_LOAD_PLAUSIBLE` - Enable/disable Plausible analytics (optional)
## 🧪 Testing the Structure
```bash
# Homepage
http://localhost:3000
# Persian version
http://localhost:3000/fa
# Health check API
http://localhost:3000/api/health
# 404 error page
http://localhost:3000/non-existent-page
```
## 🛠️ Tech Stack
- **Framework**: Nuxt 4 (Vue 3 + Vite)
- **UI Library**: Nuxt UI 4
- **Styling**: Tailwind CSS 4
- **Language**: TypeScript
- **i18n**: @nuxtjs/i18n
- **Theme**: @nuxtjs/color-mode
- **Images**: @nuxt/image
- **Utilities**: VueUse
## 📝 Auto-imports
Nuxt automatically imports:
- Components from `app/components/`
- Composables from `app/composables/`
- Utils from `app/utils/`
No explicit imports needed!
## 🤝 Contributing
Issues and pull requests are welcome. Please run `pnpm typecheck` and `pnpm format` before submitting changes.
## 📄 License
MIT License - See [LICENSE](./LICENSE) for details.
---
**Made with ❤️ by Ali Arghyani**

323
app/error.vue Normal file
View File

@@ -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>

23
app/layouts/default.vue Normal file
View File

@@ -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>

34
app/layouts/marketing.vue Normal file
View File

@@ -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>

27
app/middleware/demo.ts Normal file
View File

@@ -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
})

23
app/public/README.md Normal file
View File

@@ -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`)

View File

@@ -107,7 +107,23 @@
"readMore": "Read article"
},
"common": {
"present": "Present"
"present": "Present",
"backToHome": "Back to Home",
"goBack": "Go Back"
},
"error": {
"notFoundTitle": "Page Not Found",
"notFoundMessage": "The page you're looking for doesn't exist or has been moved.",
"forbiddenTitle": "Access Denied",
"forbiddenMessage": "You don't have permission to access this resource.",
"serverErrorTitle": "Server Error",
"serverErrorMessage": "Something went wrong on our end. Please try again later.",
"defaultTitle": "Oops! Something Went Wrong",
"defaultMessage": "An unexpected error occurred. Please try again.",
"helpTitle": "Need Help?",
"suggestion1": "Check the URL for typos",
"suggestion2": "Use the navigation menu to find what you're looking for",
"suggestion3": "Return to the homepage and start fresh"
},
"meta": {
"portfolioTitleSuffix": "Portfolio"

View File

@@ -3,8 +3,8 @@
"work": "سوابق کاری",
"education": "تحصیلات",
"recommendations": "توصیه‌نامه‌ها",
"projects": " پروژه ها ",
"blog": "بلاگ",
"projects": "پروژهها",
"blog": "وبلاگ",
"contact": "ارتباط",
"mainTools": "ابزارهای اصلی",
"roles": "نقش‌ها",
@@ -17,7 +17,7 @@
"expert": "حرفه‌ای",
"proficient": "مسلط",
"usedBefore": "تجربه قبلی",
"aiStack": "پشته هوش مصنوعی"
"aiStack": "استک هوش مصنوعی"
},
"softSkills": {
"problemSolving": {
@@ -58,7 +58,7 @@
}
},
"buttons": {
"blog": "نوشته‌ها",
"blog": "مقالات",
"contact": "ارتباط",
"website": "وب‌سایت",
"switchToEnglish": "English",
@@ -67,7 +67,7 @@
"nav": {
"home": "خانه",
"skills": "مهارت‌ها",
"blog": "بلاگ",
"blog": "وبلاگ",
"contact": "ارتباط",
"theme": "تم"
},
@@ -77,13 +77,29 @@
"readMore": "مطالعه"
},
"common": {
"present": "اکنون"
"present": "اکنون",
"backToHome": "بازگشت به خانه",
"goBack": "بازگشت"
},
"error": {
"notFoundTitle": "صفحه یافت نشد",
"notFoundMessage": "صفحه‌ای که به دنبال آن هستید وجود ندارد یا منتقل شده است.",
"forbiddenTitle": "دسترسی رد شد",
"forbiddenMessage": "شما اجازه دسترسی به این منبع را ندارید.",
"serverErrorTitle": "خطای سرور",
"serverErrorMessage": "مشکلی از سمت ما پیش آمده است. لطفاً بعداً دوباره تلاش کنید.",
"defaultTitle": "اوه! مشکلی پیش آمد",
"defaultMessage": "یک خطای غیرمنتظره رخ داد. لطفاً دوباره تلاش کنید.",
"helpTitle": "نیاز به کمک دارید؟",
"suggestion1": "آدرس URL را برای اشتباه تایپی بررسی کنید",
"suggestion2": "از منوی ناوبری برای یافتن آنچه می‌خواهید استفاده کنید",
"suggestion3": "به صفحه اصلی بازگردید و از نو شروع کنید"
},
"meta": {
"portfolioTitleSuffix": "پورتفولیو"
"portfolioTitleSuffix": "نمونه‌کارها"
},
"projectLabels": {
"openSource": "متن‌باز"
"openSource": "اوپن‌سورس"
},
"theme": {
"customizer": "سفارشی‌سازی تم",
@@ -132,16 +148,16 @@
}
},
"ai_stack": {
"title": "پشته هوش مصنوعی",
"title": "استک هوش مصنوعی",
"subtitle": "محیط‌های توسعه، پروتکل‌ها، مفاهیم و رویکردهایی که جریان کاری هوش مصنوعی من را توانمند می‌کنند",
"filter": {
"all": "همه",
"methods": "روش‌ها",
"ides": "IDEها",
"ides": "محیط‌های توسعه",
"assistants": "دستیارها",
"rules": "قوانین",
"mcps": "MCPها",
"extensions": "افزونهها",
"mcps": "پروتکل‌های MCP",
"extensions": "اکستنشنها",
"infra": "زیرساخت",
"evaluation": "ارزیابی"
},
@@ -164,6 +180,6 @@
"projectCategories": {
"current": "جاری",
"freelance": "پروژه‌های فریلنسری",
"public": "مخازن عمومی"
"public": "پروژه‌های عمومی"
}
}