mirror of
https://github.com/mmahdium/portfolio.git
synced 2025-12-20 09:23:54 +01:00
Update README and localization files to enhance project description, add error messages, and improve Persian translations.
This commit is contained in:
215
README.md
215
README.md
@@ -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
323
app/error.vue
Normal 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
23
app/layouts/default.vue
Normal 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
34
app/layouts/marketing.vue
Normal 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
27
app/middleware/demo.ts
Normal 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
23
app/public/README.md
Normal 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`)
|
||||
@@ -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"
|
||||
|
||||
@@ -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": "پروژههای عمومی"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user